audit logging
This commit is contained in:
parent
6e05c990d1
commit
be604b24c6
254
package-lock.json
generated
254
package-lock.json
generated
@ -25,6 +25,8 @@
|
|||||||
"csrf": "^3.1.0",
|
"csrf": "^3.1.0",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
|
"express-useragent": "^1.0.15",
|
||||||
|
"geoip-lite": "^1.4.6",
|
||||||
"image-size": "^1.0.2",
|
"image-size": "^1.0.2",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"marked": "^4.0.18",
|
"marked": "^4.0.18",
|
||||||
@ -54,6 +56,8 @@
|
|||||||
"@types/cookie-parser": "^1.4.3",
|
"@types/cookie-parser": "^1.4.3",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/express-session": "^1.17.5",
|
"@types/express-session": "^1.17.5",
|
||||||
|
"@types/express-useragent": "^1.0.2",
|
||||||
|
"@types/geoip-lite": "^1.4.1",
|
||||||
"@types/jest": "28.1.7",
|
"@types/jest": "28.1.7",
|
||||||
"@types/jsonwebtoken": "^8.5.8",
|
"@types/jsonwebtoken": "^8.5.8",
|
||||||
"@types/marked": "^4.0.4",
|
"@types/marked": "^4.0.4",
|
||||||
@ -3450,6 +3454,21 @@
|
|||||||
"@types/express": "*"
|
"@types/express": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/express-useragent": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/express-useragent/-/express-useragent-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-eUVCqMsmEO7adMJSxuAARPUxbEJLYQJATiB86bx3MGeyUOTgKNnLTfAMaF+z84DftcH6NBbFFwiRomIcsFVdUQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/geoip-lite": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geoip-lite/-/geoip-lite-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-qHH5eF3rL1wwqpzdsgMdgskfdWXxxQvJb9POJ66NK7/1l3QXsqHLpIheh9OmhtqZ2CF7AmN0sA2R4PgW8JSm7w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/graceful-fs": {
|
"node_modules/@types/graceful-fs": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
||||||
@ -4315,6 +4334,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
|
||||||
"integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw=="
|
"integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/async": {
|
||||||
|
"version": "2.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||||
|
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
@ -4783,6 +4810,14 @@
|
|||||||
"ieee754": "^1.1.13"
|
"ieee754": "^1.1.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-crc32": {
|
||||||
|
"version": "0.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||||
|
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer-equal-constant-time": {
|
"node_modules/buffer-equal-constant-time": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
@ -6347,6 +6382,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||||
},
|
},
|
||||||
|
"node_modules/express-useragent": {
|
||||||
|
"version": "1.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-useragent/-/express-useragent-1.0.15.tgz",
|
||||||
|
"integrity": "sha512-eq5xMiYCYwFPoekffMjvEIk+NWdlQY9Y38OsTyl13IvA728vKT+q/CSERYWzcw93HGBJcIqMIsZC5CZGARPVdg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express/node_modules/debug": {
|
"node_modules/express/node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
@ -6448,6 +6491,14 @@
|
|||||||
"bser": "2.1.1"
|
"bser": "2.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fd-slicer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||||
|
"dependencies": {
|
||||||
|
"pend": "~1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/figures": {
|
"node_modules/figures": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
||||||
@ -6834,6 +6885,49 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/geoip-lite": {
|
||||||
|
"version": "1.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/geoip-lite/-/geoip-lite-1.4.6.tgz",
|
||||||
|
"integrity": "sha512-JiG2zqGhFPJU/Zz//XkSfUJAaCWEz8rBi3k7RbNDEYkxGSkmguGNirJ1Q5C2ADKTMY7RqDRdxIbiX55zzZ5eJw==",
|
||||||
|
"dependencies": {
|
||||||
|
"async": "2.1 - 2.6.4",
|
||||||
|
"chalk": "4.1 - 4.1.2",
|
||||||
|
"iconv-lite": "0.4.13 - 0.6.3",
|
||||||
|
"ip-address": "5.8.9 - 5.9.4",
|
||||||
|
"lazy": "1.0.11",
|
||||||
|
"rimraf": "2.5.2 - 2.7.1",
|
||||||
|
"yauzl": "2.9.2 - 2.10.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=5.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/geoip-lite/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/geoip-lite/node_modules/rimraf": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||||
|
"dependencies": {
|
||||||
|
"glob": "^7.1.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rimraf": "bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-caller-file": {
|
"node_modules/get-caller-file": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
@ -7270,6 +7364,24 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "5.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-5.9.4.tgz",
|
||||||
|
"integrity": "sha512-dHkI3/YNJq4b/qQaz+c8LuarD3pY24JqZWfjB8aZx1gtpc2MDILu9L9jpZe1sHpzo/yWFweQVn+U//FhazUxmw==",
|
||||||
|
"dependencies": {
|
||||||
|
"jsbn": "1.1.0",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"sprintf-js": "1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ip-address/node_modules/sprintf-js": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@ -8511,6 +8623,11 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"js-yaml": "bin/js-yaml.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsbn": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
|
||||||
|
},
|
||||||
"node_modules/jsesc": {
|
"node_modules/jsesc": {
|
||||||
"version": "2.5.2",
|
"version": "2.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||||
@ -8658,6 +8775,14 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lazy": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/lazy/-/lazy-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/leven": {
|
"node_modules/leven": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||||
@ -9637,6 +9762,11 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pend": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||||
@ -12518,6 +12648,15 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yauzl": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-crc32": "~0.2.3",
|
||||||
|
"fd-slicer": "~1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yn": {
|
"node_modules/yn": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
@ -15026,6 +15165,21 @@
|
|||||||
"@types/express": "*"
|
"@types/express": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/express-useragent": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/express-useragent/-/express-useragent-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-eUVCqMsmEO7adMJSxuAARPUxbEJLYQJATiB86bx3MGeyUOTgKNnLTfAMaF+z84DftcH6NBbFFwiRomIcsFVdUQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/express": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/geoip-lite": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geoip-lite/-/geoip-lite-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-qHH5eF3rL1wwqpzdsgMdgskfdWXxxQvJb9POJ66NK7/1l3QXsqHLpIheh9OmhtqZ2CF7AmN0sA2R4PgW8JSm7w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/graceful-fs": {
|
"@types/graceful-fs": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
||||||
@ -15720,6 +15874,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
|
||||||
"integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw=="
|
"integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw=="
|
||||||
},
|
},
|
||||||
|
"async": {
|
||||||
|
"version": "2.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||||
|
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||||
|
"requires": {
|
||||||
|
"lodash": "^4.17.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"asynckit": {
|
"asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
@ -16069,6 +16231,11 @@
|
|||||||
"ieee754": "^1.1.13"
|
"ieee754": "^1.1.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"buffer-crc32": {
|
||||||
|
"version": "0.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||||
|
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="
|
||||||
|
},
|
||||||
"buffer-equal-constant-time": {
|
"buffer-equal-constant-time": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
@ -17260,6 +17427,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"express-useragent": {
|
||||||
|
"version": "1.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-useragent/-/express-useragent-1.0.15.tgz",
|
||||||
|
"integrity": "sha512-eq5xMiYCYwFPoekffMjvEIk+NWdlQY9Y38OsTyl13IvA728vKT+q/CSERYWzcw93HGBJcIqMIsZC5CZGARPVdg=="
|
||||||
|
},
|
||||||
"external-editor": {
|
"external-editor": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
|
||||||
@ -17337,6 +17509,14 @@
|
|||||||
"bser": "2.1.1"
|
"bser": "2.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fd-slicer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||||
|
"requires": {
|
||||||
|
"pend": "~1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"figures": {
|
"figures": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
||||||
@ -17628,6 +17808,39 @@
|
|||||||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"geoip-lite": {
|
||||||
|
"version": "1.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/geoip-lite/-/geoip-lite-1.4.6.tgz",
|
||||||
|
"integrity": "sha512-JiG2zqGhFPJU/Zz//XkSfUJAaCWEz8rBi3k7RbNDEYkxGSkmguGNirJ1Q5C2ADKTMY7RqDRdxIbiX55zzZ5eJw==",
|
||||||
|
"requires": {
|
||||||
|
"async": "2.1 - 2.6.4",
|
||||||
|
"chalk": "4.1 - 4.1.2",
|
||||||
|
"iconv-lite": "0.4.13 - 0.6.3",
|
||||||
|
"ip-address": "5.8.9 - 5.9.4",
|
||||||
|
"lazy": "1.0.11",
|
||||||
|
"rimraf": "2.5.2 - 2.7.1",
|
||||||
|
"yauzl": "2.9.2 - 2.10.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"requires": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rimraf": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||||
|
"requires": {
|
||||||
|
"glob": "^7.1.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"get-caller-file": {
|
"get-caller-file": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
@ -17933,6 +18146,23 @@
|
|||||||
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
|
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"ip-address": {
|
||||||
|
"version": "5.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-5.9.4.tgz",
|
||||||
|
"integrity": "sha512-dHkI3/YNJq4b/qQaz+c8LuarD3pY24JqZWfjB8aZx1gtpc2MDILu9L9jpZe1sHpzo/yWFweQVn+U//FhazUxmw==",
|
||||||
|
"requires": {
|
||||||
|
"jsbn": "1.1.0",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"sprintf-js": "1.1.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"sprintf-js": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"ipaddr.js": {
|
"ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@ -18870,6 +19100,11 @@
|
|||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jsbn": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
|
||||||
|
},
|
||||||
"jsesc": {
|
"jsesc": {
|
||||||
"version": "2.5.2",
|
"version": "2.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||||
@ -18989,6 +19224,11 @@
|
|||||||
"integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==",
|
"integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"lazy": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/lazy/-/lazy-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA=="
|
||||||
|
},
|
||||||
"leven": {
|
"leven": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||||
@ -19737,6 +19977,11 @@
|
|||||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"pend": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
|
||||||
|
},
|
||||||
"picocolors": {
|
"picocolors": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||||
@ -21762,6 +22007,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
|
||||||
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="
|
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="
|
||||||
},
|
},
|
||||||
|
"yauzl": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||||
|
"requires": {
|
||||||
|
"buffer-crc32": "~0.2.3",
|
||||||
|
"fd-slicer": "~1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"yn": {
|
"yn": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
@ -40,6 +40,8 @@
|
|||||||
"csrf": "^3.1.0",
|
"csrf": "^3.1.0",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
|
"express-useragent": "^1.0.15",
|
||||||
|
"geoip-lite": "^1.4.6",
|
||||||
"image-size": "^1.0.2",
|
"image-size": "^1.0.2",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"marked": "^4.0.18",
|
"marked": "^4.0.18",
|
||||||
@ -69,6 +71,8 @@
|
|||||||
"@types/cookie-parser": "^1.4.3",
|
"@types/cookie-parser": "^1.4.3",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/express-session": "^1.17.5",
|
"@types/express-session": "^1.17.5",
|
||||||
|
"@types/express-useragent": "^1.0.2",
|
||||||
|
"@types/geoip-lite": "^1.4.1",
|
||||||
"@types/jest": "28.1.7",
|
"@types/jest": "28.1.7",
|
||||||
"@types/jsonwebtoken": "^8.5.8",
|
"@types/jsonwebtoken": "^8.5.8",
|
||||||
"@types/marked": "^4.0.4",
|
"@types/marked": "^4.0.4",
|
||||||
|
50
src/fe/scss/_logins.scss
Normal file
50
src/fe/scss/_logins.scss
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
.login {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__current {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__indicator {
|
||||||
|
position: absolute;
|
||||||
|
left: -16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 100%;
|
||||||
|
|
||||||
|
&--current {
|
||||||
|
background-color: rgb(0, 190, 63);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--other {
|
||||||
|
background-color: rgb(0, 136, 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__using {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@
|
|||||||
@import 'authorize';
|
@import 'authorize';
|
||||||
@import 'settings';
|
@import 'settings';
|
||||||
@import 'modal';
|
@import 'modal';
|
||||||
|
@import 'logins';
|
||||||
|
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
|
@ -18,4 +18,9 @@ import { ModalManager } from './modal/modals';
|
|||||||
const modals = new ModalManager();
|
const modals = new ModalManager();
|
||||||
const avatar = new AvatarModal();
|
const avatar = new AvatarModal();
|
||||||
modals.register(avatar);
|
modals.register(avatar);
|
||||||
|
|
||||||
|
const dateify = document.querySelectorAll('[data-locale-time]');
|
||||||
|
dateify.forEach((element: HTMLElement) => {
|
||||||
|
element.innerText = new Date(element.innerText).toString();
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
36
src/modules/objects/audit/audit.entity.ts
Normal file
36
src/modules/objects/audit/audit.entity.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { User } from '../user/user.entity';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class AuditLog {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: false })
|
||||||
|
action: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
actor_ip: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
actor_ua: string;
|
||||||
|
|
||||||
|
/** Potentially unwanted behavior will be flagged by the system */
|
||||||
|
@Column({ default: false })
|
||||||
|
flagged: boolean;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, { nullable: true, onDelete: 'SET NULL' })
|
||||||
|
public actor: User;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
public created_at: Date;
|
||||||
|
}
|
10
src/modules/objects/audit/audit.enum.ts
Normal file
10
src/modules/objects/audit/audit.enum.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export enum AuditAction {
|
||||||
|
LOGIN = 'login',
|
||||||
|
REGISTRATION = 'registration',
|
||||||
|
TOTP_ACTIVATE = 'totp_activate',
|
||||||
|
TOTP_DEACTIVATE = 'totp_deactivate',
|
||||||
|
PASSWORD_CHANGE = 'password_change',
|
||||||
|
EMAIL_CHANGE = 'email_change',
|
||||||
|
MALICIOUS_REQUEST = 'malicious_request',
|
||||||
|
THROTTLE = 'throttle',
|
||||||
|
}
|
12
src/modules/objects/audit/audit.module.ts
Normal file
12
src/modules/objects/audit/audit.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigurationModule } from 'src/modules/config/config.module';
|
||||||
|
import { DatabaseModule } from '../database/database.module';
|
||||||
|
import { auditProviders } from './audit.providers';
|
||||||
|
import { AuditService } from './audit.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [DatabaseModule, ConfigurationModule],
|
||||||
|
exports: [AuditService],
|
||||||
|
providers: [...auditProviders, AuditService],
|
||||||
|
})
|
||||||
|
export class AuditModule {}
|
11
src/modules/objects/audit/audit.providers.ts
Normal file
11
src/modules/objects/audit/audit.providers.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { FactoryProvider } from '@nestjs/common';
|
||||||
|
import { DataSource, Repository } from 'typeorm';
|
||||||
|
import { AuditLog } from './audit.entity';
|
||||||
|
|
||||||
|
export const auditProviders: FactoryProvider<Repository<AuditLog>>[] = [
|
||||||
|
{
|
||||||
|
provide: 'AUDIT_REPOSITORY',
|
||||||
|
useFactory: (dataSource: DataSource) => dataSource.getRepository(AuditLog),
|
||||||
|
inject: ['DATA_SOURCE'],
|
||||||
|
},
|
||||||
|
];
|
146
src/modules/objects/audit/audit.service.ts
Normal file
146
src/modules/objects/audit/audit.service.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { User } from '../user/user.entity';
|
||||||
|
import { AuditLog } from './audit.entity';
|
||||||
|
import { AuditAction } from './audit.enum';
|
||||||
|
import { lookup, Lookup } from 'geoip-lite';
|
||||||
|
import { parse, Details } from 'express-useragent';
|
||||||
|
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
||||||
|
|
||||||
|
export interface UserLoginEntry {
|
||||||
|
login_at: Date;
|
||||||
|
current: boolean;
|
||||||
|
location: Partial<Lookup>;
|
||||||
|
user_agent: Partial<Details>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuditService {
|
||||||
|
constructor(
|
||||||
|
@Inject('AUDIT_REPOSITORY')
|
||||||
|
private readonly audit: Repository<AuditLog>,
|
||||||
|
private readonly form: FormUtilityService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async insertAudit(
|
||||||
|
action: AuditAction,
|
||||||
|
comment?: string,
|
||||||
|
user?: User,
|
||||||
|
ip?: string,
|
||||||
|
ua?: string,
|
||||||
|
) {
|
||||||
|
const audit = new AuditLog();
|
||||||
|
audit.action = action as string;
|
||||||
|
audit.content = comment;
|
||||||
|
audit.actor_ip = ip;
|
||||||
|
audit.actor_ua = ua;
|
||||||
|
audit.actor = user;
|
||||||
|
|
||||||
|
if (
|
||||||
|
action === AuditAction.MALICIOUS_REQUEST ||
|
||||||
|
action === AuditAction.THROTTLE
|
||||||
|
) {
|
||||||
|
audit.flagged = true;
|
||||||
|
// TODO: email administrator
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.updateAudit(audit);
|
||||||
|
return audit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async auditRequest(
|
||||||
|
req: Request,
|
||||||
|
type: AuditAction,
|
||||||
|
comment?: string,
|
||||||
|
user?: User,
|
||||||
|
) {
|
||||||
|
return this.insertAudit(
|
||||||
|
type,
|
||||||
|
comment,
|
||||||
|
user || req.user || null,
|
||||||
|
req.ip,
|
||||||
|
req.header('user-agent'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getIPLocation(ip: string) {
|
||||||
|
return lookup(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUserAgentInfo(ua: string) {
|
||||||
|
return parse(ua);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getUserLogins(
|
||||||
|
user: User,
|
||||||
|
sessid?: string,
|
||||||
|
): Promise<UserLoginEntry[]> {
|
||||||
|
const userLogins: UserLoginEntry[] = [];
|
||||||
|
const auditEntries = await this.audit.find({
|
||||||
|
where: { actor: { id: user.id }, action: AuditAction.LOGIN },
|
||||||
|
order: { created_at: 'DESC' },
|
||||||
|
take: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
auditEntries.forEach((entry) => {
|
||||||
|
userLogins.push({
|
||||||
|
login_at: entry.created_at,
|
||||||
|
current: sessid === entry.content,
|
||||||
|
location: entry.actor_ip
|
||||||
|
? this.form.pluckObject(this.getIPLocation(entry.actor_ip), [
|
||||||
|
'country',
|
||||||
|
'city',
|
||||||
|
'timezone',
|
||||||
|
'll',
|
||||||
|
])
|
||||||
|
: null,
|
||||||
|
user_agent: entry.actor_ua
|
||||||
|
? this.form.pluckObject(this.getUserAgentInfo(entry.actor_ua), [
|
||||||
|
'browser',
|
||||||
|
'version',
|
||||||
|
'os',
|
||||||
|
'platform',
|
||||||
|
])
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return userLogins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getUserAccountCreation(user: User) {
|
||||||
|
const auditEntry = await this.audit.findOne({
|
||||||
|
where: { actor: { id: user.id }, action: AuditAction.REGISTRATION },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!auditEntry) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
created_at: auditEntry.created_at,
|
||||||
|
ip: auditEntry.actor_ip,
|
||||||
|
location: auditEntry.actor_ip
|
||||||
|
? this.form.pluckObject(this.getIPLocation(auditEntry.actor_ip), [
|
||||||
|
'country',
|
||||||
|
'city',
|
||||||
|
'timezone',
|
||||||
|
'll',
|
||||||
|
])
|
||||||
|
: null,
|
||||||
|
user_agent: auditEntry.actor_ua
|
||||||
|
? this.form.pluckObject(this.getUserAgentInfo(auditEntry.actor_ua), [
|
||||||
|
'browser',
|
||||||
|
'version',
|
||||||
|
'os',
|
||||||
|
'platform',
|
||||||
|
])
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateAudit(audit: AuditLog): Promise<void> {
|
||||||
|
await this.audit.save(audit);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigurationModule } from '../config/config.module';
|
import { ConfigurationModule } from '../config/config.module';
|
||||||
|
|
||||||
|
import { AuditModule } from './audit/audit.module';
|
||||||
import { DatabaseModule } from './database/database.module';
|
import { DatabaseModule } from './database/database.module';
|
||||||
import { DocumentModule } from './document/document.module';
|
import { DocumentModule } from './document/document.module';
|
||||||
import { EmailModule } from './email/email.module';
|
import { EmailModule } from './email/email.module';
|
||||||
@ -21,6 +22,7 @@ import { UserModule } from './user/user.module';
|
|||||||
UploadModule,
|
UploadModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
DocumentModule,
|
DocumentModule,
|
||||||
|
AuditModule,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
DatabaseModule,
|
DatabaseModule,
|
||||||
@ -31,6 +33,7 @@ import { UserModule } from './user/user.module';
|
|||||||
UploadModule,
|
UploadModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
DocumentModule,
|
DocumentModule,
|
||||||
|
AuditModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ObjectsModule {}
|
export class ObjectsModule {}
|
||||||
|
@ -12,6 +12,8 @@ import {
|
|||||||
import { Throttle } from '@nestjs/throttler';
|
import { Throttle } from '@nestjs/throttler';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { SessionData } from 'express-session';
|
import { SessionData } from 'express-session';
|
||||||
|
import { AuditAction } from 'src/modules/objects/audit/audit.enum';
|
||||||
|
import { AuditService } from 'src/modules/objects/audit/audit.service';
|
||||||
import {
|
import {
|
||||||
UserToken,
|
UserToken,
|
||||||
UserTokenType,
|
UserTokenType,
|
||||||
@ -31,6 +33,7 @@ export class LoginController {
|
|||||||
private readonly userTokenService: UserTokenService,
|
private readonly userTokenService: UserTokenService,
|
||||||
private readonly formUtil: FormUtilityService,
|
private readonly formUtil: FormUtilityService,
|
||||||
private readonly token: TokenService,
|
private readonly token: TokenService,
|
||||||
|
private readonly audit: AuditService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ -74,6 +77,8 @@ export class LoginController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.audit.auditRequest(req, AuditAction.LOGIN, req.session.id, user);
|
||||||
|
|
||||||
if (await this.totpService.userHasTOTP(user)) {
|
if (await this.totpService.userHasTOTP(user)) {
|
||||||
const challenge = { type: 'verify', user: user.uuid, remember };
|
const challenge = { type: 'verify', user: user.uuid, remember };
|
||||||
const encrypted = await this.token.encryptChallenge(challenge);
|
const encrypted = await this.token.encryptChallenge(challenge);
|
||||||
@ -336,6 +341,7 @@ export class LoginController {
|
|||||||
|
|
||||||
await this.userService.updateUser(token.user);
|
await this.userService.updateUser(token.user);
|
||||||
await this.userTokenService.delete(token);
|
await this.userTokenService.delete(token);
|
||||||
|
await this.audit.auditRequest(req, AuditAction.PASSWORD_CHANGE, 'token');
|
||||||
|
|
||||||
req.flash('message', {
|
req.flash('message', {
|
||||||
error: false,
|
error: false,
|
||||||
|
@ -8,12 +8,13 @@ import { CSRFMiddleware } from 'src/middleware/csrf.middleware';
|
|||||||
import { FlashMiddleware } from 'src/middleware/flash.middleware';
|
import { FlashMiddleware } from 'src/middleware/flash.middleware';
|
||||||
import { UserMiddleware } from 'src/middleware/user.middleware';
|
import { UserMiddleware } from 'src/middleware/user.middleware';
|
||||||
import { ValidateCSRFMiddleware } from 'src/middleware/validate-csrf.middleware';
|
import { ValidateCSRFMiddleware } from 'src/middleware/validate-csrf.middleware';
|
||||||
|
import { AuditModule } from 'src/modules/objects/audit/audit.module';
|
||||||
import { UserTokenModule } from 'src/modules/objects/user-token/user-token.module';
|
import { UserTokenModule } from 'src/modules/objects/user-token/user-token.module';
|
||||||
import { UserModule } from 'src/modules/objects/user/user.module';
|
import { UserModule } from 'src/modules/objects/user/user.module';
|
||||||
import { LoginController } from './login.controller';
|
import { LoginController } from './login.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UserModule, UserTokenModule],
|
imports: [UserModule, UserTokenModule, AuditModule],
|
||||||
controllers: [LoginController],
|
controllers: [LoginController],
|
||||||
})
|
})
|
||||||
export class LoginModule implements NestModule {
|
export class LoginModule implements NestModule {
|
||||||
|
@ -12,6 +12,8 @@ import {
|
|||||||
import { Throttle } from '@nestjs/throttler';
|
import { Throttle } from '@nestjs/throttler';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { ConfigurationService } from 'src/modules/config/config.service';
|
import { ConfigurationService } from 'src/modules/config/config.service';
|
||||||
|
import { AuditAction } from 'src/modules/objects/audit/audit.enum';
|
||||||
|
import { AuditService } from 'src/modules/objects/audit/audit.service';
|
||||||
import { UserService } from 'src/modules/objects/user/user.service';
|
import { UserService } from 'src/modules/objects/user/user.service';
|
||||||
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
||||||
import { RegisterDto } from './register.interfaces';
|
import { RegisterDto } from './register.interfaces';
|
||||||
@ -22,6 +24,7 @@ export class RegisterController {
|
|||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly formUtil: FormUtilityService,
|
private readonly formUtil: FormUtilityService,
|
||||||
private readonly config: ConfigurationService,
|
private readonly config: ConfigurationService,
|
||||||
|
private readonly audit: AuditService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ -87,7 +90,8 @@ export class RegisterController {
|
|||||||
throw new Error('The passwords do not match!');
|
throw new Error('The passwords do not match!');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.userService.userRegistration(body, redirectTo);
|
const user = await this.userService.userRegistration(body, redirectTo);
|
||||||
|
await this.audit.auditRequest(req, AuditAction.REGISTRATION, null, user);
|
||||||
|
|
||||||
req.flash('message', {
|
req.flash('message', {
|
||||||
error: false,
|
error: false,
|
||||||
|
@ -8,11 +8,12 @@ import { CSRFMiddleware } from 'src/middleware/csrf.middleware';
|
|||||||
import { FlashMiddleware } from 'src/middleware/flash.middleware';
|
import { FlashMiddleware } from 'src/middleware/flash.middleware';
|
||||||
import { UserMiddleware } from 'src/middleware/user.middleware';
|
import { UserMiddleware } from 'src/middleware/user.middleware';
|
||||||
import { ValidateCSRFMiddleware } from 'src/middleware/validate-csrf.middleware';
|
import { ValidateCSRFMiddleware } from 'src/middleware/validate-csrf.middleware';
|
||||||
|
import { AuditModule } from 'src/modules/objects/audit/audit.module';
|
||||||
import { UserModule } from 'src/modules/objects/user/user.module';
|
import { UserModule } from 'src/modules/objects/user/user.module';
|
||||||
import { RegisterController } from './register.controller';
|
import { RegisterController } from './register.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UserModule],
|
imports: [UserModule, AuditModule],
|
||||||
controllers: [RegisterController],
|
controllers: [RegisterController],
|
||||||
})
|
})
|
||||||
export class RegisterModule implements NestModule {
|
export class RegisterModule implements NestModule {
|
||||||
|
@ -18,6 +18,8 @@ import { FileInterceptor } from '@nestjs/platform-express';
|
|||||||
import { Throttle } from '@nestjs/throttler';
|
import { Throttle } from '@nestjs/throttler';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { unlink } from 'fs/promises';
|
import { unlink } from 'fs/promises';
|
||||||
|
import { AuditAction } from 'src/modules/objects/audit/audit.enum';
|
||||||
|
import { AuditService } from 'src/modules/objects/audit/audit.service';
|
||||||
import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service';
|
import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service';
|
||||||
import { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service';
|
import { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service';
|
||||||
import { UploadService } from 'src/modules/objects/upload/upload.service';
|
import { UploadService } from 'src/modules/objects/upload/upload.service';
|
||||||
@ -38,6 +40,7 @@ export class SettingsController {
|
|||||||
private readonly _totp: UserTOTPService,
|
private readonly _totp: UserTOTPService,
|
||||||
private readonly _client: OAuth2ClientService,
|
private readonly _client: OAuth2ClientService,
|
||||||
private readonly _oaToken: OAuth2TokenService,
|
private readonly _oaToken: OAuth2TokenService,
|
||||||
|
private readonly _audit: AuditService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ -231,6 +234,11 @@ export class SettingsController {
|
|||||||
const newPassword = await this._user.hashPassword(new_password);
|
const newPassword = await this._user.hashPassword(new_password);
|
||||||
req.user.password = newPassword;
|
req.user.password = newPassword;
|
||||||
await this._user.updateUser(req.user);
|
await this._user.updateUser(req.user);
|
||||||
|
await this._audit.auditRequest(
|
||||||
|
req,
|
||||||
|
AuditAction.PASSWORD_CHANGE,
|
||||||
|
'settings',
|
||||||
|
);
|
||||||
|
|
||||||
req.flash('message', {
|
req.flash('message', {
|
||||||
error: false,
|
error: false,
|
||||||
@ -291,6 +299,7 @@ export class SettingsController {
|
|||||||
|
|
||||||
req.user.email = email;
|
req.user.email = email;
|
||||||
await this._user.updateUser(req.user);
|
await this._user.updateUser(req.user);
|
||||||
|
await this._audit.auditRequest(req, AuditAction.EMAIL_CHANGE, 'settings');
|
||||||
|
|
||||||
req.flash('message', {
|
req.flash('message', {
|
||||||
error: false,
|
error: false,
|
||||||
@ -311,4 +320,15 @@ export class SettingsController {
|
|||||||
|
|
||||||
req.session.destroy(() => res.redirect('/login'));
|
req.session.destroy(() => res.redirect('/login'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('logins')
|
||||||
|
@Render('login-list')
|
||||||
|
public async userLogins(@Req() req: Request) {
|
||||||
|
const logins = await this._audit.getUserLogins(req.user, req.session.id);
|
||||||
|
const creation = await this._audit.getUserAccountCreation(req.user);
|
||||||
|
return this._form.populateTemplate(req, {
|
||||||
|
logins,
|
||||||
|
creation,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import { SettingsController } from './settings.controller';
|
|||||||
import { SettingsService } from './settings.service';
|
import { SettingsService } from './settings.service';
|
||||||
import { CSRFMiddleware } from 'src/middleware/csrf.middleware';
|
import { CSRFMiddleware } from 'src/middleware/csrf.middleware';
|
||||||
import { UserMiddleware } from 'src/middleware/user.middleware';
|
import { UserMiddleware } from 'src/middleware/user.middleware';
|
||||||
|
import { AuditModule } from 'src/modules/objects/audit/audit.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [SettingsController],
|
controllers: [SettingsController],
|
||||||
@ -31,6 +32,7 @@ import { UserMiddleware } from 'src/middleware/user.middleware';
|
|||||||
UploadModule,
|
UploadModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
UserTokenModule,
|
UserTokenModule,
|
||||||
|
AuditModule,
|
||||||
OAuth2Module,
|
OAuth2Module,
|
||||||
OAuth2ClientModule,
|
OAuth2ClientModule,
|
||||||
OAuth2TokenModule,
|
OAuth2TokenModule,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { Body, Controller, Get, Post, Req, Res } from '@nestjs/common';
|
import { Body, Controller, Get, Post, Req, Res } from '@nestjs/common';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import { AuditAction } from 'src/modules/objects/audit/audit.enum';
|
||||||
|
import { AuditService } from 'src/modules/objects/audit/audit.service';
|
||||||
import { UserTOTPService } from 'src/modules/objects/user-token/user-totp-token.service';
|
import { UserTOTPService } from 'src/modules/objects/user-token/user-totp-token.service';
|
||||||
import { UserService } from 'src/modules/objects/user/user.service';
|
import { UserService } from 'src/modules/objects/user/user.service';
|
||||||
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
|
||||||
@ -14,6 +16,7 @@ export class TwoFactorController {
|
|||||||
private token: TokenService,
|
private token: TokenService,
|
||||||
private user: UserService,
|
private user: UserService,
|
||||||
private form: FormUtilityService,
|
private form: FormUtilityService,
|
||||||
|
private audit: AuditService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('activate')
|
@Get('activate')
|
||||||
@ -92,6 +95,7 @@ export class TwoFactorController {
|
|||||||
|
|
||||||
// TODO: show the recovery tokens to the user
|
// TODO: show the recovery tokens to the user
|
||||||
await this.totp.activateTOTP(req.user, secret);
|
await this.totp.activateTOTP(req.user, secret);
|
||||||
|
await this.audit.auditRequest(req, AuditAction.TOTP_ACTIVATE);
|
||||||
req.flash('message', {
|
req.flash('message', {
|
||||||
error: false,
|
error: false,
|
||||||
text: 'Two-factor authenticator has been enabled successfully. Your account is now more secure!',
|
text: 'Two-factor authenticator has been enabled successfully. Your account is now more secure!',
|
||||||
@ -132,6 +136,7 @@ export class TwoFactorController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.totp.deactivateTOTP(twoFA);
|
await this.totp.deactivateTOTP(twoFA);
|
||||||
|
await this.audit.auditRequest(req, AuditAction.TOTP_DEACTIVATE);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
req.flash('message', {
|
req.flash('message', {
|
||||||
error: true,
|
error: true,
|
||||||
|
@ -3,12 +3,13 @@ import { AuthMiddleware } from 'src/middleware/auth.middleware';
|
|||||||
import { CSRFMiddleware } from 'src/middleware/csrf.middleware';
|
import { CSRFMiddleware } from 'src/middleware/csrf.middleware';
|
||||||
import { FlashMiddleware } from 'src/middleware/flash.middleware';
|
import { FlashMiddleware } from 'src/middleware/flash.middleware';
|
||||||
import { UserMiddleware } from 'src/middleware/user.middleware';
|
import { UserMiddleware } from 'src/middleware/user.middleware';
|
||||||
|
import { AuditModule } from 'src/modules/objects/audit/audit.module';
|
||||||
import { UserTokenModule } from 'src/modules/objects/user-token/user-token.module';
|
import { UserTokenModule } from 'src/modules/objects/user-token/user-token.module';
|
||||||
import { UserModule } from 'src/modules/objects/user/user.module';
|
import { UserModule } from 'src/modules/objects/user/user.module';
|
||||||
import { TwoFactorController } from './two-factor.controller';
|
import { TwoFactorController } from './two-factor.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UserModule, UserTokenModule],
|
imports: [UserModule, UserTokenModule, AuditModule],
|
||||||
controllers: [TwoFactorController],
|
controllers: [TwoFactorController],
|
||||||
})
|
})
|
||||||
export class TwoFactorModule implements NestModule {
|
export class TwoFactorModule implements NestModule {
|
||||||
|
@ -40,6 +40,7 @@ export class FormUtilityService {
|
|||||||
* @returns Stripped object
|
* @returns Stripped object
|
||||||
*/
|
*/
|
||||||
public stripObject<T>(object: T, keys: string[]): T {
|
public stripObject<T>(object: T, keys: string[]): T {
|
||||||
|
if (!object) return null;
|
||||||
return keys.reduce<T>((obj, field) => {
|
return keys.reduce<T>((obj, field) => {
|
||||||
delete obj[field];
|
delete obj[field];
|
||||||
return obj;
|
return obj;
|
||||||
@ -53,6 +54,7 @@ export class FormUtilityService {
|
|||||||
* @returns Plucked object
|
* @returns Plucked object
|
||||||
*/
|
*/
|
||||||
public pluckObject<T>(object: T, keys: string[]): Partial<T> {
|
public pluckObject<T>(object: T, keys: string[]): Partial<T> {
|
||||||
|
if (!object) return null;
|
||||||
return Object.keys(object).reduce<Partial<T>>((obj, field) => {
|
return Object.keys(object).reduce<Partial<T>>((obj, field) => {
|
||||||
if (keys.includes(field)) {
|
if (keys.includes(field)) {
|
||||||
obj[field] = object[field];
|
obj[field] = object[field];
|
||||||
@ -68,6 +70,7 @@ export class FormUtilityService {
|
|||||||
* @returns Stripped object
|
* @returns Stripped object
|
||||||
*/
|
*/
|
||||||
public stripObjectArray<T>(array: T[], keys: string[]): T[] {
|
public stripObjectArray<T>(array: T[], keys: string[]): T[] {
|
||||||
|
if (!array) return null;
|
||||||
return array.map((object) => this.stripObject(object, keys));
|
return array.map((object) => this.stripObject(object, keys));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,6 +81,7 @@ export class FormUtilityService {
|
|||||||
* @returns Plucked object
|
* @returns Plucked object
|
||||||
*/
|
*/
|
||||||
public pluckObjectArray<T>(array: T[], keys: string[]): Partial<T>[] {
|
public pluckObjectArray<T>(array: T[], keys: string[]): Partial<T>[] {
|
||||||
|
if (!array) return null;
|
||||||
return array.map((object) => this.pluckObject(object, keys));
|
return array.map((object) => this.pluckObject(object, keys));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
45
views/login-list.pug
Normal file
45
views/login-list.pug
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
extends partials/layout.pug
|
||||||
|
|
||||||
|
block title
|
||||||
|
|Login history | Icy Network
|
||||||
|
|
||||||
|
block body
|
||||||
|
include partials/logo.pug
|
||||||
|
div.container
|
||||||
|
div.center-box
|
||||||
|
h1 Login history
|
||||||
|
|
||||||
|
div.login__list
|
||||||
|
each login in logins
|
||||||
|
div.login
|
||||||
|
if login.current
|
||||||
|
div.login__indicator.login__indicator--current
|
||||||
|
else
|
||||||
|
div.login__indicator.login__indicator--other
|
||||||
|
div.login__title
|
||||||
|
p Login at
|
||||||
|
span(data-locale-time) #{login.login_at.toISOString()}
|
||||||
|
if login.current
|
||||||
|
span.login__current (current)
|
||||||
|
div.login__using
|
||||||
|
if login.location
|
||||||
|
span.login__location near #{login.location.city}, #{login.location.country}
|
||||||
|
|
|
||||||
|
if login.user_agent
|
||||||
|
span.login__browser using #{login.user_agent.browser} #{login.user_agent.version} on #{login.user_agent.platform} (#{login.user_agent.os})
|
||||||
|
if creation
|
||||||
|
div.login
|
||||||
|
div.login__indicator.login__indicator--current
|
||||||
|
div.login__title
|
||||||
|
p Account created at
|
||||||
|
span(data-locale-time) #{creation.created_at.toISOString()}
|
||||||
|
div.login__using
|
||||||
|
if creation.location
|
||||||
|
span.login__location near #{creation.location.city}, #{creation.location.country}
|
||||||
|
|
|
||||||
|
if creation.user_agent
|
||||||
|
span.login__browser using #{creation.user_agent.browser} #{creation.user_agent.version} on #{creation.user_agent.platform} (#{creation.user_agent.os})
|
||||||
|
|
|
||||||
|
if creation.ip
|
||||||
|
span.login__ip
|
||||||
|
|IP address: #{creation.ip}
|
Reference in New Issue
Block a user