audit logging

This commit is contained in:
Evert Prants 2022-09-09 20:12:22 +03:00
parent 6e05c990d1
commit be604b24c6
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
21 changed files with 625 additions and 4 deletions

254
package-lock.json generated
View File

@ -25,6 +25,8 @@
"csrf": "^3.1.0",
"dotenv": "^16.0.1",
"express-session": "^1.17.3",
"express-useragent": "^1.0.15",
"geoip-lite": "^1.4.6",
"image-size": "^1.0.2",
"jsonwebtoken": "^8.5.1",
"marked": "^4.0.18",
@ -54,6 +56,8 @@
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.5",
"@types/express-useragent": "^1.0.2",
"@types/geoip-lite": "^1.4.1",
"@types/jest": "28.1.7",
"@types/jsonwebtoken": "^8.5.8",
"@types/marked": "^4.0.4",
@ -3450,6 +3454,21 @@
"@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": {
"version": "4.1.5",
"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",
"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": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -4783,6 +4810,14 @@
"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": {
"version": "1.0.1",
"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",
"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": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -6448,6 +6491,14 @@
"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": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@ -6834,6 +6885,49 @@
"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": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@ -7270,6 +7364,24 @@
"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": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -8511,6 +8623,11 @@
"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": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@ -8658,6 +8775,14 @@
"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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@ -9637,6 +9762,11 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -12518,6 +12648,15 @@
"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": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
@ -15026,6 +15165,21 @@
"@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": {
"version": "4.1.5",
"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",
"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": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -16069,6 +16231,11 @@
"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": {
"version": "1.0.1",
"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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
@ -17337,6 +17509,14 @@
"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": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@ -17628,6 +17808,39 @@
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"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": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@ -17933,6 +18146,23 @@
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
"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": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -18870,6 +19100,11 @@
"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": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@ -18989,6 +19224,11 @@
"integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==",
"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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@ -19737,6 +19977,11 @@
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"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": {
"version": "1.0.0",
"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",
"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": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",

View File

@ -40,6 +40,8 @@
"csrf": "^3.1.0",
"dotenv": "^16.0.1",
"express-session": "^1.17.3",
"express-useragent": "^1.0.15",
"geoip-lite": "^1.4.6",
"image-size": "^1.0.2",
"jsonwebtoken": "^8.5.1",
"marked": "^4.0.18",
@ -69,6 +71,8 @@
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.5",
"@types/express-useragent": "^1.0.2",
"@types/geoip-lite": "^1.4.1",
"@types/jest": "28.1.7",
"@types/jsonwebtoken": "^8.5.8",
"@types/marked": "^4.0.4",

50
src/fe/scss/_logins.scss Normal file
View 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;
}
}

View File

@ -8,6 +8,7 @@
@import 'authorize';
@import 'settings';
@import 'modal';
@import 'logins';
*,
*::before,

View File

@ -18,4 +18,9 @@ import { ModalManager } from './modal/modals';
const modals = new ModalManager();
const avatar = new AvatarModal();
modals.register(avatar);
const dateify = document.querySelectorAll('[data-locale-time]');
dateify.forEach((element: HTMLElement) => {
element.innerText = new Date(element.innerText).toString();
});
})();

View 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;
}

View 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',
}

View 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 {}

View 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'],
},
];

View 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);
}
}

View File

@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { ConfigurationModule } from '../config/config.module';
import { AuditModule } from './audit/audit.module';
import { DatabaseModule } from './database/database.module';
import { DocumentModule } from './document/document.module';
import { EmailModule } from './email/email.module';
@ -21,6 +22,7 @@ import { UserModule } from './user/user.module';
UploadModule,
UserModule,
DocumentModule,
AuditModule,
],
exports: [
DatabaseModule,
@ -31,6 +33,7 @@ import { UserModule } from './user/user.module';
UploadModule,
UserModule,
DocumentModule,
AuditModule,
],
})
export class ObjectsModule {}

View File

@ -12,6 +12,8 @@ import {
import { Throttle } from '@nestjs/throttler';
import { Request, Response } from 'express';
import { SessionData } from 'express-session';
import { AuditAction } from 'src/modules/objects/audit/audit.enum';
import { AuditService } from 'src/modules/objects/audit/audit.service';
import {
UserToken,
UserTokenType,
@ -31,6 +33,7 @@ export class LoginController {
private readonly userTokenService: UserTokenService,
private readonly formUtil: FormUtilityService,
private readonly token: TokenService,
private readonly audit: AuditService,
) {}
@Get()
@ -74,6 +77,8 @@ export class LoginController {
return;
}
await this.audit.auditRequest(req, AuditAction.LOGIN, req.session.id, user);
if (await this.totpService.userHasTOTP(user)) {
const challenge = { type: 'verify', user: user.uuid, remember };
const encrypted = await this.token.encryptChallenge(challenge);
@ -336,6 +341,7 @@ export class LoginController {
await this.userService.updateUser(token.user);
await this.userTokenService.delete(token);
await this.audit.auditRequest(req, AuditAction.PASSWORD_CHANGE, 'token');
req.flash('message', {
error: false,

View File

@ -8,12 +8,13 @@ import { CSRFMiddleware } from 'src/middleware/csrf.middleware';
import { FlashMiddleware } from 'src/middleware/flash.middleware';
import { UserMiddleware } from 'src/middleware/user.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 { UserModule } from 'src/modules/objects/user/user.module';
import { LoginController } from './login.controller';
@Module({
imports: [UserModule, UserTokenModule],
imports: [UserModule, UserTokenModule, AuditModule],
controllers: [LoginController],
})
export class LoginModule implements NestModule {

View File

@ -12,6 +12,8 @@ import {
import { Throttle } from '@nestjs/throttler';
import { Request, Response } from 'express';
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 { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
import { RegisterDto } from './register.interfaces';
@ -22,6 +24,7 @@ export class RegisterController {
private readonly userService: UserService,
private readonly formUtil: FormUtilityService,
private readonly config: ConfigurationService,
private readonly audit: AuditService,
) {}
@Get()
@ -87,7 +90,8 @@ export class RegisterController {
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', {
error: false,

View File

@ -8,11 +8,12 @@ import { CSRFMiddleware } from 'src/middleware/csrf.middleware';
import { FlashMiddleware } from 'src/middleware/flash.middleware';
import { UserMiddleware } from 'src/middleware/user.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 { RegisterController } from './register.controller';
@Module({
imports: [UserModule],
imports: [UserModule, AuditModule],
controllers: [RegisterController],
})
export class RegisterModule implements NestModule {

View File

@ -18,6 +18,8 @@ import { FileInterceptor } from '@nestjs/platform-express';
import { Throttle } from '@nestjs/throttler';
import { Request, Response } from 'express';
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 { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service';
import { UploadService } from 'src/modules/objects/upload/upload.service';
@ -38,6 +40,7 @@ export class SettingsController {
private readonly _totp: UserTOTPService,
private readonly _client: OAuth2ClientService,
private readonly _oaToken: OAuth2TokenService,
private readonly _audit: AuditService,
) {}
@Get()
@ -231,6 +234,11 @@ export class SettingsController {
const newPassword = await this._user.hashPassword(new_password);
req.user.password = newPassword;
await this._user.updateUser(req.user);
await this._audit.auditRequest(
req,
AuditAction.PASSWORD_CHANGE,
'settings',
);
req.flash('message', {
error: false,
@ -291,6 +299,7 @@ export class SettingsController {
req.user.email = email;
await this._user.updateUser(req.user);
await this._audit.auditRequest(req, AuditAction.EMAIL_CHANGE, 'settings');
req.flash('message', {
error: false,
@ -311,4 +320,15 @@ export class SettingsController {
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,
});
}
}

View File

@ -23,6 +23,7 @@ import { SettingsController } from './settings.controller';
import { SettingsService } from './settings.service';
import { CSRFMiddleware } from 'src/middleware/csrf.middleware';
import { UserMiddleware } from 'src/middleware/user.middleware';
import { AuditModule } from 'src/modules/objects/audit/audit.module';
@Module({
controllers: [SettingsController],
@ -31,6 +32,7 @@ import { UserMiddleware } from 'src/middleware/user.middleware';
UploadModule,
UserModule,
UserTokenModule,
AuditModule,
OAuth2Module,
OAuth2ClientModule,
OAuth2TokenModule,

View File

@ -1,5 +1,7 @@
import { Body, Controller, Get, Post, Req, Res } from '@nestjs/common';
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 { UserService } from 'src/modules/objects/user/user.service';
import { FormUtilityService } from 'src/modules/utility/services/form-utility.service';
@ -14,6 +16,7 @@ export class TwoFactorController {
private token: TokenService,
private user: UserService,
private form: FormUtilityService,
private audit: AuditService,
) {}
@Get('activate')
@ -92,6 +95,7 @@ export class TwoFactorController {
// TODO: show the recovery tokens to the user
await this.totp.activateTOTP(req.user, secret);
await this.audit.auditRequest(req, AuditAction.TOTP_ACTIVATE);
req.flash('message', {
error: false,
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.audit.auditRequest(req, AuditAction.TOTP_DEACTIVATE);
} catch (e: any) {
req.flash('message', {
error: true,

View File

@ -3,12 +3,13 @@ import { AuthMiddleware } from 'src/middleware/auth.middleware';
import { CSRFMiddleware } from 'src/middleware/csrf.middleware';
import { FlashMiddleware } from 'src/middleware/flash.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 { UserModule } from 'src/modules/objects/user/user.module';
import { TwoFactorController } from './two-factor.controller';
@Module({
imports: [UserModule, UserTokenModule],
imports: [UserModule, UserTokenModule, AuditModule],
controllers: [TwoFactorController],
})
export class TwoFactorModule implements NestModule {

View File

@ -40,6 +40,7 @@ export class FormUtilityService {
* @returns Stripped object
*/
public stripObject<T>(object: T, keys: string[]): T {
if (!object) return null;
return keys.reduce<T>((obj, field) => {
delete obj[field];
return obj;
@ -53,6 +54,7 @@ export class FormUtilityService {
* @returns Plucked object
*/
public pluckObject<T>(object: T, keys: string[]): Partial<T> {
if (!object) return null;
return Object.keys(object).reduce<Partial<T>>((obj, field) => {
if (keys.includes(field)) {
obj[field] = object[field];
@ -68,6 +70,7 @@ export class FormUtilityService {
* @returns Stripped object
*/
public stripObjectArray<T>(array: T[], keys: string[]): T[] {
if (!array) return null;
return array.map((object) => this.stripObject(object, keys));
}
@ -78,6 +81,7 @@ export class FormUtilityService {
* @returns Plucked object
*/
public pluckObjectArray<T>(array: T[], keys: string[]): Partial<T>[] {
if (!array) return null;
return array.map((object) => this.pluckObject(object, keys));
}

45
views/login-list.pug Normal file
View 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}
|&nbsp;
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}
|&nbsp;
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})
|&nbsp;
if creation.ip
span.login__ip
|IP address: #{creation.ip}