diff --git a/Anvil/Tools/Account.pm b/Anvil/Tools/Account.pm index 68fbaf2d..e567e0d4 100644 --- a/Anvil/Tools/Account.pm +++ b/Anvil/Tools/Account.pm @@ -253,10 +253,14 @@ sub login my $self = shift; my $parameter = shift; my $anvil = $self->parent; - my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; + + my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; + my $password = $parameter->{password} // $anvil->data->{cgi}{password}{value}; + my $username = $parameter->{username} // $anvil->data->{cgi}{username}{value}; + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Account->login()" }}); - if ((not $anvil->data->{cgi}{username}{value}) or (not $anvil->data->{cgi}{password}{value})) + if ((not $username) or (not $password)) { # The user forgot something... $anvil->data->{form}{error_massage} = $anvil->Template->get({file => "main.html", name => "error_message", variables => { error_message => $anvil->Words->string({key => "error_0027"}) }}); @@ -275,7 +279,7 @@ FROM WHERE user_algorithm != 'DELETED' AND - user_name = ".$anvil->Database->quote($anvil->data->{cgi}{username}{value})." + user_name = ".$anvil->Database->quote($username)." ;"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query }}); @@ -309,7 +313,7 @@ AND # Test the passed-in password. my $test_password_answer = $anvil->Account->encrypt_password({ debug => 2, - password => $anvil->data->{cgi}{password}{value}, + password => $password, salt => $user_salt, algorithm => $user_algorithm, hash_count => $user_hash_count, @@ -345,7 +349,7 @@ AND }); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { session_uuid => $session_uuid }}); - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0183", variables => { user => $anvil->data->{cgi}{username}{value} }}); + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0183", variables => { user => $username }}); $anvil->Account->_write_cookies({ debug => $debug, hash => $session_hash, @@ -360,7 +364,7 @@ AND $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0184", variables => { user_agent => $ENV{HTTP_USER_AGENT} ? $ENV{HTTP_USER_AGENT} : "#!string!log_0185!#", source_ip => $ENV{REMOTE_ADDR} ? $ENV{REMOTE_ADDR} : "#!string!log_0185!#", - user => $anvil->data->{cgi}{username}{value}, + user => $username, }}); # Slow them down a bit... diff --git a/anvil.spec.in b/anvil.spec.in index 34cf10ca..c26d1580 100644 --- a/anvil.spec.in +++ b/anvil.spec.in @@ -5,6 +5,7 @@ %define debug_package %{nil} %define anviluser admin %define anvilgroup admin +%define suiapi striker-ui-api Name: anvil Version: @version@ @@ -134,7 +135,6 @@ Requires: firefox Requires: gcc Requires: gdm Requires: gnome-terminal -Requires: httpd Requires: nmap Requires: nodejs Requires: openssh-askpass @@ -243,14 +243,32 @@ setenforce 0 systemctl enable --now chronyd.service systemctl enable --now anvil-daemon.service systemctl enable --now scancore.service -systemctl enable --now striker-ui-api.service -systemctl restart striker-ui-api.service + +%pre striker +getent passwd %{suiapi} >/dev/null \ + || useradd \ + --comment "Striker UI API" \ + --home-dir %{_datadir}/%{suiapi} \ + --shell %{_sbindir}/nologin \ + --user-group \ + %{suiapi} + +if [ $1 -gt 1 ]; then # >1=Upgrade + # Transfer files owned by apache to Striker UI API user. + chown -R --from apache %{suiapi}: /mnt + chown -R --from apache %{suiapi}: %{_localstatedir}/www +fi %post striker ### NOTE: PostgreSQL is initialized and enabled by striker-prep-database later. -echo "Enabling and starting apache." -systemctl enable httpd.service -systemctl start httpd.service + +# Always reload to handle service file changes. +systemctl daemon-reload + +systemctl enable %{suiapi}.service +# Striker UI API needs explicit restart for changes to take effect. +systemctl restart %{suiapi}.service + restorecon -rv /%{_localstatedir}/www if ! $(ls -l /etc/systemd/system/default.target | grep -q graphical); then @@ -329,6 +347,11 @@ touch /etc/anvil/type.dr # sed -i.anvil 's/SELINUX=permissive/SELINUX=enforcing/' /etc/selinux/config # setenforce 1 +%preun striker +if [ $1 == 0 ]; then # 0=Uninstall, 1=First install, >1=Upgrade (version count) + systemctl disable --now %{suiapi}.service +fi + %postun striker ### TODO: Stopping postgres breaks the Anvil! during OS updates. Need to find a ### way to run this only during uninstalls, and not during updates. @@ -340,11 +363,13 @@ touch /etc/anvil/type.dr # firewall-cmd --zone=public --remove-service=postgresql # firewall-cmd --zone=public --remove-service=postgresql --permanent # echo "Disabling and stopping postgresql-9.6." -# systemctl disable httpd.service -# systemctl stop httpd.service # systemctl disable postgresql.service # systemctl stop postgresql.service +if [ $1 == 0 ]; then # 0=Uninstall + systemctl daemon-reload +fi + # Remove the system type file. if [ -e '/etc/anvil/type.striker' ] then @@ -376,7 +401,7 @@ fi %{_sbindir}/* %{_sysconfdir}/anvil/anvil.version %{_datadir}/perl5/* -%{_datadir}/striker-ui-api/* +%{_datadir}/%{suiapi}/* %{_mandir}/* %files striker diff --git a/striker-ui-api/package-lock.json b/striker-ui-api/package-lock.json index a5f2b961..813eabcf 100644 --- a/striker-ui-api/package-lock.json +++ b/striker-ui-api/package-lock.json @@ -8,9 +8,14 @@ "name": "striker-ui-api", "version": "0.1.0", "dependencies": { + "core-js": "^3.30.1", "cors": "^2.8.5", "express": "^4.18.2", - "multer": "^1.4.4" + "express-session": "^1.17.3", + "multer": "^1.4.4", + "passport": "^0.6.0", + "passport-local": "^1.0.0", + "regenerator-runtime": "^0.13.11" }, "devDependencies": { "@babel/core": "^7.17.8", @@ -18,8 +23,11 @@ "@babel/preset-typescript": "^7.16.7", "@types/cors": "^2.8.12", "@types/express": "^4.17.13", + "@types/express-session": "^1.17.7", "@types/multer": "^1.4.7", "@types/node": "^17.0.22", + "@types/passport": "^1.0.12", + "@types/passport-local": "^1.0.35", "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", "babel-loader": "^8.2.3", @@ -2412,6 +2420,15 @@ "@types/range-parser": "*" } }, + "node_modules/@types/express-session": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.7.tgz", + "integrity": "sha512-L25080PBYoRLu472HY/HNCxaXY8AaGgqGC8/p/8+BYMhG0RDOLQ1wpXOpAzr4Gi5TGozTKyJv5BVODM5UNyVMw==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.10", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.10.tgz", @@ -2445,6 +2462,36 @@ "integrity": "sha512-8FwbVoG4fy+ykY86XCAclKZDORttqE5/s7dyWZKLXTdv3vRy5HozBEinG5IqhvPXXzIZEcTVbuHlQEI6iuwcmw==", "dev": true }, + "node_modules/@types/passport": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.12.tgz", + "integrity": "sha512-QFdJ2TiAEoXfEQSNDISJR1Tm51I78CymqcBa8imbjo6dNNu+l2huDxxbDEIoFIwOSKMkOfHEikyDuZ38WwWsmw==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-local": { + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.35.tgz", + "integrity": "sha512-K4eLTJ8R0yYW8TvCqkjB0pTKoqfUSdl5PfZdidTjV2ETV3604fQxtY6BHKjQWAx50WUS0lqzBvKv3LoI1ZBPeA==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.35", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz", + "integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -3504,6 +3551,16 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "node_modules/core-js": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz", + "integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-js-compat": { "version": "3.21.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.1.tgz", @@ -4272,6 +4329,51 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "dependencies": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5485,6 +5587,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5582,6 +5692,42 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5629,6 +5775,11 @@ "node": ">=8" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -5740,6 +5891,14 @@ } ] }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -5816,10 +5975,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/regenerator-transform": { "version": "0.14.5", @@ -6446,6 +6604,17 @@ "node": ">=4.2.0" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -8507,6 +8676,15 @@ "@types/range-parser": "*" } }, + "@types/express-session": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.7.tgz", + "integrity": "sha512-L25080PBYoRLu472HY/HNCxaXY8AaGgqGC8/p/8+BYMhG0RDOLQ1wpXOpAzr4Gi5TGozTKyJv5BVODM5UNyVMw==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/json-schema": { "version": "7.0.10", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.10.tgz", @@ -8540,6 +8718,36 @@ "integrity": "sha512-8FwbVoG4fy+ykY86XCAclKZDORttqE5/s7dyWZKLXTdv3vRy5HozBEinG5IqhvPXXzIZEcTVbuHlQEI6iuwcmw==", "dev": true }, + "@types/passport": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.12.tgz", + "integrity": "sha512-QFdJ2TiAEoXfEQSNDISJR1Tm51I78CymqcBa8imbjo6dNNu+l2huDxxbDEIoFIwOSKMkOfHEikyDuZ38WwWsmw==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/passport-local": { + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.35.tgz", + "integrity": "sha512-K4eLTJ8R0yYW8TvCqkjB0pTKoqfUSdl5PfZdidTjV2ETV3604fQxtY6BHKjQWAx50WUS0lqzBvKv3LoI1ZBPeA==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, + "@types/passport-strategy": { + "version": "0.2.35", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz", + "integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/passport": "*" + } + }, "@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -9329,6 +9537,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "core-js": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz", + "integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==" + }, "core-js-compat": { "version": "3.21.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.1.tgz", @@ -9937,6 +10150,33 @@ } } }, + "express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "requires": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -10808,6 +11048,11 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -10878,6 +11123,29 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + } + }, + "passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10913,6 +11181,11 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -10980,6 +11253,11 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -11044,10 +11322,9 @@ } }, "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "regenerator-transform": { "version": "0.14.5", @@ -11498,6 +11775,14 @@ "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==", "dev": true }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, "unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", diff --git a/striker-ui-api/package.json b/striker-ui-api/package.json index 6cc22f41..980561dd 100644 --- a/striker-ui-api/package.json +++ b/striker-ui-api/package.json @@ -11,9 +11,14 @@ "start": "npm run build && node out/index.js" }, "dependencies": { + "core-js": "^3.30.1", "cors": "^2.8.5", "express": "^4.18.2", - "multer": "^1.4.4" + "express-session": "^1.17.3", + "multer": "^1.4.4", + "passport": "^0.6.0", + "passport-local": "^1.0.0", + "regenerator-runtime": "^0.13.11" }, "devDependencies": { "@babel/core": "^7.17.8", @@ -21,8 +26,11 @@ "@babel/preset-typescript": "^7.16.7", "@types/cors": "^2.8.12", "@types/express": "^4.17.13", + "@types/express-session": "^1.17.7", "@types/multer": "^1.4.7", "@types/node": "^17.0.22", + "@types/passport": "^1.0.12", + "@types/passport-local": "^1.0.35", "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", "babel-loader": "^8.2.3", diff --git a/striker-ui-api/src/app.ts b/striker-ui-api/src/app.ts index 1bb6396d..5ee5d262 100644 --- a/striker-ui-api/src/app.ts +++ b/striker-ui-api/src/app.ts @@ -1,18 +1,38 @@ import cors from 'cors'; -import express from 'express'; -import path from 'path'; - -import API_ROOT_PATH from './lib/consts/API_ROOT_PATH'; +import express, { json } from 'express'; +import { guardApi } from './lib/assertAuthentication'; +import passport from './passport'; import routes from './routes'; +import { rrouters } from './lib/rrouters'; +import session from './session'; + +export default (async () => { + const app = express(); + + app.use(json()); + + app.use( + cors({ + origin: true, + credentials: true, + }), + ); + + // Add session handler to the chain **after** adding other handlers that do + // not depend on session(s). + app.use(await session); -const app = express(); + app.use(passport.initialize()); + app.use(passport.authenticate('session')); -app.use(express.json()); -app.use(cors()); + rrouters(app, routes.private, { + assign: (router) => [guardApi, router], + route: '/api', + }); + rrouters(app, routes.public, { route: '/api' }); -Object.entries(routes).forEach(([route, router]) => { - app.use(path.join(API_ROOT_PATH, route), router); -}); + app.use(routes.static); -export default app; + return app; +})(); diff --git a/striker-ui-api/src/index.ts b/striker-ui-api/src/index.ts index 51c13841..8cfe2903 100644 --- a/striker-ui-api/src/index.ts +++ b/striker-ui-api/src/index.ts @@ -1,7 +1,26 @@ +import { getgid, getuid, setgid, setuid } from 'process'; + +import { PGID, PUID, PORT, ECODE_DROP_PRIVILEGES } from './lib/consts'; + import app from './app'; +import { stderr, stdout } from './lib/shell'; + +(async () => { + stdout(`Starting process with ownership ${getuid()}:${getgid()}`); + + (await app).listen(PORT, () => { + try { + // Group must be set before user to avoid permission error. + setgid(PGID); + setuid(PUID); + + stdout(`Process ownership changed to ${getuid()}:${getgid()}.`); + } catch (error) { + stderr(`Failed to change process ownership; CAUSE: ${error}`); -import SERVER_PORT from './lib/consts/SERVER_PORT'; + process.exit(ECODE_DROP_PRIVILEGES); + } -app.listen(SERVER_PORT, () => { - console.log(`Listening on localhost:${SERVER_PORT}.`); -}); + stdout(`Listening on localhost:${PORT}.`); + }); +})(); diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index 6365cd54..c006f5eb 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -1,109 +1,214 @@ -import { spawnSync, SpawnSyncOptions } from 'child_process'; +import { ChildProcess, spawn, SpawnOptions } from 'child_process'; +import EventEmitter from 'events'; +import { readFileSync } from 'fs'; + +import { SERVER_PATHS, PGID, PUID } from './consts'; + +import { formatSql } from './formatSql'; +import { + date, + stderr as sherr, + stdout as shout, + stdoutVar as shvar, + uuid, +} from './shell'; + +class Access extends EventEmitter { + private ps: ChildProcess; + private queue: string[] = []; + + constructor({ + eventEmitterOptions = {}, + spawnOptions = {}, + }: { + eventEmitterOptions?: ConstructorParameters[0]; + spawnOptions?: SpawnOptions; + } = {}) { + super(eventEmitterOptions); + + this.ps = this.start(spawnOptions); + } -import SERVER_PATHS from './consts/SERVER_PATHS'; + private start({ + args = [], + gid = PGID, + stdio = 'pipe', + timeout = 10000, + uid = PUID, + ...restSpawnOptions + }: AccessStartOptions = {}) { + shvar( + { gid, stdio, timeout, uid, ...restSpawnOptions }, + `Starting anvil-access-module daemon with options: `, + ); -import { stderr as sherr, stdout as shout } from './shell'; + const ps = spawn(SERVER_PATHS.usr.sbin['anvil-access-module'].self, args, { + gid, + stdio, + timeout, + uid, + ...restSpawnOptions, + }); -const formatQuery = (query: string) => query.replace(/\s+/g, ' '); + let stderr = ''; + let stdout = ''; -const execAnvilAccessModule = ( - args: string[], - options: SpawnSyncOptions = { - encoding: 'utf-8', - timeout: 10000, - }, -) => { - const { error, stdout, stderr } = spawnSync( - SERVER_PATHS.usr.sbin['anvil-access-module'].self, - args, - options, - ); + ps.stderr?.setEncoding('utf-8').on('data', (chunk: string) => { + stderr += chunk; + + const scriptId: string | undefined = this.queue[0]; + + if (scriptId) { + sherr(`${Access.event(scriptId, 'stderr')}: ${stderr}`); + + stderr = ''; + } + }); + + ps.stdout?.setEncoding('utf-8').on('data', (chunk: string) => { + stdout += chunk; + + let nindex: number = stdout.indexOf('\n'); - if (error) { - throw error; + // 1. ~a is the shorthand for -(a + 1) + // 2. negatives are evaluated to true + while (~nindex) { + const scriptId = this.queue.shift(); + + if (scriptId) this.emit(scriptId, stdout.substring(0, nindex)); + + stdout = stdout.substring(nindex + 1); + nindex = stdout.indexOf('\n'); + } + }); + + return ps; } - if (stderr.length > 0) { - throw new Error(stderr.toString()); + private stop() { + this.ps.once('error', () => !this.ps.killed && this.ps.kill('SIGKILL')); + + this.ps.kill(); } - let output; + private restart(options?: AccessStartOptions) { + this.ps.once('close', () => this.start(options)); - try { - output = JSON.parse(stdout.toString()); - } catch (stdoutParseError) { - output = stdout; + this.stop(); + } - sherr( - `Failed to parse anvil-access-module output [${output}]; CAUSE: [${stdoutParseError}]`, - ); + private static event(scriptId: string, category: 'stderr'): string { + return `${scriptId}-${category}`; } - return { - stdout: output, - }; -}; + public interact(command: string, ...args: string[]) { + const { stdin } = this.ps; + + const scriptId = uuid(); + const script = `${command} ${args.join(' ')}\n`; + + const promise = new Promise((resolve, reject) => { + this.once(scriptId, (data) => { + let result: T; + + try { + result = JSON.parse(data); + } catch (error) { + return reject(`Failed to parse line ${scriptId}; got [${data}]`); + } + + return resolve(result); + }); + }); + + shvar({ scriptId, script }, 'Access interact: '); + + this.queue.push(scriptId); + stdin?.write(script); -const execModuleSubroutine = ( - subName: string, + return promise; + } +} + +const access = new Access(); + +const subroutine = async ( + subroutine: string, { - spawnSyncOptions, - subModuleName, - subParams, - }: ExecModuleSubroutineOptions = {}, + params = [], + pre = ['Database'], + }: { + params?: unknown[]; + pre?: string[]; + } = {}, ) => { - const args = ['--sub', subName]; + const chain = `${pre.join('->')}->${subroutine}`; - // Defaults to "Database" in anvil-access-module. - if (subModuleName) { - args.push('--sub-module', subModuleName); - } + const subParams: string[] = params.map((p) => { + let result: string; - if (subParams) { - args.push('--sub-params', JSON.stringify(subParams)); - } + try { + result = JSON.stringify(p); + } catch (error) { + result = String(p); + } + + return `'${result}'`; + }); - shout( - `...${subModuleName}->${subName} with params: ${JSON.stringify( - subParams, - null, - 2, - )}`, + const { sub_results: results } = await access.interact<{ sub_results: T }>( + 'x', + chain, + ...subParams, ); - const { stdout } = execAnvilAccessModule(args, spawnSyncOptions); + shvar(results, `${chain} results: `); - return { - stdout: stdout['sub_results'], - }; + return results; }; -const dbInsertOrUpdateJob = ( - { job_progress = 0, line = 0, ...rest }: DBJobParams, - { spawnSyncOptions }: DBInsertOrUpdateJobOptions = {}, -) => - execModuleSubroutine('insert_or_update_jobs', { - spawnSyncOptions, - subParams: { job_progress, line, ...rest }, - }).stdout; - -const dbInsertOrUpdateVariable: DBInsertOrUpdateVariableFunction = ( - subParams, - { spawnSyncOptions } = {}, -) => - execModuleSubroutine('insert_or_update_variables', { - spawnSyncOptions, - subParams, - }).stdout; - -const dbJobAnvilSyncShared = ( +const query = (script: string) => + access.interact('r', formatSql(script)); + +const write = async (script: string) => { + const { write_code: wcode } = await access.interact<{ write_code: number }>( + 'w', + formatSql(script), + ); + + return wcode; +}; + +const insertOrUpdateJob = async ({ + job_progress = 0, + line = 0, + ...rest +}: JobParams) => { + const [uuid]: [string] = await subroutine('insert_or_update_jobs', { + params: [{ job_progress, line, ...rest }], + }); + + return uuid; +}; + +const insertOrUpdateVariable: InsertOrUpdateVariableFunction = async ( + params, +) => { + const [uuid]: [string] = await subroutine('insert_or_update_variables', { + params: [params], + }); + + return uuid; +}; + +const anvilSyncShared = ( jobName: string, jobData: string, jobTitle: string, jobDescription: string, - { jobHostUUID }: DBJobAnvilSyncSharedOptions = { jobHostUUID: undefined }, + { jobHostUUID }: JobAnvilSyncSharedOptions = { jobHostUUID: undefined }, ) => { - const subParams: DBJobParams = { + const subParams: JobParams = { file: __filename, job_command: SERVER_PATHS.usr.sbin['anvil-sync-shared'].self, job_data: jobData, @@ -116,45 +221,54 @@ const dbJobAnvilSyncShared = ( subParams.job_host_uuid = jobHostUUID; } - return dbInsertOrUpdateJob(subParams); + return insertOrUpdateJob(subParams); }; -const dbQuery = (query: string, options?: SpawnSyncOptions) => { - shout(formatQuery(query)); +const refreshTimestamp = () => { + let result: string; + + try { + result = date('--rfc-3339', 'ns').trim(); + } catch (shError) { + throw new Error( + `Failed to get timestamp for database use; CAUSE: ${shError}`, + ); + } - return execAnvilAccessModule(['--query', query], options); + return result; }; -const dbSubRefreshTimestamp = () => - execModuleSubroutine('refresh_timestamp').stdout; +const getData = async (...keys: string[]) => { + const chain = `data->${keys.join('->')}`; + + const { + sub_results: [data], + } = await access.interact<{ sub_results: [T] }>('x', chain); + + shvar(data, `${chain} data: `); + + return data; +}; -const dbWrite = (query: string, options?: SpawnSyncOptions) => { - shout(formatQuery(query)); +const getFenceSpec = async () => { + await subroutine('get_fence_data', { pre: ['Striker'] }); - return execAnvilAccessModule(['--query', query, '--mode', 'write'], options); + return getData('fence_data'); }; -const getAnvilData = ( - dataStruct: AnvilDataStruct, - { predata, ...spawnSyncOptions }: GetAnvilDataOptions = {}, -): HashType => - execAnvilAccessModule( - [ - '--predata', - JSON.stringify(predata), - '--data', - JSON.stringify(dataStruct), - ], - spawnSyncOptions, - ).stdout; +const getHostData = async () => { + await subroutine('get_hosts'); + + return getData('hosts'); +}; const getLocalHostName = () => { let result: string; try { - result = execModuleSubroutine('host_name', { - subModuleName: 'Get', - }).stdout; + result = readFileSync(SERVER_PATHS.etc.hostname.self, { + encoding: 'utf-8', + }).trim(); } catch (subError) { throw new Error(`Failed to get local host name; CAUSE: ${subError}`); } @@ -164,13 +278,13 @@ const getLocalHostName = () => { return result; }; -const getLocalHostUUID = () => { +const getLocalHostUuid = () => { let result: string; try { - result = execModuleSubroutine('host_uuid', { - subModuleName: 'Get', - }).stdout; + result = readFileSync(SERVER_PATHS.etc.anvil['host.uuid'].self, { + encoding: 'utf-8', + }).trim(); } catch (subError) { throw new Error(`Failed to get local host UUID; CAUSE: ${subError}`); } @@ -180,9 +294,18 @@ const getLocalHostUUID = () => { return result; }; -const getPeerData: GetPeerDataFunction = ( +const getManifestData = async (manifestUuid?: string) => { + await subroutine('load_manifest', { + params: [{ manifest_uuid: manifestUuid }], + pre: ['Striker'], + }); + + return getData('manifests'); +}; + +const getPeerData: GetPeerDataFunction = async ( target, - { password, port, ...restOptions } = {}, + { password, port } = {}, ) => { const [ rawIsConnected, @@ -193,11 +316,13 @@ const getPeerData: GetPeerDataFunction = ( internet: rawIsInetConnected, os_registered: rawIsOSRegistered, }, - ] = execModuleSubroutine('get_peer_data', { - subModuleName: 'Striker', - subParams: { password, port, target }, - ...restOptions, - }).stdout as [connected: string, data: PeerDataHash]; + ]: [connected: string, data: PeerDataHash] = await subroutine( + 'get_peer_data', + { + params: [{ password, port, target }], + pre: ['Striker'], + }, + ); return { hostName, @@ -209,16 +334,26 @@ const getPeerData: GetPeerDataFunction = ( }; }; +const getUpsSpec = async () => { + await subroutine('get_ups_data', { pre: ['Striker'] }); + + return getData('ups_data'); +}; + export { - dbInsertOrUpdateJob as job, - dbInsertOrUpdateVariable as variable, - dbJobAnvilSyncShared, - dbQuery, - dbSubRefreshTimestamp, - dbWrite, - getAnvilData, + insertOrUpdateJob as job, + insertOrUpdateVariable as variable, + anvilSyncShared, + refreshTimestamp as timestamp, + getData, + getFenceSpec, + getHostData, getLocalHostName, - getLocalHostUUID, + getLocalHostUuid as getLocalHostUUID, + getManifestData, getPeerData, - execModuleSubroutine as sub, + getUpsSpec, + query, + subroutine as sub, + write, }; diff --git a/striker-ui-api/src/lib/assertAuthentication.ts b/striker-ui-api/src/lib/assertAuthentication.ts new file mode 100644 index 00000000..b9d17afc --- /dev/null +++ b/striker-ui-api/src/lib/assertAuthentication.ts @@ -0,0 +1,55 @@ +import { Handler } from 'express'; + +import { stdout } from './shell'; + +type AssertAuthenticationOptions = { + fail?: string | ((...args: Parameters) => void); + failReturnTo?: boolean | string; + succeed?: string | ((...args: Parameters) => void); +}; + +type AssertAuthenticationFunction = ( + options?: AssertAuthenticationOptions, +) => Handler; + +export const assertAuthentication: AssertAuthenticationFunction = ({ + fail: initFail = (request, response) => response.status(404).send(), + failReturnTo, + succeed: initSucceed = (request, response, next) => next(), +}: AssertAuthenticationOptions = {}) => { + const fail: (...args: Parameters) => void = + typeof initFail === 'string' + ? (request, response) => response.redirect(initFail) + : initFail; + + const succeed: (...args: Parameters) => void = + typeof initSucceed === 'string' + ? (request, response) => response.redirect(initSucceed) + : initSucceed; + + let getReturnTo: ((...args: Parameters) => string) | undefined; + + if (failReturnTo === true) { + getReturnTo = ({ originalUrl, url }) => originalUrl || url; + } else if (typeof failReturnTo === 'string') { + getReturnTo = () => failReturnTo; + } + + return (...args) => { + const { 0: request } = args; + const { originalUrl, session } = request; + const { passport } = session; + + if (passport?.user) return succeed(...args); + + session.returnTo = getReturnTo?.call(null, ...args); + + stdout( + `Unauthenticated access to ${originalUrl}; set return to ${session.returnTo}`, + ); + + return fail(...args); + }; +}; + +export const guardApi = assertAuthentication(); diff --git a/striker-ui-api/src/lib/consts/AN_VARIABLE_NAME_LIST.ts b/striker-ui-api/src/lib/consts/AN_VARIABLE_NAME_LIST.ts new file mode 100644 index 00000000..28756512 --- /dev/null +++ b/striker-ui-api/src/lib/consts/AN_VARIABLE_NAME_LIST.ts @@ -0,0 +1 @@ +export const VNAME_SESSION_SECRET = 'striker-ui-api::session::secret'; diff --git a/striker-ui-api/src/lib/consts/API_ROOT_PATH.ts b/striker-ui-api/src/lib/consts/API_ROOT_PATH.ts deleted file mode 100644 index fd320c2d..00000000 --- a/striker-ui-api/src/lib/consts/API_ROOT_PATH.ts +++ /dev/null @@ -1,3 +0,0 @@ -const API_ROOT_PATH = '/api'; - -export default API_ROOT_PATH; diff --git a/striker-ui-api/src/lib/consts/DELETED.ts b/striker-ui-api/src/lib/consts/DELETED.ts new file mode 100644 index 00000000..68afc4e5 --- /dev/null +++ b/striker-ui-api/src/lib/consts/DELETED.ts @@ -0,0 +1 @@ +export const DELETED = 'DELETED'; diff --git a/striker-ui-api/src/lib/consts/EXIT_CODE_LIST.ts b/striker-ui-api/src/lib/consts/EXIT_CODE_LIST.ts new file mode 100644 index 00000000..33faa997 --- /dev/null +++ b/striker-ui-api/src/lib/consts/EXIT_CODE_LIST.ts @@ -0,0 +1,2 @@ +export const ECODE_DROP_PRIVILEGES = 1; +export const ECODE_SESSION_SECRET = 2; diff --git a/striker-ui-api/src/lib/consts/PROCESS_OWNER.ts b/striker-ui-api/src/lib/consts/PROCESS_OWNER.ts new file mode 100644 index 00000000..d37c1c61 --- /dev/null +++ b/striker-ui-api/src/lib/consts/PROCESS_OWNER.ts @@ -0,0 +1,5 @@ +import { resolveGid, resolveUid } from '../shell'; + +export const PUID = resolveUid(process.env.PUID ?? 'striker-ui-api'); + +export const PGID = resolveGid(process.env.PGID ?? PUID); diff --git a/striker-ui-api/src/lib/consts/REG_EXP_PATTERNS.ts b/striker-ui-api/src/lib/consts/REG_EXP_PATTERNS.ts index 0a9bd4fe..c62557a6 100644 --- a/striker-ui-api/src/lib/consts/REG_EXP_PATTERNS.ts +++ b/striker-ui-api/src/lib/consts/REG_EXP_PATTERNS.ts @@ -1,23 +1,21 @@ -const hex = '[0-9a-f]'; -const octet = '(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9]|)[0-9])'; -const alphanumeric = '[a-z0-9]'; -const alphanumericDash = '[a-z0-9-]'; -const ipv4 = `(?:${octet}[.]){3}${octet}`; +export const P_HEX = '[[:xdigit:]]'; +export const P_OCTET = '(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9]|)[0-9])'; +export const P_ALPHANUM = '[a-z0-9]'; +export const P_ALPHANUM_DASH = '[a-z0-9-]'; +export const P_IPV4 = `(?:${P_OCTET}[.]){3}${P_OCTET}`; +export const P_UUID = `${P_HEX}{8}-${P_HEX}{4}-[1-5]${P_HEX}{3}-[89ab]${P_HEX}{3}-${P_HEX}{12}`; export const REP_DOMAIN = new RegExp( - `^(?:${alphanumeric}(?:${alphanumericDash}{0,61}${alphanumeric})?[.])+${alphanumeric}${alphanumericDash}{0,61}${alphanumeric}$`, + `^(?:${P_ALPHANUM}(?:${P_ALPHANUM_DASH}{0,61}${P_ALPHANUM})?[.])+${P_ALPHANUM}${P_ALPHANUM_DASH}{0,61}${P_ALPHANUM}$`, ); export const REP_INTEGER = /^\d+$/; -export const REP_IPV4 = new RegExp(`^${ipv4}$`); +export const REP_IPV4 = new RegExp(`^${P_IPV4}$`); -export const REP_IPV4_CSV = new RegExp(`(?:${ipv4},)*${ipv4}`); +export const REP_IPV4_CSV = new RegExp(`(?:${P_IPV4},)*${P_IPV4}`); // Peaceful string is temporarily defined as a string without single-quote, double-quote, slash (/), backslash (\\), angle brackets (< >), and curly brackets ({ }). export const REP_PEACEFUL_STRING = /^[^'"/\\><}{]+$/; -export const REP_UUID = new RegExp( - `^${hex}{8}-${hex}{4}-[1-5]${hex}{3}-[89ab]${hex}{3}-${hex}{12}$`, - 'i', -); +export const REP_UUID = new RegExp(`^${P_UUID}$`); diff --git a/striker-ui-api/src/lib/consts/SERVER_PATHS.ts b/striker-ui-api/src/lib/consts/SERVER_PATHS.ts index 132d3015..8192aca6 100644 --- a/striker-ui-api/src/lib/consts/SERVER_PATHS.ts +++ b/striker-ui-api/src/lib/consts/SERVER_PATHS.ts @@ -1,6 +1,10 @@ import path from 'path'; const EMPTY_SERVER_PATHS: ServerPath = { + etc: { + anvil: { 'host.uuid': {} }, + hostname: {}, + }, mnt: { shared: { incoming: {}, @@ -10,10 +14,13 @@ const EMPTY_SERVER_PATHS: ServerPath = { usr: { bin: { date: {}, + getent: {}, mkfifo: {}, + openssl: {}, psql: {}, rm: {}, sed: {}, + uuidgen: {}, }, sbin: { 'anvil-access-module': {}, @@ -31,6 +38,7 @@ const EMPTY_SERVER_PATHS: ServerPath = { 'striker-parse-os-list': {}, }, }, + var: { www: { html: {} } }, }; const generatePaths = ( diff --git a/striker-ui-api/src/lib/consts/SERVER_PORT.ts b/striker-ui-api/src/lib/consts/SERVER_PORT.ts index db7f5f0e..f668d4c2 100644 --- a/striker-ui-api/src/lib/consts/SERVER_PORT.ts +++ b/striker-ui-api/src/lib/consts/SERVER_PORT.ts @@ -1,3 +1 @@ -const SERVER_PORT = process.env.SERVER_PORT ?? 8080; - -export default SERVER_PORT; +export const PORT = process.env.PORT ?? 8080; diff --git a/striker-ui-api/src/lib/consts/index.ts b/striker-ui-api/src/lib/consts/index.ts new file mode 100644 index 00000000..8f37097d --- /dev/null +++ b/striker-ui-api/src/lib/consts/index.ts @@ -0,0 +1,10 @@ +import SERVER_PATHS from './SERVER_PATHS'; + +export { SERVER_PATHS }; + +export * from './AN_VARIABLE_NAME_LIST'; +export * from './DELETED'; +export * from './EXIT_CODE_LIST'; +export * from './PROCESS_OWNER'; +export * from './REG_EXP_PATTERNS'; +export * from './SERVER_PORT'; diff --git a/striker-ui-api/src/lib/formatSql.ts b/striker-ui-api/src/lib/formatSql.ts new file mode 100644 index 00000000..b4a7acb4 --- /dev/null +++ b/striker-ui-api/src/lib/formatSql.ts @@ -0,0 +1 @@ +export const formatSql = (script: string) => script.replace(/\s+/g, ' '); diff --git a/striker-ui-api/src/lib/getSessionSecret.ts b/striker-ui-api/src/lib/getSessionSecret.ts new file mode 100644 index 00000000..46878e74 --- /dev/null +++ b/striker-ui-api/src/lib/getSessionSecret.ts @@ -0,0 +1,56 @@ +import assert from 'assert'; + +import { ECODE_SESSION_SECRET, VNAME_SESSION_SECRET } from './consts'; + +import { query, variable } from './accessModule'; +import { openssl, stderr, stdout } from './shell'; + +export const getSessionSecret = async (): Promise => { + let sessionSecret: string; + + try { + const rows: [sessionSecret: string][] = await query( + `SELECT variable_value + FROM variables + WHERE variable_name = '${VNAME_SESSION_SECRET}';`, + ); + + assert(rows.length > 0, 'No existing session secret found.'); + + ({ + 0: [sessionSecret], + } = rows); + + stdout('Found an existing session secret.'); + + return sessionSecret; + } catch (queryError) { + stderr(`Failed to get session secret from database; CAUSE: ${queryError}`); + } + + try { + sessionSecret = openssl('rand', '-base64', '32').trim(); + + stdout('Generated a new session secret.'); + } catch (sysError) { + stderr(`Failed to generate session secret; CAUSE: ${sysError}`); + + process.exit(ECODE_SESSION_SECRET); + } + + try { + const vuuid = await variable({ + file: __filename, + variable_name: VNAME_SESSION_SECRET, + variable_value: sessionSecret, + }); + + stdout(`Recorded session secret as variable identified by ${vuuid}.`); + } catch (subError) { + stderr(`Failed to record session secret; CAUSE: ${subError}`); + + process.exit(ECODE_SESSION_SECRET); + } + + return sessionSecret; +}; diff --git a/striker-ui-api/src/lib/isObject.ts b/striker-ui-api/src/lib/isObject.ts new file mode 100644 index 00000000..caab9fe2 --- /dev/null +++ b/striker-ui-api/src/lib/isObject.ts @@ -0,0 +1,10 @@ +export const isObject = (value: unknown) => { + const result: { is: boolean; obj: object } = { is: false, obj: {} }; + + if (typeof value === 'object' && value !== null) { + result.is = true; + result.obj = value; + } + + return result; +}; diff --git a/striker-ui-api/src/lib/request_handlers/anvil/buildQueryAnvilDetail.ts b/striker-ui-api/src/lib/request_handlers/anvil/buildQueryAnvilDetail.ts index 227d0291..a1561bd5 100644 --- a/striker-ui-api/src/lib/request_handlers/anvil/buildQueryAnvilDetail.ts +++ b/striker-ui-api/src/lib/request_handlers/anvil/buildQueryAnvilDetail.ts @@ -2,6 +2,7 @@ import NODE_AND_DR_RESERVED_MEMORY_SIZE from '../../consts/NODE_AND_DR_RESERVED_ import { OS_LIST } from '../../consts/OS_LIST'; import join from '../../join'; +import { stdoutVar } from '../../shell'; const buildQueryAnvilDetail = ({ anvilUUIDs = ['*'], @@ -19,7 +20,7 @@ const buildQueryAnvilDetail = ({ separator: ', ', }); - console.log(`condAnvilsUUID=[${condAnvilsUUID}]`); + stdoutVar({ condAnvilsUUID }); const buildHostQuery = ({ isSummary = false, diff --git a/striker-ui-api/src/lib/request_handlers/auth/index.ts b/striker-ui-api/src/lib/request_handlers/auth/index.ts new file mode 100644 index 00000000..8c008ef4 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/auth/index.ts @@ -0,0 +1,2 @@ +export * from './login'; +export * from './logout'; diff --git a/striker-ui-api/src/lib/request_handlers/auth/login.ts b/striker-ui-api/src/lib/request_handlers/auth/login.ts new file mode 100644 index 00000000..4dbd272b --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/auth/login.ts @@ -0,0 +1,18 @@ +import { RequestHandler } from 'express'; + +import { stdout } from '../../shell'; + +export const login: RequestHandler = ( + request, + response, +) => { + const { user } = request; + + if (user) { + const { name: userName } = user; + + stdout(`Successfully authenticated user [${userName}]`); + } + + response.status(204).send(); +}; diff --git a/striker-ui-api/src/lib/request_handlers/auth/logout.ts b/striker-ui-api/src/lib/request_handlers/auth/logout.ts new file mode 100644 index 00000000..c6b28e24 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/auth/logout.ts @@ -0,0 +1,17 @@ +import { RequestHandler } from 'express'; + +import { stdout } from '../../shell'; + +export const logout: RequestHandler = (request, response) => { + request.session.destroy((error) => { + let scode = 204; + + if (error) { + scode = 500; + + stdout(`Failed to destroy session upon logout; CAUSE: ${error}`); + } + + response.status(scode).send(); + }); +}; diff --git a/striker-ui-api/src/lib/request_handlers/buildGetRequestHandler.ts b/striker-ui-api/src/lib/request_handlers/buildGetRequestHandler.ts index 2ddd6e32..fcb65376 100644 --- a/striker-ui-api/src/lib/request_handlers/buildGetRequestHandler.ts +++ b/striker-ui-api/src/lib/request_handlers/buildGetRequestHandler.ts @@ -1,64 +1,53 @@ import { Request, Response } from 'express'; -import { dbQuery } from '../accessModule'; +import { query } from '../accessModule'; import call from '../call'; +import { stderr, stdout, stdoutVar } from '../shell'; const buildGetRequestHandler = ( - query: string | BuildQueryFunction, + scriptOrCallback: string | BuildQueryFunction, { beforeRespond }: BuildGetRequestHandlerOptions = {}, ) => - (request: Request, response: Response) => { - console.log('Calling CLI script to get data.'); + async (request: Request, response: Response) => { + stdout('Calling CLI script to get data.'); const buildQueryOptions: BuildQueryOptions = {}; - let queryStdout; + let result: (number | null | string)[][]; try { - ({ stdout: queryStdout } = dbQuery( - call(query, { - parameters: [request, buildQueryOptions], - notCallableReturn: query, - }), - )); + const sqlscript: string = + typeof scriptOrCallback === 'function' + ? await scriptOrCallback(request, buildQueryOptions) + : scriptOrCallback; + + result = await query(sqlscript); } catch (queryError) { - console.log(`Failed to execute query; CAUSE: ${queryError}`); + stderr(`Failed to execute query; CAUSE: ${queryError}`); response.status(500).send(); return; } - console.log( - `Query stdout pre-hooks (type=[${typeof queryStdout}]): ${JSON.stringify( - queryStdout, - null, - 2, - )}`, - ); + stdoutVar(result, `Query stdout pre-hooks (type=[${typeof result}]): `); const { afterQueryReturn } = buildQueryOptions; - queryStdout = call(afterQueryReturn, { - parameters: [queryStdout], - notCallableReturn: queryStdout, + result = call(afterQueryReturn, { + parameters: [result], + notCallableReturn: result, }); - queryStdout = call(beforeRespond, { - parameters: [queryStdout], - notCallableReturn: queryStdout, + result = call(beforeRespond, { + parameters: [result], + notCallableReturn: result, }); - console.log( - `Query stdout post-hooks (type=[${typeof queryStdout}]): ${JSON.stringify( - queryStdout, - null, - 2, - )}`, - ); + stdoutVar(result, `Query stdout post-hooks (type=[${typeof result}]): `); - response.json(queryStdout); + response.json(result); }; export default buildGetRequestHandler; diff --git a/striker-ui-api/src/lib/request_handlers/command/buildHostPowerHandler.ts b/striker-ui-api/src/lib/request_handlers/command/buildHostPowerHandler.ts index b69c6377..842f0d51 100644 --- a/striker-ui-api/src/lib/request_handlers/command/buildHostPowerHandler.ts +++ b/striker-ui-api/src/lib/request_handlers/command/buildHostPowerHandler.ts @@ -5,14 +5,14 @@ import SERVER_PATHS from '../../consts/SERVER_PATHS'; import { job } from '../../accessModule'; import { stderr } from '../../shell'; -type DistinctDBJobParams = Omit< - DBJobParams, +type DistinctJobParams = Omit< + JobParams, 'file' | 'line' | 'job_data' | 'job_progress' >; const MANAGE_HOST_POWER_JOB_PARAMS: { - poweroff: DistinctDBJobParams; - reboot: DistinctDBJobParams; + poweroff: DistinctJobParams; + reboot: DistinctJobParams; } = { poweroff: { job_command: `${SERVER_PATHS.usr.sbin['anvil-manage-power'].self} --poweroff -y`, @@ -32,21 +32,19 @@ export const buildHostPowerHandler: ( task?: 'poweroff' | 'reboot', ) => RequestHandler = (task = 'reboot') => - (request, response) => { - const subParams: DBJobParams = { + async (request, response) => { + const subParams: JobParams = { file: __filename, ...MANAGE_HOST_POWER_JOB_PARAMS[task], }; try { - job(subParams); + await job(subParams); } catch (subError) { stderr(`Failed to ${task} host; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } response.status(204).send(); diff --git a/striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts b/striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts index 5b359625..c800dcd2 100644 --- a/striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts +++ b/striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts @@ -1,71 +1,68 @@ +import assert from 'assert'; import { RequestHandler } from 'express'; +import { REP_IPV4, REP_PEACEFUL_STRING } from '../../consts'; import { HOST_KEY_CHANGED_PREFIX } from '../../consts/HOST_KEY_CHANGED_PREFIX'; -import { dbQuery, getLocalHostUUID, getPeerData } from '../../accessModule'; -import { sanitizeSQLParam } from '../../sanitizeSQLParam'; +import { getLocalHostUUID, getPeerData, query } from '../../accessModule'; +import { sanitize } from '../../sanitize'; import { stderr } from '../../shell'; export const getHostSSH: RequestHandler< unknown, - { - badSSHKeys?: DeleteSSHKeyConflictRequestBody; - hostName: string; - hostOS: string; - hostUUID: string; - isConnected: boolean; - isInetConnected: boolean; - isOSRegistered: boolean; - }, - { - password: string; - port?: number; - ipAddress: string; - } -> = (request, response) => { + GetHostSshResponseBody, + GetHostSshRequestBody +> = async (request, response) => { const { - body: { password, port = 22, ipAddress: target }, + body: { password: rPassword, port: rPort = 22, ipAddress: rTarget } = {}, } = request; - let hostName: string; - let hostOS: string; - let hostUUID: string; - let isConnected: boolean; - let isInetConnected: boolean; - let isOSRegistered: boolean; + const password = sanitize(rPassword, 'string'); + const port = sanitize(rPort, 'number'); + const target = sanitize(rTarget, 'string', { modifierType: 'sql' }); + + try { + assert( + REP_PEACEFUL_STRING.test(password), + `Password must be a peaceful string; got [${password}]`, + ); + + assert( + Number.isInteger(port), + `Port must be a valid integer; got [${port}]`, + ); + + assert( + REP_IPV4.test(target), + `IP address must be a valid IPv4 address; got [${target}]`, + ); + } catch (assertError) { + stderr(`Assert failed when getting host SSH data; CAUSE: ${assertError}`); + + return response.status(400).send(); + } const localHostUUID = getLocalHostUUID(); + let rsbody: GetHostSshResponseBody; + try { - ({ - hostName, - hostOS, - hostUUID, - isConnected, - isInetConnected, - isOSRegistered, - } = getPeerData(target, { password, port })); + rsbody = await getPeerData(target, { password, port }); } catch (subError) { stderr(`Failed to get peer data; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } - let badSSHKeys: DeleteSSHKeyConflictRequestBody | undefined; - - if (!isConnected) { - const rows = dbQuery(` + if (!rsbody.isConnected) { + const rows: [stateNote: string, stateUUID: string][] = await query(` SELECT sta.state_note, sta.state_uuid FROM states AS sta WHERE sta.state_host_uuid = '${localHostUUID}' - AND sta.state_name = '${HOST_KEY_CHANGED_PREFIX}${sanitizeSQLParam( - target, - )}';`).stdout as [stateNote: string, stateUUID: string][]; + AND sta.state_name = '${HOST_KEY_CHANGED_PREFIX}${target}';`); if (rows.length > 0) { - badSSHKeys = rows.reduce( + rsbody.badSSHKeys = rows.reduce( (previous, [, stateUUID]) => { previous[localHostUUID].push(stateUUID); @@ -76,13 +73,5 @@ export const getHostSSH: RequestHandler< } } - response.status(200).send({ - badSSHKeys, - hostName, - hostOS, - hostUUID, - isConnected, - isInetConnected, - isOSRegistered, - }); + response.status(200).send(rsbody); }; diff --git a/striker-ui-api/src/lib/request_handlers/command/runManifest.ts b/striker-ui-api/src/lib/request_handlers/command/runManifest.ts index 6f98296a..e6f52447 100644 --- a/striker-ui-api/src/lib/request_handlers/command/runManifest.ts +++ b/striker-ui-api/src/lib/request_handlers/command/runManifest.ts @@ -4,7 +4,13 @@ import { RequestHandler } from 'express'; import { REP_PEACEFUL_STRING, REP_UUID } from '../../consts/REG_EXP_PATTERNS'; import SERVER_PATHS from '../../consts/SERVER_PATHS'; -import { getAnvilData, job, sub } from '../../accessModule'; +import { + getData, + getHostData, + getManifestData, + job, + sub, +} from '../../accessModule'; import { sanitize } from '../../sanitize'; import { stderr } from '../../shell'; @@ -12,7 +18,7 @@ export const runManifest: RequestHandler< { manifestUuid: string }, undefined, RunManifestRequestBody -> = (request, response) => { +> = async (request, response) => { const { params: { manifestUuid }, body: { @@ -75,9 +81,7 @@ export const runManifest: RequestHandler< assert(isHostListUnique, `Each entry in hosts must be unique`); } catch (assertError) { - handleAssertError(assertError); - - return; + return handleAssertError(assertError); } let rawHostListData: AnvilDataHostListHash | undefined; @@ -85,43 +89,19 @@ export const runManifest: RequestHandler< let rawSysData: AnvilDataSysHash | undefined; try { - ({ - hosts: rawHostListData, - manifests: rawManifestListData, - sys: rawSysData, - } = getAnvilData<{ - hosts?: AnvilDataHostListHash; - manifests?: AnvilDataManifestListHash; - sys?: AnvilDataSysHash; - }>( - { hosts: true, manifests: true, sys: true }, - { - predata: [ - ['Database->get_hosts'], - [ - 'Striker->load_manifest', - { - debug, - manifest_uuid: manifestUuid, - }, - ], - ], - }, - )); + rawHostListData = await getHostData(); + rawManifestListData = await getManifestData(manifestUuid); + rawSysData = await getData('sys'); } catch (subError) { stderr( `Failed to get install manifest ${manifestUuid}; CAUSE: ${subError}`, ); - response.status(500).send(); - - return; + return response.status(500).send(); } if (!rawHostListData || !rawManifestListData || !rawSysData) { - response.status(404).send(); - - return; + return response.status(404).send(); } const { host_uuid: hostUuidMapToData } = rawHostListData; @@ -134,9 +114,9 @@ export const runManifest: RequestHandler< } = rawManifestListData; const { hosts: { by_uuid: mapToHostNameData = {} } = {} } = rawSysData; - const joinAnJobs: DBJobParams[] = []; + const joinAnJobs: JobParams[] = []; - let anParams: Record | undefined; + let anParams: Record; try { anParams = Object.values(hostList).reduce>( @@ -177,19 +157,19 @@ export const runManifest: RequestHandler< } try { - const [newAnUuid] = sub('insert_or_update_anvils', { subParams: anParams }) - .stdout as [string]; + const [newAnUuid]: [string] = await sub('insert_or_update_anvils', { + params: [anParams], + }); - joinAnJobs.forEach((jobParams) => { + for (const jobParams of joinAnJobs) { jobParams.job_data += `,anvil_uuid=${newAnUuid}`; - job(jobParams); - }); + + await job(jobParams); + } } catch (subError) { stderr(`Failed to record new anvil node entry; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } response.status(204).send(); diff --git a/striker-ui-api/src/lib/request_handlers/command/updateSystem.ts b/striker-ui-api/src/lib/request_handlers/command/updateSystem.ts index d6ec70d7..d644f52a 100644 --- a/striker-ui-api/src/lib/request_handlers/command/updateSystem.ts +++ b/striker-ui-api/src/lib/request_handlers/command/updateSystem.ts @@ -5,9 +5,9 @@ import SERVER_PATHS from '../../consts/SERVER_PATHS'; import { job } from '../../accessModule'; import { stderr } from '../../shell'; -export const updateSystem: RequestHandler = (request, response) => { +export const updateSystem: RequestHandler = async (request, response) => { try { - job({ + await job({ file: __filename, job_command: SERVER_PATHS.usr.sbin['anvil-update-system'].self, job_description: 'job_0004', @@ -17,9 +17,7 @@ export const updateSystem: RequestHandler = (request, response) => { } catch (subError) { stderr(`Failed to initiate system update; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } response.status(204).send(); diff --git a/striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts b/striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts index beec0a0e..7a723fcd 100644 --- a/striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts +++ b/striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts @@ -1,15 +1,13 @@ import { RequestHandler } from 'express'; -import { getAnvilData } from '../../accessModule'; + +import { getFenceSpec } from '../../accessModule'; import { stderr } from '../../shell'; -export const getFenceTemplate: RequestHandler = (request, response) => { +export const getFenceTemplate: RequestHandler = async (request, response) => { let rawFenceData; try { - ({ fence_data: rawFenceData } = getAnvilData<{ fence_data: unknown }>( - { fence_data: true }, - { predata: [['Striker->get_fence_data']] }, - )); + rawFenceData = await getFenceSpec(); } catch (subError) { stderr(`Failed to get fence device template; CAUSE: ${subError}`); diff --git a/striker-ui-api/src/lib/request_handlers/file/buildQueryFileDetail.ts b/striker-ui-api/src/lib/request_handlers/file/buildQueryFileDetail.ts index 7bb2ba50..3fb2a89f 100644 --- a/striker-ui-api/src/lib/request_handlers/file/buildQueryFileDetail.ts +++ b/striker-ui-api/src/lib/request_handlers/file/buildQueryFileDetail.ts @@ -1,6 +1,9 @@ +import { DELETED } from '../../consts'; + import join from '../../join'; +import { stdoutVar } from '../../shell'; -const buildQueryFileDetail = ({ +export const buildQueryFileDetail = ({ fileUUIDs = ['*'], }: { fileUUIDs?: string[] | '*'; @@ -14,7 +17,7 @@ const buildQueryFileDetail = ({ separator: ', ', }); - console.log(`condFilesUUID=[${condFileUUIDs}]`); + stdoutVar({ condFileUUIDs }); return ` SELECT @@ -33,8 +36,6 @@ const buildQueryFileDetail = ({ ON fil.file_uuid = fil_loc.file_location_file_uuid JOIN anvils AS anv ON fil_loc.file_location_anvil_uuid = anv.anvil_uuid - WHERE fil.file_type != 'DELETED' + WHERE fil.file_type != '${DELETED}' ${condFileUUIDs};`; }; - -export default buildQueryFileDetail; diff --git a/striker-ui-api/src/lib/request_handlers/file/createFile.ts b/striker-ui-api/src/lib/request_handlers/file/createFile.ts new file mode 100644 index 00000000..991138e4 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/file/createFile.ts @@ -0,0 +1,16 @@ +import { RequestHandler } from 'express'; + +import { anvilSyncShared } from '../../accessModule'; +import { stdout, stdoutVar } from '../../shell'; + +export const createFile: RequestHandler = async ({ file, body }, response) => { + stdout('Receiving shared file.'); + + if (!file) return response.status(400).send(); + + stdoutVar({ body, file }); + + await anvilSyncShared('move_incoming', `file=${file.path}`, '0132', '0133'); + + response.status(201).send(); +}; diff --git a/striker-ui-api/src/lib/request_handlers/file/deleteFile.ts b/striker-ui-api/src/lib/request_handlers/file/deleteFile.ts new file mode 100644 index 00000000..c1c36705 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/file/deleteFile.ts @@ -0,0 +1,29 @@ +import { RequestHandler } from 'express'; + +import { DELETED } from '../../consts'; + +import { anvilSyncShared, query, timestamp, write } from '../../accessModule'; + +export const deleteFile: RequestHandler = async (request, response) => { + const { fileUUID } = request.params; + + const [[oldFileType]] = await query( + `SELECT file_type FROM files WHERE file_uuid = '${fileUUID}';`, + ); + + if (oldFileType !== DELETED) { + await write( + `UPDATE files + SET + file_type = '${DELETED}', + modified_date = '${timestamp()}' + WHERE file_uuid = '${fileUUID}';`, + ); + + await anvilSyncShared('purge', `file_uuid=${fileUUID}`, '0136', '0137', { + jobHostUUID: 'all', + }); + } + + response.status(204).send(); +}; diff --git a/striker-ui-api/src/lib/request_handlers/file/getFile.ts b/striker-ui-api/src/lib/request_handlers/file/getFile.ts index 3e28212c..f8876ea9 100644 --- a/striker-ui-api/src/lib/request_handlers/file/getFile.ts +++ b/striker-ui-api/src/lib/request_handlers/file/getFile.ts @@ -1,10 +1,12 @@ import { RequestHandler } from 'express'; +import { DELETED } from '../../consts'; + import buildGetRequestHandler from '../buildGetRequestHandler'; -import buildQueryFileDetail from './buildQueryFileDetail'; +import { buildQueryFileDetail } from './buildQueryFileDetail'; import { sanitize } from '../../sanitize'; -const getFile: RequestHandler = buildGetRequestHandler((request) => { +export const getFile: RequestHandler = buildGetRequestHandler((request) => { const { fileUUIDs } = request.query; let query = ` @@ -15,7 +17,7 @@ const getFile: RequestHandler = buildGetRequestHandler((request) => { file_type, file_md5sum FROM files - WHERE file_type != 'DELETED';`; + WHERE file_type != '${DELETED}';`; if (fileUUIDs) { query = buildQueryFileDetail({ @@ -27,5 +29,3 @@ const getFile: RequestHandler = buildGetRequestHandler((request) => { return query; }); - -export default getFile; diff --git a/striker-ui-api/src/lib/request_handlers/file/getFileDetail.ts b/striker-ui-api/src/lib/request_handlers/file/getFileDetail.ts index 36561213..18201dd2 100644 --- a/striker-ui-api/src/lib/request_handlers/file/getFileDetail.ts +++ b/striker-ui-api/src/lib/request_handlers/file/getFileDetail.ts @@ -1,12 +1,10 @@ import { RequestHandler } from 'express'; import buildGetRequestHandler from '../buildGetRequestHandler'; -import buildQueryFileDetail from './buildQueryFileDetail'; +import { buildQueryFileDetail } from './buildQueryFileDetail'; import { sanitizeSQLParam } from '../../sanitizeSQLParam'; -const getFileDetail: RequestHandler = buildGetRequestHandler( +export const getFileDetail: RequestHandler = buildGetRequestHandler( ({ params: { fileUUID } }) => buildQueryFileDetail({ fileUUIDs: [sanitizeSQLParam(fileUUID)] }), ); - -export default getFileDetail; diff --git a/striker-ui-api/src/lib/request_handlers/file/index.ts b/striker-ui-api/src/lib/request_handlers/file/index.ts new file mode 100644 index 00000000..9eb9f73b --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/file/index.ts @@ -0,0 +1,5 @@ +export * from './createFile'; +export * from './deleteFile'; +export * from './getFile'; +export * from './getFileDetail'; +export * from './updateFile'; diff --git a/striker-ui-api/src/lib/request_handlers/file/updateFile.ts b/striker-ui-api/src/lib/request_handlers/file/updateFile.ts new file mode 100644 index 00000000..8e257b70 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/file/updateFile.ts @@ -0,0 +1,134 @@ +import { RequestHandler } from 'express'; + +import { anvilSyncShared, query, timestamp, write } from '../../accessModule'; +import { stderr, stdoutVar } from '../../shell'; + +export const updateFile: RequestHandler = async (request, response) => { + const { body = {}, params } = request; + + stdoutVar(body, 'Begin edit single file. body='); + + const { fileUUID } = params; + const { fileName, fileLocations, fileType } = body; + const anvilSyncSharedFunctions = []; + + let sqlscript = ''; + + if (fileName) { + const [[oldFileName]] = await query( + `SELECT file_name FROM files WHERE file_uuid = '${fileUUID}';`, + ); + + stdoutVar({ oldFileName, fileName }); + + if (fileName !== oldFileName) { + sqlscript += ` + UPDATE files + SET + file_name = '${fileName}', + modified_date = '${timestamp()}' + WHERE file_uuid = '${fileUUID}';`; + + anvilSyncSharedFunctions.push(() => + anvilSyncShared( + 'rename', + `file_uuid=${fileUUID}\nold_name=${oldFileName}\nnew_name=${fileName}`, + '0138', + '0139', + { jobHostUUID: 'all' }, + ), + ); + } + } + + if (fileType) { + sqlscript += ` + UPDATE files + SET + file_type = '${fileType}', + modified_date = '${timestamp()}' + WHERE file_uuid = '${fileUUID}';`; + + anvilSyncSharedFunctions.push(() => + anvilSyncShared('check_mode', `file_uuid=${fileUUID}`, '0143', '0144', { + jobHostUUID: 'all', + }), + ); + } + + if (fileLocations) { + fileLocations.forEach( + async ({ + fileLocationUUID, + isFileLocationActive, + }: { + fileLocationUUID: string; + isFileLocationActive: boolean; + }) => { + let fileLocationActive = 0; + let jobName = 'purge'; + let jobTitle = '0136'; + let jobDescription = '0137'; + + if (isFileLocationActive) { + fileLocationActive = 1; + jobName = 'pull_file'; + jobTitle = '0132'; + jobDescription = '0133'; + } + + sqlscript += ` + UPDATE file_locations + SET + file_location_active = '${fileLocationActive}', + modified_date = '${timestamp()}' + WHERE file_location_uuid = '${fileLocationUUID}';`; + + const targetHosts: [ + n1uuid: string, + n2uuid: string, + dr1uuid: null | string, + ][] = await query( + `SELECT + anv.anvil_node1_host_uuid, + anv.anvil_node2_host_uuid, + anv.anvil_dr1_host_uuid + FROM anvils AS anv + JOIN file_locations AS fil_loc + ON anv.anvil_uuid = fil_loc.file_location_anvil_uuid + WHERE fil_loc.file_location_uuid = '${fileLocationUUID}';`, + ); + + targetHosts.flat().forEach((hostUUID: null | string) => { + if (hostUUID) { + anvilSyncSharedFunctions.push(() => + anvilSyncShared( + jobName, + `file_uuid=${fileUUID}`, + jobTitle, + jobDescription, + { jobHostUUID: hostUUID }, + ), + ); + } + }); + }, + ); + } + + let wcode: number; + + try { + wcode = await write(sqlscript); + } catch (queryError) { + stderr(`Failed to execute query; CAUSE: ${queryError}`); + + return response.status(500).send(); + } + + anvilSyncSharedFunctions.forEach(async (fn, index) => + stdoutVar(await fn(), `Anvil sync shared [${index}] output: `), + ); + + response.status(200).send(wcode); +}; diff --git a/striker-ui-api/src/lib/request_handlers/host/configStriker.ts b/striker-ui-api/src/lib/request_handlers/host/configStriker.ts index 0ae24596..0b186177 100644 --- a/striker-ui-api/src/lib/request_handlers/host/configStriker.ts +++ b/striker-ui-api/src/lib/request_handlers/host/configStriker.ts @@ -3,13 +3,15 @@ import { RequestHandler } from 'express'; import { REP_DOMAIN, - REP_INTEGER, REP_IPV4, REP_IPV4_CSV, -} from '../../consts/REG_EXP_PATTERNS'; -import SERVER_PATHS from '../../consts/SERVER_PATHS'; + REP_PEACEFUL_STRING, + SERVER_PATHS, +} from '../../consts'; import { job } from '../../accessModule'; +import { sanitize } from '../../sanitize'; +import { stderr, stdoutVar } from '../../shell'; const fvar = (configStepCount: number, fieldName: string) => ['form', `config_step${configStepCount}`, fieldName, 'value'].join('::'); @@ -38,91 +40,90 @@ ${fvar( export const configStriker: RequestHandler< unknown, undefined, - InitializeStrikerForm -> = ({ body }, response) => { - console.log('Begin initialize Striker.'); - console.dir(body, { depth: null }); + Partial +> = async (request, response) => { + const { body = {} } = request; + + stdoutVar(body, 'Begin initialize Striker; body='); const { - adminPassword = '', - domainName = '', - hostName = '', - hostNumber = 0, - networkDNS = '', - networkGateway = '', + adminPassword: rAdminPassword = '', + domainName: rDomainName = '', + hostName: rHostName = '', + hostNumber: rHostNumber = 0, + networkDNS: rNetworkDns = '', + networkGateway: rNetworkGateway = '', networks = [], - organizationName = '', - organizationPrefix = '', - } = body || {}; - - const dataAdminPassword = String(adminPassword); - const dataDomainName = String(domainName); - const dataHostName = String(hostName); - const dataHostNumber = String(hostNumber); - const dataNetworkDNS = String(networkDNS); - const dataNetworkGateway = String(networkGateway); - const dataOrganizationName = String(organizationName); - const dataOrganizationPrefix = String(organizationPrefix); + organizationName: rOrganizationName = '', + organizationPrefix: rOrganizationPrefix = '', + } = body; + + const adminPassword = sanitize(rAdminPassword, 'string'); + const domainName = sanitize(rDomainName, 'string'); + const hostName = sanitize(rHostName, 'string'); + const hostNumber = sanitize(rHostNumber, 'number'); + const networkDns = sanitize(rNetworkDns, 'string'); + const networkGateway = sanitize(rNetworkGateway, 'string'); + const organizationName = sanitize(rOrganizationName, 'string'); + const organizationPrefix = sanitize(rOrganizationPrefix, 'string'); try { assert( - !/['"/\\><}{]/g.test(dataAdminPassword), - `Data admin password cannot contain single-quote, double-quote, slash, backslash, angle brackets, and curly brackets; got [${dataAdminPassword}]`, + REP_PEACEFUL_STRING.test(adminPassword), + `Data admin password cannot contain single-quote, double-quote, slash, backslash, angle brackets, and curly brackets; got [${adminPassword}]`, ); assert( - REP_DOMAIN.test(dataDomainName), - `Data domain name can only contain alphanumeric, hyphen, and dot characters; got [${dataDomainName}]`, + REP_DOMAIN.test(domainName), + `Data domain name can only contain alphanumeric, hyphen, and dot characters; got [${domainName}]`, ); assert( - REP_DOMAIN.test(dataHostName), - `Data host name can only contain alphanumeric, hyphen, and dot characters; got [${dataHostName}]`, + REP_DOMAIN.test(hostName), + `Data host name can only contain alphanumeric, hyphen, and dot characters; got [${hostName}]`, ); assert( - REP_INTEGER.test(dataHostNumber) && hostNumber > 0, - `Data host number can only contain digits; got [${dataHostNumber}]`, + Number.isInteger(hostNumber) && hostNumber > 0, + `Data host number can only contain digits; got [${hostNumber}]`, ); assert( - REP_IPV4_CSV.test(dataNetworkDNS), - `Data network DNS must be a comma separated list of valid IPv4 addresses; got [${dataNetworkDNS}]`, + REP_IPV4_CSV.test(networkDns), + `Data network DNS must be a comma separated list of valid IPv4 addresses; got [${networkDns}]`, ); assert( - REP_IPV4.test(dataNetworkGateway), - `Data network gateway must be a valid IPv4 address; got [${dataNetworkGateway}]`, + REP_IPV4.test(networkGateway), + `Data network gateway must be a valid IPv4 address; got [${networkGateway}]`, ); assert( - dataOrganizationName.length > 0, - `Data organization name cannot be empty; got [${dataOrganizationName}]`, + REP_PEACEFUL_STRING.test(organizationName), + `Data organization name cannot be empty; got [${organizationName}]`, ); assert( - /^[a-z0-9]{1,5}$/.test(dataOrganizationPrefix), - `Data organization prefix can only contain 1 to 5 lowercase alphanumeric characters; got [${dataOrganizationPrefix}]`, + /^[a-z0-9]{1,5}$/.test(organizationPrefix), + `Data organization prefix can only contain 1 to 5 lowercase alphanumeric characters; got [${organizationPrefix}]`, ); } catch (assertError) { - console.log( + stderr( `Failed to assert value when trying to initialize striker; CAUSE: ${assertError}.`, ); - response.status(400).send(); - - return; + return response.status(400).send(); } try { - job({ + await job({ file: __filename, job_command: SERVER_PATHS.usr.sbin['anvil-configure-host'].self, job_data: `${fvar(1, 'domain')}=${domainName} ${fvar(1, 'organization')}=${organizationName} ${fvar(1, 'prefix')}=${organizationPrefix} ${fvar(1, 'sequence')}=${hostNumber} -${fvar(2, 'dns')}=${networkDNS} +${fvar(2, 'dns')}=${networkDns} ${fvar(2, 'gateway')}=${networkGateway} ${fvar(2, 'host_name')}=${hostName} ${fvar(2, 'striker_password')}=${adminPassword} @@ -153,11 +154,9 @@ ${buildNetworkLinks(2, networkShortName, interfaces)}`; job_description: 'job_0071', }); } catch (subError) { - console.log(`Failed to queue striker initialization; CAUSE: ${subError}`); - - response.status(500).send(); + stderr(`Failed to queue striker initialization; CAUSE: ${subError}`); - return; + return response.status(500).send(); } response.status(200).send(); diff --git a/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts index 38a66989..c38cf815 100644 --- a/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts +++ b/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts @@ -3,7 +3,7 @@ import { RequestHandler } from 'express'; import SERVER_PATHS from '../../consts/SERVER_PATHS'; import { - getAnvilData, + getData, getLocalHostUUID, getPeerData, job, @@ -16,7 +16,7 @@ export const createHostConnection: RequestHandler< unknown, undefined, CreateHostConnectionRequestBody -> = (request, response) => { +> = async (request, response) => { const { body: { dbName = 'anvil', @@ -46,16 +46,15 @@ export const createHostConnection: RequestHandler< let peerHostUUID: string; try { - ({ hostUUID: peerHostUUID, isConnected: isPeerReachable } = getPeerData( - peerIPAddress, - { password: commonPassword, port: peerSSHPort }, - )); + ({ hostUUID: peerHostUUID, isConnected: isPeerReachable } = + await getPeerData(peerIPAddress, { + password: commonPassword, + port: peerSSHPort, + })); } catch (subError) { stderr(`Failed to get peer data; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } stdoutVar({ peerHostUUID, isPeerReachable }); @@ -65,22 +64,18 @@ export const createHostConnection: RequestHandler< `Cannot connect to peer; please verify credentials and SSH keys validity.`, ); - response.status(400).send(); - - return; + return response.status(400).send(); } try { - localIPAddress = sub('find_matching_ip', { - subModuleName: 'System', - subParams: { host: peerIPAddress }, - }).stdout; + [localIPAddress] = await sub('find_matching_ip', { + params: [{ host: peerIPAddress }], + pre: ['System'], + }); } catch (subError) { stderr(`Failed to get matching IP address; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } stdoutVar({ localIPAddress }); @@ -94,31 +89,34 @@ export const createHostConnection: RequestHandler< stdoutVar({ pgpassFilePath, pgpassFileBody }); try { - sub('write_file', { - subModuleName: 'Storage', - subParams: { - body: pgpassFileBody, - file: pgpassFilePath, - mode: '0600', - overwrite: 1, - secure: 1, - }, + await sub('write_file', { + params: [ + { + body: pgpassFileBody, + file: pgpassFilePath, + mode: '0600', + overwrite: 1, + secure: 1, + }, + ], + pre: ['Storage'], }); } catch (subError) { stderr(`Failed to write ${pgpassFilePath}; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } try { - const [rawIsPeerDBReachable] = sub('call', { - subModuleName: 'System', - subParams: { - shell_call: `PGPASSFILE="${pgpassFilePath}" ${SERVER_PATHS.usr.bin.psql.self} --host ${peerIPAddress} --port ${commonDBPort} --dbname ${commonDBName} --username ${commonDBUser} --no-password --tuples-only --no-align --command "SELECT 1"`, - }, - }).stdout as [output: string, returnCode: number]; + const [rawIsPeerDBReachable]: [output: string, returnCode: number] = + await sub('call', { + params: [ + { + shell_call: `PGPASSFILE="${pgpassFilePath}" ${SERVER_PATHS.usr.bin.psql.self} --host ${peerIPAddress} --port ${commonDBPort} --dbname ${commonDBName} --username ${commonDBUser} --no-password --tuples-only --no-align --command "SELECT 1"`, + }, + ], + pre: ['System'], + }); isPeerDBReachable = rawIsPeerDBReachable === '1'; } catch (subError) { @@ -130,9 +128,7 @@ export const createHostConnection: RequestHandler< } catch (fsError) { stderr(`Failed to remove ${pgpassFilePath}; CAUSE: ${fsError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } stdoutVar({ isPeerDBReachable }); @@ -142,34 +138,28 @@ export const createHostConnection: RequestHandler< `Cannot connect to peer database; please verify database credentials.`, ); - response.status(400).send(); - - return; + return response.status(400).send(); } const localHostUUID = getLocalHostUUID(); try { const { - database: { - [localHostUUID]: { port: rawLocalDBPort }, - }, - } = getAnvilData<{ database: AnvilDataDatabaseHash }>({ database: true }); + [localHostUUID]: { port: rawLocalDBPort }, + } = await getData('database'); localDBPort = sanitize(rawLocalDBPort, 'number'); } catch (subError) { stderr(`Failed to get local database data from hash; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } const jobCommand = `${SERVER_PATHS.usr.sbin['striker-manage-peers'].self} --add --host-uuid ${peerHostUUID} --host ${peerIPAddress} --port ${commonDBPort} --ping ${commonPing}`; const peerJobCommand = `${SERVER_PATHS.usr.sbin['striker-manage-peers'].self} --add --host-uuid ${localHostUUID} --host ${localIPAddress} --port ${localDBPort} --ping ${commonPing}`; try { - job({ + await job({ file: __filename, job_command: jobCommand, job_data: `password=${commonPassword} @@ -181,9 +171,7 @@ peer_job_command=${peerJobCommand}`, } catch (subError) { stderr(`Failed to add peer ${peerHostUUID}; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } response.status(201).send(); diff --git a/striker-ui-api/src/lib/request_handlers/host/deleteHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/deleteHostConnection.ts index 3f23f7f4..2470bf05 100644 --- a/striker-ui-api/src/lib/request_handlers/host/deleteHostConnection.ts +++ b/striker-ui-api/src/lib/request_handlers/host/deleteHostConnection.ts @@ -10,33 +10,31 @@ export const deleteHostConnection: RequestHandler< unknown, undefined, DeleteHostConnectionRequestBody -> = (request, response) => { +> = async (request, response) => { const { body } = request; - const hostUUIDs = Object.keys(body); + const hostUuids = Object.keys(body); - hostUUIDs.forEach((key) => { - const hostUUID = toHostUUID(key); - const peerHostUUIDs = body[key]; + for (const key of hostUuids) { + const hostUuid = toHostUUID(key); + const peerHostUuids = body[key]; - peerHostUUIDs.forEach((peerHostUUID) => { + for (const peerHostUuid of peerHostUuids) { try { - job({ + await job({ file: __filename, - job_command: `${SERVER_PATHS.usr.sbin['striker-manage-peers'].self} --remove --host-uuid ${peerHostUUID}`, + job_command: `${SERVER_PATHS.usr.sbin['striker-manage-peers'].self} --remove --host-uuid ${peerHostUuid}`, job_description: 'job_0014', - job_host_uuid: hostUUID, + job_host_uuid: hostUuid, job_name: 'striker-peer::delete', job_title: 'job_0013', }); } catch (subError) { - stderr(`Failed to delete peer ${peerHostUUID}; CAUSE: ${subError}`); + stderr(`Failed to delete peer ${peerHostUuid}; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } - }); - }); + } + } response.status(204).send(); }; diff --git a/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts index cf5e5453..0f06c9d5 100644 --- a/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts +++ b/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts @@ -1,4 +1,4 @@ -import { getAnvilData, getLocalHostUUID } from '../../accessModule'; +import { getData, getLocalHostUUID } from '../../accessModule'; import { buildUnknownIDCondition } from '../../buildCondition'; import buildGetRequestHandler from '../buildGetRequestHandler'; import { toLocal } from '../../convertHostUUID'; @@ -39,7 +39,7 @@ const buildHostConnections = ( ); export const getHostConnection = buildGetRequestHandler( - (request, buildQueryOptions) => { + async (request, buildQueryOptions) => { const { hostUUIDs: rawHostUUIDs } = request.query; let rawDatabaseData: AnvilDataDatabaseHash; @@ -59,9 +59,7 @@ export const getHostConnection = buildGetRequestHandler( stdout(`condHostUUIDs=[${condHostUUIDs}]`); try { - ({ database: rawDatabaseData } = getAnvilData<{ database: AnvilDataDatabaseHash }>( - { database: true }, - )); + rawDatabaseData = await getData('database'); } catch (subError) { throw new Error(`Failed to get anvil data; CAUSE: ${subError}`); } diff --git a/striker-ui-api/src/lib/request_handlers/host/prepareHost.ts b/striker-ui-api/src/lib/request_handlers/host/prepareHost.ts index 3b402136..326718af 100644 --- a/striker-ui-api/src/lib/request_handlers/host/prepareHost.ts +++ b/striker-ui-api/src/lib/request_handlers/host/prepareHost.ts @@ -6,8 +6,8 @@ import { REP_IPV4, REP_PEACEFUL_STRING, REP_UUID, -} from '../../consts/REG_EXP_PATTERNS'; -import SERVER_PATHS from '../../consts/SERVER_PATHS'; + SERVER_PATHS, +} from '../../consts'; import { job, variable } from '../../accessModule'; import { sanitize } from '../../sanitize'; @@ -17,7 +17,7 @@ export const prepareHost: RequestHandler< unknown, undefined, PrepareHostRequestBody -> = (request, response) => { +> = async (request, response) => { const { body: { enterpriseUUID, @@ -106,14 +106,12 @@ export const prepareHost: RequestHandler< `Failed to assert value when trying to prepare host; CAUSE: ${assertError}`, ); - response.status(400).send(); - - return; + return response.status(400).send(); } try { if (isHostUUIDProvided) { - variable({ + await variable({ file: __filename, update_value_only: 1, variable_name: 'system::configured', @@ -123,7 +121,7 @@ export const prepareHost: RequestHandler< }); } - job({ + await job({ file: __filename, job_command: SERVER_PATHS.usr.sbin['striker-initialize-host'].self, job_data: `enterprise_uuid=${dataEnterpriseUUID} @@ -141,9 +139,7 @@ type=${dataHostType}`, } catch (subError) { stderr(`Failed to init host; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } response.status(200).send(); diff --git a/striker-ui-api/src/lib/request_handlers/host/setHostInstallTarget.ts b/striker-ui-api/src/lib/request_handlers/host/setHostInstallTarget.ts index d74c0b40..e6785dec 100644 --- a/striker-ui-api/src/lib/request_handlers/host/setHostInstallTarget.ts +++ b/striker-ui-api/src/lib/request_handlers/host/setHostInstallTarget.ts @@ -4,36 +4,38 @@ import { LOCAL } from '../../consts/LOCAL'; import SERVER_PATHS from '../../consts/SERVER_PATHS'; import { job } from '../../accessModule'; -import { stderr, stdout } from '../../shell'; - -export const setHostInstallTarget: RequestHandler = (request, response) => { - stdout( - `Begin set host install target.\n${JSON.stringify(request.body, null, 2)}`, - ); - - const { isEnableInstallTarget } = - request.body as SetHostInstallTargetRequestBody; - const { hostUUID: rawHostUUID } = request.params as UpdateHostParams; - const hostUUID: string | undefined = - rawHostUUID === LOCAL ? undefined : rawHostUUID; +import { stderr, stdoutVar } from '../../shell'; + +export const setHostInstallTarget: RequestHandler< + UpdateHostParams, + undefined, + SetHostInstallTargetRequestBody +> = async (request, response) => { + const { body, params } = request; + + stdoutVar(body, `Begin set host install target; body=`); + + const { isEnableInstallTarget } = body; + const { hostUUID: rHostUuid } = params; + + const hostUuid: string | undefined = + rHostUuid === LOCAL ? undefined : rHostUuid; const task = isEnableInstallTarget ? 'enable' : 'disable'; try { - job({ + await job({ file: __filename, job_command: `${SERVER_PATHS.usr.sbin['striker-manage-install-target'].self} --${task}`, job_description: 'job_0016', - job_host_uuid: hostUUID, + job_host_uuid: hostUuid, job_name: `install-target::${task}`, job_title: 'job_0015', }); } catch (subError) { stderr(`Failed to ${task} install target; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } - response.status(200).send(); + response.status(204).send(); }; diff --git a/striker-ui-api/src/lib/request_handlers/manifest/buildManifest.ts b/striker-ui-api/src/lib/request_handlers/manifest/buildManifest.ts index 249499d8..a6fcda08 100644 --- a/striker-ui-api/src/lib/request_handlers/manifest/buildManifest.ts +++ b/striker-ui-api/src/lib/request_handlers/manifest/buildManifest.ts @@ -13,7 +13,7 @@ import { sub } from '../../accessModule'; import { sanitize } from '../../sanitize'; import { stdout } from '../../shell'; -export const buildManifest = ( +export const buildManifest = async ( ...[request]: Parameters< RequestHandler< { manifestUuid?: string }, @@ -257,24 +257,29 @@ export const buildManifest = ( {}, ); - let result: { name: string; uuid: string } | undefined; + let result: { name: string; uuid: string }; try { - const [uuid, name] = sub('generate_manifest', { - subModuleName: 'Striker', - subParams: { - dns, - domain, - manifest_uuid: manifestUuid, - mtu, - ntp, - prefix, - sequence, - ...networkCountContainer, - ...networkContainer, - ...hostContainer, + const [uuid, name]: [manifestUuid: string, anvilName: string] = await sub( + 'generate_manifest', + { + params: [ + { + dns, + domain, + manifest_uuid: manifestUuid, + mtu, + ntp, + prefix, + sequence, + ...networkCountContainer, + ...networkContainer, + ...hostContainer, + }, + ], + pre: ['Striker'], }, - }).stdout as [manifestUuid: string, anvilName: string]; + ); result = { name, uuid }; } catch (subError) { diff --git a/striker-ui-api/src/lib/request_handlers/manifest/createManifest.ts b/striker-ui-api/src/lib/request_handlers/manifest/createManifest.ts index b2c5331d..292e249f 100644 --- a/striker-ui-api/src/lib/request_handlers/manifest/createManifest.ts +++ b/striker-ui-api/src/lib/request_handlers/manifest/createManifest.ts @@ -4,13 +4,13 @@ import { RequestHandler } from 'express'; import { buildManifest } from './buildManifest'; import { stderr } from '../../shell'; -export const createManifest: RequestHandler = (...handlerArgs) => { +export const createManifest: RequestHandler = async (...handlerArgs) => { const [, response] = handlerArgs; let result: Record = {}; try { - result = buildManifest(...handlerArgs); + result = await buildManifest(...handlerArgs); } catch (buildError) { stderr(`Failed to create new install manifest; CAUSE ${buildError}`); diff --git a/striker-ui-api/src/lib/request_handlers/manifest/deleteManifest.ts b/striker-ui-api/src/lib/request_handlers/manifest/deleteManifest.ts index 0ba2d14b..870906e7 100644 --- a/striker-ui-api/src/lib/request_handlers/manifest/deleteManifest.ts +++ b/striker-ui-api/src/lib/request_handlers/manifest/deleteManifest.ts @@ -7,7 +7,7 @@ export const deleteManifest: RequestHandler< { manifestUuid: string }, undefined, { uuids: string[] } -> = (request, response) => { +> = async (request, response) => { const { params: { manifestUuid: rawManifestUuid }, body: { uuids: rawManifestUuidList } = {}, @@ -17,21 +17,19 @@ export const deleteManifest: RequestHandler< ? rawManifestUuidList : [rawManifestUuid]; - manifestUuidList.forEach((uuid) => { + for (const uuid of manifestUuidList) { stdout(`Begin delete manifest ${uuid}.`); try { - sub('insert_or_update_manifests', { - subParams: { delete: 1, manifest_uuid: uuid }, + await sub('insert_or_update_manifests', { + params: [{ delete: 1, manifest_uuid: uuid }], }); } catch (subError) { stderr(`Failed to delete manifest ${uuid}; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } - }); + } response.status(204).send(); }; diff --git a/striker-ui-api/src/lib/request_handlers/manifest/getManifestDetail.ts b/striker-ui-api/src/lib/request_handlers/manifest/getManifestDetail.ts index 0da929a9..fcabc918 100644 --- a/striker-ui-api/src/lib/request_handlers/manifest/getManifestDetail.ts +++ b/striker-ui-api/src/lib/request_handlers/manifest/getManifestDetail.ts @@ -1,6 +1,6 @@ import { RequestHandler } from 'express'; -import { getAnvilData } from '../../accessModule'; +import { getManifestData } from '../../accessModule'; import { getEntityParts } from '../../disassembleEntityId'; import { stderr, stdout } from '../../shell'; @@ -67,25 +67,18 @@ const handleSortNetworks = ( return result; }; -export const getManifestDetail: RequestHandler = (request, response) => { +export const getManifestDetail: RequestHandler = async (request, response) => { const { - params: { manifestUUID }, + params: { manifestUUID: manifestUuid }, } = request; let rawManifestListData: AnvilDataManifestListHash | undefined; try { - ({ manifests: rawManifestListData } = getAnvilData<{ - manifests?: AnvilDataManifestListHash; - }>( - { manifests: true }, - { - predata: [['Striker->load_manifest', { manifest_uuid: manifestUUID }]], - }, - )); + rawManifestListData = await getManifestData(manifestUuid); } catch (subError) { stderr( - `Failed to get install manifest ${manifestUUID}; CAUSE: ${subError}`, + `Failed to get install manifest ${manifestUuid}; CAUSE: ${subError}`, ); response.status(500).send(); @@ -109,7 +102,7 @@ export const getManifestDetail: RequestHandler = (request, response) => { const { manifest_uuid: { - [manifestUUID]: { + [manifestUuid]: { parsed: { domain, fences: fenceUuidList = {}, diff --git a/striker-ui-api/src/lib/request_handlers/manifest/getManifestTemplate.ts b/striker-ui-api/src/lib/request_handlers/manifest/getManifestTemplate.ts index d3a87085..8a02b345 100644 --- a/striker-ui-api/src/lib/request_handlers/manifest/getManifestTemplate.ts +++ b/striker-ui-api/src/lib/request_handlers/manifest/getManifestTemplate.ts @@ -1,6 +1,6 @@ import { RequestHandler } from 'express'; -import { dbQuery, getLocalHostName } from '../../accessModule'; +import { getLocalHostName, query } from '../../accessModule'; import { getHostNameDomain, getHostNamePrefix, @@ -8,17 +8,18 @@ import { } from '../../disassembleHostName'; import { stderr } from '../../shell'; -export const getManifestTemplate: RequestHandler = (request, response) => { - let localHostName = ''; +export const getManifestTemplate: RequestHandler = async ( + request, + response, +) => { + let localHostName: string; try { localHostName = getLocalHostName(); } catch (subError) { stderr(String(subError)); - response.status(500).send(); - - return; + return response.status(500).send(); } const localShortHostName = getShortHostName(localHostName); @@ -38,7 +39,7 @@ export const getManifestTemplate: RequestHandler = (request, response) => { >; try { - ({ stdout: rawQueryResult } = dbQuery( + rawQueryResult = await query( `SELECT a.fence_uuid, a.fence_name, @@ -71,13 +72,11 @@ export const getManifestTemplate: RequestHandler = (request, response) => { ORDER BY manifest_name DESC LIMIT 1 ) AS c ON a.row_number = c.row_number;`, - )); + ); } catch (queryError) { stderr(`Failed to execute query; CAUSE: ${queryError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } const queryResult = rawQueryResult.reduce< diff --git a/striker-ui-api/src/lib/request_handlers/manifest/updateManifest.ts b/striker-ui-api/src/lib/request_handlers/manifest/updateManifest.ts index 728d0af7..9d583ac6 100644 --- a/striker-ui-api/src/lib/request_handlers/manifest/updateManifest.ts +++ b/striker-ui-api/src/lib/request_handlers/manifest/updateManifest.ts @@ -4,7 +4,7 @@ import { RequestHandler } from 'express'; import { buildManifest } from './buildManifest'; import { stderr } from '../../shell'; -export const updateManifest: RequestHandler = (...args) => { +export const updateManifest: RequestHandler = async (...args) => { const [request, response] = args; const { params: { manifestUuid }, @@ -13,7 +13,7 @@ export const updateManifest: RequestHandler = (...args) => { let result: Record = {}; try { - result = buildManifest(...args); + result = await buildManifest(...args); } catch (buildError) { stderr( `Failed to update install manifest ${manifestUuid}; CAUSE: ${buildError}`, diff --git a/striker-ui-api/src/lib/request_handlers/server/createServer.ts b/striker-ui-api/src/lib/request_handlers/server/createServer.ts index cae0d884..4be884f0 100644 --- a/striker-ui-api/src/lib/request_handlers/server/createServer.ts +++ b/striker-ui-api/src/lib/request_handlers/server/createServer.ts @@ -1,107 +1,120 @@ import assert from 'assert'; import { RequestHandler } from 'express'; +import { REP_UUID, SERVER_PATHS } from '../../consts'; import { OS_LIST_MAP } from '../../consts/OS_LIST'; -import { REP_INTEGER, REP_UUID } from '../../consts/REG_EXP_PATTERNS'; -import SERVER_PATHS from '../../consts/SERVER_PATHS'; -import { dbQuery, job } from '../../accessModule'; -import { stderr, stdout } from '../../shell'; +import { job, query } from '../../accessModule'; +import { sanitize } from '../../sanitize'; +import { stderr, stdout, stdoutVar } from '../../shell'; -export const createServer: RequestHandler = ({ body }, response) => { - stdout(`Creating server.\n${JSON.stringify(body, null, 2)}`); +export const createServer: RequestHandler = async (request, response) => { + const { body = {} } = request; + + stdoutVar(body, 'Creating server; body='); const { - serverName, - cpuCores, - memory, + serverName: rServerName, + cpuCores: rCpuCores, + memory: rMemory, virtualDisks: [ - { storageSize = undefined, storageGroupUUID = undefined } = {}, + { + storageSize: rStorageSize = undefined, + storageGroupUUID: rStorageGroupUuid = undefined, + } = {}, ] = [], - installISOFileUUID, - driverISOFileUUID, - anvilUUID, - optimizeForOS, - } = body || {}; - - const dataServerName = String(serverName); - const dataOS = String(optimizeForOS); - const dataCPUCores = String(cpuCores); - const dataRAM = String(memory); - const dataStorageGroupUUID = String(storageGroupUUID); - const dataStorageSize = String(storageSize); - const dataInstallISO = String(installISOFileUUID); - const dataDriverISO = String(driverISOFileUUID) || 'none'; - const dataAnvilUUID = String(anvilUUID); + installISOFileUUID: rInstallIsoUuid, + driverISOFileUUID: rDriverIsoUuid, + anvilUUID: rAnvilUuid, + optimizeForOS: rOptimizeForOs, + } = body; + + const serverName = sanitize(rServerName, 'string'); + const os = sanitize(rOptimizeForOs, 'string'); + const cpuCores = sanitize(rCpuCores, 'number'); + const memory = sanitize(rMemory, 'number'); + const storageGroupUUID = sanitize(rStorageGroupUuid, 'string'); + const storageSize = sanitize(rStorageSize, 'number'); + const installIsoUuid = sanitize(rInstallIsoUuid, 'string'); + const driverIsoUuid = sanitize(rDriverIsoUuid, 'string', { + fallback: 'none', + }); + const anvilUuid = sanitize(rAnvilUuid, 'string'); try { assert( - /^[0-9a-z_-]+$/i.test(dataServerName), - `Data server name can only contain alphanumeric, underscore, and hyphen characters; got [${dataServerName}].`, + /^[0-9a-z_-]+$/i.test(serverName), + `Data server name can only contain alphanumeric, underscore, and hyphen characters; got [${serverName}]`, ); - const [[serverNameCount]] = dbQuery( - `SELECT COUNT(server_uuid) FROM servers WHERE server_name = '${dataServerName}'`, - ).stdout; + const [[serverNameCount]] = await query( + `SELECT COUNT(server_uuid) FROM servers WHERE server_name = '${serverName}'`, + ); assert( serverNameCount === 0, - `Data server name already exists; got [${dataServerName}]`, + `Data server name already exists; got [${serverName}]`, ); + assert( - OS_LIST_MAP[dataOS] !== undefined, - `Data OS not recognized; got [${dataOS}].`, + OS_LIST_MAP[os] !== undefined, + `Data OS not recognized; got [${os}]`, ); + assert( - REP_INTEGER.test(dataCPUCores), - `Data CPU cores can only contain digits; got [${dataCPUCores}].`, + Number.isInteger(cpuCores), + `Data CPU cores can only contain digits; got [${cpuCores}]`, ); + assert( - REP_INTEGER.test(dataRAM), - `Data RAM can only contain digits; got [${dataRAM}].`, + Number.isInteger(memory), + `Data RAM can only contain digits; got [${memory}]`, ); + assert( - REP_UUID.test(dataStorageGroupUUID), - `Data storage group UUID must be a valid UUID; got [${dataStorageGroupUUID}].`, + REP_UUID.test(storageGroupUUID), + `Data storage group UUID must be a valid UUID; got [${storageGroupUUID}]`, ); + assert( - REP_INTEGER.test(dataStorageSize), - `Data storage size can only contain digits; got [${dataStorageSize}].`, + Number.isInteger(storageSize), + `Data storage size can only contain digits; got [${storageSize}]`, ); + assert( - REP_UUID.test(dataInstallISO), - `Data install ISO must be a valid UUID; got [${dataInstallISO}].`, + REP_UUID.test(installIsoUuid), + `Data install ISO must be a valid UUID; got [${installIsoUuid}]`, ); + assert( - dataDriverISO === 'none' || REP_UUID.test(dataDriverISO), - `Data driver ISO must be a valid UUID when provided; got [${dataDriverISO}].`, + driverIsoUuid === 'none' || REP_UUID.test(driverIsoUuid), + `Data driver ISO must be a valid UUID when provided; got [${driverIsoUuid}]`, ); + assert( - REP_UUID.test(dataAnvilUUID), - `Data anvil UUID must be a valid UUID; got [${dataAnvilUUID}].`, + REP_UUID.test(anvilUuid), + `Data anvil UUID must be a valid UUID; got [${anvilUuid}]`, ); } catch (assertError) { stdout( - `Failed to assert value when trying to provision a server; CAUSE: ${assertError}.`, + `Failed to assert value when trying to provision a server; CAUSE: ${assertError}`, ); - response.status(400).send(); - - return; + return response.status(400).send(); } - const provisionServerJobData = `server_name=${dataServerName} -os=${dataOS} -cpu_cores=${dataCPUCores} -ram=${dataRAM} -storage_group_uuid=${dataStorageGroupUUID} -storage_size=${dataStorageSize} -install_iso=${dataInstallISO} -driver_iso=${dataDriverISO}`; + const provisionServerJobData = `server_name=${serverName} +os=${os} +cpu_cores=${cpuCores} +ram=${memory} +storage_group_uuid=${storageGroupUUID} +storage_size=${storageSize} +install_iso=${installIsoUuid} +driver_iso=${driverIsoUuid}`; stdout(`provisionServerJobData=[${provisionServerJobData}]`); - const [[provisionServerJobHostUUID]] = dbQuery( + const [[provisionServerJobHostUUID]]: [[string]] = await query( `SELECT CASE WHEN pri_hos.primary_host_uuid IS NULL @@ -120,7 +133,7 @@ driver_iso=${dataDriverISO}`; AND sca_clu_nod.scan_cluster_node_crmd_member AND sca_clu_nod.scan_cluster_node_cluster_member AND (NOT sca_clu_nod.scan_cluster_node_maintenance_mode) - AND anv.anvil_uuid = '${dataAnvilUUID}' + AND anv.anvil_uuid = '${anvilUuid}' ORDER BY sca_clu_nod.scan_cluster_node_name LIMIT 1 ) AS pri_hos @@ -129,15 +142,15 @@ driver_iso=${dataDriverISO}`; 1 AS phr, anv.anvil_node1_host_uuid AS node1_host_uuid FROM anvils AS anv - WHERE anv.anvil_uuid = '${dataAnvilUUID}' + WHERE anv.anvil_uuid = '${anvilUuid}' ) AS nod_1 ON pri_hos.phl = nod_1.phr;`, - ).stdout; + ); stdout(`provisionServerJobHostUUID=[${provisionServerJobHostUUID}]`); try { - job({ + await job({ file: __filename, job_command: SERVER_PATHS.usr.sbin['anvil-provision-server'].self, job_data: provisionServerJobData, @@ -149,9 +162,7 @@ driver_iso=${dataDriverISO}`; } catch (subError) { stderr(`Failed to provision server; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } response.status(202).send(); diff --git a/striker-ui-api/src/lib/request_handlers/server/getServer.ts b/striker-ui-api/src/lib/request_handlers/server/getServer.ts index 5aebe5b3..516e73ed 100644 --- a/striker-ui-api/src/lib/request_handlers/server/getServer.ts +++ b/striker-ui-api/src/lib/request_handlers/server/getServer.ts @@ -1,6 +1,7 @@ import buildGetRequestHandler from '../buildGetRequestHandler'; import join from '../../join'; import { sanitize } from '../../sanitize'; +import { stdoutVar } from '../../shell'; export const getServer = buildGetRequestHandler( (request, buildQueryOptions) => { @@ -13,7 +14,7 @@ export const getServer = buildGetRequestHandler( separator: ', ', }); - console.log(`condAnvilsUUID=[${condAnvilUUIDs}]`); + stdoutVar({ condAnvilUUIDs }); if (buildQueryOptions) { buildQueryOptions.afterQueryReturn = (queryStdout) => { diff --git a/striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts b/striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts index c847133e..befd04ef 100644 --- a/striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts +++ b/striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts @@ -3,10 +3,9 @@ import { RequestHandler } from 'express'; import { createReadStream } from 'fs'; import path from 'path'; -import { REP_UUID } from '../../consts/REG_EXP_PATTERNS'; -import SERVER_PATHS from '../../consts/SERVER_PATHS'; +import { REP_UUID, SERVER_PATHS } from '../../consts'; -import { dbQuery, getLocalHostUUID, job } from '../../accessModule'; +import { getLocalHostUUID, job, query } from '../../accessModule'; import { sanitize } from '../../sanitize'; import { mkfifo, rm, stderr, stdout } from '../../shell'; @@ -18,7 +17,7 @@ const rmfifo = (path: string) => { } }; -export const getServerDetail: RequestHandler = (request, response) => { +export const getServerDetail: RequestHandler = async (request, response) => { const { serverUUID } = request.params; const { ss, resize } = request.query; @@ -39,9 +38,7 @@ export const getServerDetail: RequestHandler = (request, response) => { `Failed to assert value when trying to get server detail; CAUSE: ${assertError}.`, ); - response.status(500).send(); - - return; + return response.status(500).send(); } if (isScreenshot) { @@ -52,24 +49,20 @@ export const getServerDetail: RequestHandler = (request, response) => { } catch (subError) { stderr(String(subError)); - response.status(500).send(); - - return; + return response.status(500).send(); } stdout(`requestHostUUID=[${requestHostUUID}]`); try { - [[serverHostUUID]] = dbQuery(` + [[serverHostUUID]] = await query(` SELECT server_host_uuid FROM servers - WHERE server_uuid = '${serverUUID}';`).stdout; + WHERE server_uuid = '${serverUUID}';`); } catch (queryError) { stderr(`Failed to get server host UUID; CAUSE: ${queryError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } stdout(`serverHostUUID=[${serverHostUUID}]`); @@ -94,9 +87,9 @@ export const getServerDetail: RequestHandler = (request, response) => { namedPipeReadStream.once('close', () => { stdout(`On close; removing named pipe at ${imageFilePath}.`); - response.status(200).send({ screenshot: imageData }); - rmfifo(imageFilePath); + + return response.status(200).send({ screenshot: imageData }); }); namedPipeReadStream.on('data', (data) => { @@ -123,11 +116,9 @@ export const getServerDetail: RequestHandler = (request, response) => { `Failed to prepare named pipe and/or receive image data; CAUSE: ${prepPipeError}`, ); - response.status(500).send(); - rmfifo(imageFilePath); - return; + return response.status(500).send(); } let resizeArgs = sanitize(resize, 'string'); @@ -137,7 +128,7 @@ export const getServerDetail: RequestHandler = (request, response) => { } try { - job({ + await job({ file: __filename, job_command: SERVER_PATHS.usr.sbin['anvil-get-server-screenshot'].self, job_data: `server-uuid=${serverUUID} @@ -152,9 +143,7 @@ out-file-id=${epoch}`, } catch (subError) { stderr(`Failed to queue fetch server screenshot job; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } } else { // For getting sever detail data. diff --git a/striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts b/striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts index c4144efb..16aa9687 100644 --- a/striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts +++ b/striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts @@ -9,33 +9,31 @@ import { stderr } from '../../shell'; export const deleteSSHKeyConflict: RequestHandler< unknown, undefined, - DeleteSSHKeyConflictRequestBody -> = (request, response) => { + DeleteSshKeyConflictRequestBody +> = async (request, response) => { const { body } = request; - const hostUUIDs = Object.keys(body); + const hostUuids = Object.keys(body); - hostUUIDs.forEach((key) => { - const hostUUID = toHostUUID(key); - const stateUUIDs = body[key]; + for (const uuid of hostUuids) { + const hostUuid = toHostUUID(uuid); + const stateUuids = body[uuid]; try { - job({ + await job({ file: __filename, job_command: SERVER_PATHS.usr.sbin['anvil-manage-keys'].self, - job_data: stateUUIDs.join(','), + job_data: stateUuids.join(','), job_description: 'job_0057', - job_host_uuid: hostUUID, + job_host_uuid: hostUuid, job_name: 'manage::broken_keys', job_title: 'job_0056', }); } catch (subError) { stderr(`Failed to delete bad SSH keys; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } - }); + } response.status(204).send(); }; diff --git a/striker-ui-api/src/lib/request_handlers/ssh-key/getSSHKeyConflict.ts b/striker-ui-api/src/lib/request_handlers/ssh-key/getSSHKeyConflict.ts index fcb29b94..d808e64c 100644 --- a/striker-ui-api/src/lib/request_handlers/ssh-key/getSSHKeyConflict.ts +++ b/striker-ui-api/src/lib/request_handlers/ssh-key/getSSHKeyConflict.ts @@ -22,16 +22,7 @@ export const getSSHKeyConflict = buildGetRequestHandler( ON sta.state_host_uuid = hos.host_uuid WHERE sta.state_name LIKE '${HOST_KEY_CHANGED_PREFIX}%';`; const afterQueryReturn = buildQueryResultReducer<{ - [hostUUID: string]: { - [stateUUID: string]: { - badFile: string; - badLine: number; - hostName: string; - hostUUID: string; - ipAddress: string; - stateUUID: string; - }; - }; + [hostUUID: string]: SshKeyConflict; }>((previous, [hostName, hostUUID, stateName, stateNote, stateUUID]) => { const hostUUIDKey = toLocal(hostUUID, localHostUUID); diff --git a/striker-ui-api/src/lib/request_handlers/ups/getUPS.ts b/striker-ui-api/src/lib/request_handlers/ups/getUPS.ts index e884e54f..099cdbc8 100644 --- a/striker-ui-api/src/lib/request_handlers/ups/getUPS.ts +++ b/striker-ui-api/src/lib/request_handlers/ups/getUPS.ts @@ -14,7 +14,7 @@ export const getUPS: RequestHandler = buildGetRequestHandler( FROM upses ORDER BY ups_name ASC;`; const afterQueryReturn: QueryResultModifierFunction | undefined = - buildQueryResultReducer<{ [upsUUID: string]: UPSOverview }>( + buildQueryResultReducer<{ [upsUUID: string]: UpsOverview }>( (previous, [upsUUID, upsName, upsAgent, upsIPAddress]) => { previous[upsUUID] = { upsAgent, diff --git a/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts index 3c4a8b9c..3c3f4f76 100644 --- a/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts +++ b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts @@ -1,16 +1,13 @@ import { RequestHandler } from 'express'; -import { getAnvilData } from '../../accessModule'; +import { getUpsSpec } from '../../accessModule'; import { stderr } from '../../shell'; -export const getUPSTemplate: RequestHandler = (request, response) => { +export const getUPSTemplate: RequestHandler = async (request, response) => { let rawUPSData: AnvilDataUPSHash; try { - ({ ups_data: rawUPSData } = getAnvilData<{ ups_data: AnvilDataUPSHash }>( - { ups_data: true }, - { predata: [['Striker->get_ups_data']] }, - )); + rawUPSData = await getUpsSpec(); } catch (subError) { stderr(`Failed to get ups template; CAUSE: ${subError}`); @@ -21,13 +18,13 @@ export const getUPSTemplate: RequestHandler = (request, response) => { const upsData: AnvilDataUPSHash = Object.entries( rawUPSData, - ).reduce((previous, [upsTypeId, value]) => { + ).reduce((previous, [upsTypeId, value]) => { const { brand, description: rawDescription, ...rest } = value; const matched = rawDescription.match( /^(.+)\s+[-]\s+[<][^>]+href=[\\"]+([^\s]+)[\\"]+.+[>]([^<]+)[<]/, ); - const result: UPSTemplate[string] = { + const result: UpsTemplate[string] = { ...rest, brand, description: rawDescription, diff --git a/striker-ui-api/src/lib/request_handlers/user/deleteUser.ts b/striker-ui-api/src/lib/request_handlers/user/deleteUser.ts new file mode 100644 index 00000000..2170ba3c --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/user/deleteUser.ts @@ -0,0 +1,59 @@ +import assert from 'assert'; +import { RequestHandler } from 'express'; + +import { DELETED, REP_UUID } from '../../consts'; + +import { write } from '../../accessModule'; +import join from '../../join'; +import { sanitize } from '../../sanitize'; +import { stderr, stdoutVar } from '../../shell'; + +export const deleteUser: RequestHandler< + DeleteUserParamsDictionary, + undefined, + DeleteUserRequestBody +> = async (request, response) => { + const { + body: { uuids: rawUserUuidList } = {}, + params: { userUuid }, + } = request; + + const userUuidList = sanitize(rawUserUuidList, 'string[]'); + + const ulist = userUuidList.length > 0 ? userUuidList : [userUuid]; + + stdoutVar({ ulist }); + + try { + let failedIndex = 0; + + assert( + ulist.every((uuid, index) => { + failedIndex = index; + + return REP_UUID.test(uuid); + }), + `All UUIDs must be valid UUIDv4; failed at ${failedIndex}, got [${ulist[failedIndex]}]`, + ); + } catch (assertError) { + stderr(`Failed to assert value during delete user; CAUSE: ${assertError}`); + + return response.status(400).send(); + } + + try { + const wcode = await write( + `UPDATE users + SET user_algorithm = '${DELETED}' + WHERE user_uuid IN (${join(ulist)});`, + ); + + assert(wcode === 0, `Write exited with code ${wcode}`); + } catch (error) { + stderr(`Failed to delete user(s); CAUSE: ${error}`); + + return response.status(500).send(); + } + + response.status(204).send(); +}; diff --git a/striker-ui-api/src/lib/request_handlers/user/index.ts b/striker-ui-api/src/lib/request_handlers/user/index.ts index 3f9351e9..2649ee0e 100644 --- a/striker-ui-api/src/lib/request_handlers/user/index.ts +++ b/striker-ui-api/src/lib/request_handlers/user/index.ts @@ -1 +1,2 @@ +export * from './deleteUser'; export * from './getUser'; diff --git a/striker-ui-api/src/lib/rrouters.ts b/striker-ui-api/src/lib/rrouters.ts new file mode 100644 index 00000000..0ecaa262 --- /dev/null +++ b/striker-ui-api/src/lib/rrouters.ts @@ -0,0 +1,44 @@ +import { Application, Handler, Router } from 'express'; +import path from 'path'; + +import { stdout } from './shell'; + +export const rrouters = < + A extends Application, + M extends MapToRouter, + R extends Router, + H extends Handler, +>( + app: A, + union: Readonly | R, + { + assign = (router) => [router], + key, + route = '/', + }: { + assign?: (router: R) => Array; + key?: keyof M; + route?: string; + } = {}, +) => { + if ('route' in union) { + const handlers = assign(union as R); + const { length: hcount } = handlers; + + stdout(`Set up route ${route} with ${hcount} handler(s)`); + + app.use(route, ...handlers); + } else if (key) { + rrouters(app, union[key], { + assign, + route: path.posix.join(route, String(key)), + }); + } else { + Object.entries(union).forEach(([extend, subunion]) => { + rrouters(app, subunion, { + assign, + route: path.posix.join(route, extend), + }); + }); + } +}; diff --git a/striker-ui-api/src/lib/sanitize.ts b/striker-ui-api/src/lib/sanitize.ts index d76a87a7..24066d91 100644 --- a/striker-ui-api/src/lib/sanitize.ts +++ b/striker-ui-api/src/lib/sanitize.ts @@ -12,6 +12,7 @@ type MapToReturnFunction = { [ReturnTypeName in keyof MapToReturnType]: ( value: unknown, modifier: (unmodified: unknown) => string, + fallback?: MapToReturnType[ReturnTypeName], ) => MapToReturnType[ReturnTypeName]; }; @@ -29,10 +30,10 @@ const MAP_TO_MODIFIER_FUNCTION: MapToModifierFunction = { const MAP_TO_RETURN_FUNCTION: MapToReturnFunction = { boolean: (value) => value !== undefined, - number: (value) => parseFloat(String(value)) || 0, - string: (value, mod) => (value ? mod(value) : ''), - 'string[]': (value, mod) => { - let result: string[] = []; + number: (value, mod, fallback = 0) => parseFloat(String(value)) || fallback, + string: (value, mod, fallback = '') => (value ? mod(value) : fallback), + 'string[]': (value, mod, fallback = []) => { + let result: string[] = fallback; if (value instanceof Array) { result = value.reduce((reduceContainer, element) => { @@ -54,18 +55,24 @@ export const sanitize = ( value: unknown, returnType: ReturnTypeName, { + fallback, modifierType = 'none', modifier = MAP_TO_MODIFIER_FUNCTION[modifierType], }: { + fallback?: MapToReturnType[ReturnTypeName]; modifier?: ModifierFunction; modifierType?: keyof MapToModifierFunction; } = {}, ): MapToReturnType[ReturnTypeName] => - MAP_TO_RETURN_FUNCTION[returnType](value, (unmodified: unknown) => { - const input = String(unmodified); + MAP_TO_RETURN_FUNCTION[returnType]( + value, + (unmodified: unknown) => { + const input = String(unmodified); - return call(modifier, { - notCallableReturn: input, - parameters: [input], - }); - }) as MapToReturnType[ReturnTypeName]; + return call(modifier, { + notCallableReturn: input, + parameters: [input], + }); + }, + fallback, + ) as MapToReturnType[ReturnTypeName]; diff --git a/striker-ui-api/src/lib/shell.ts b/striker-ui-api/src/lib/shell.ts index dcd12f2e..ffff8a7c 100644 --- a/striker-ui-api/src/lib/shell.ts +++ b/striker-ui-api/src/lib/shell.ts @@ -32,15 +32,33 @@ const systemCall = ( export const date = (...args: string[]) => systemCall(SERVER_PATHS.usr.bin.date.self, args); +export const getent = (...args: string[]) => + systemCall(SERVER_PATHS.usr.bin.getent.self, args); + export const mkfifo = (...args: string[]) => systemCall(SERVER_PATHS.usr.bin.mkfifo.self, args); +export const openssl = (...args: string[]) => + systemCall(SERVER_PATHS.usr.bin.openssl.self, args); + export const rm = (...args: string[]) => systemCall(SERVER_PATHS.usr.bin.rm.self, args); +export const uuidgen = (...args: string[]) => + systemCall(SERVER_PATHS.usr.bin.uuidgen.self, args); + +export const resolveId = (id: number | string, database: string) => + Number.parseInt(getent(database, String(id)).split(':', 3)[2]); + +export const resolveGid = (id: number | string) => resolveId(id, 'group'); + +export const resolveUid = (id: number | string) => resolveId(id, 'passwd'); + export const stderr = (message: string) => print(message, { stream: 'stderr' }); export const stdout = (message: string) => print(message); -export const stdoutVar = (variable: { [name: string]: unknown }) => - print(`Variables: ${JSON.stringify(variable, null, 2)}`); +export const stdoutVar = (variable: unknown, label = 'Variables: ') => + print(`${label}${JSON.stringify(variable, null, 2)}`); + +export const uuid = () => uuidgen('--random').trim(); diff --git a/striker-ui-api/src/lib/traverse.ts b/striker-ui-api/src/lib/traverse.ts new file mode 100644 index 00000000..994fefb1 --- /dev/null +++ b/striker-ui-api/src/lib/traverse.ts @@ -0,0 +1,28 @@ +type NestedObject = { + [key: number | string]: NestedObject | T; +}; + +export const traverse = , V = unknown>( + obj: O, + init: T, + onKey: (previous: T, obj: O, key: string) => { is: boolean; next: O }, + { + onEnd, + previous = init, + }: { + onEnd?: (previous: T, obj: O, key: string) => void; + previous?: T; + } = {}, +) => { + Object.keys(obj).forEach((key: string) => { + const { is: proceed, next } = onKey(previous, obj, key); + + if (proceed) { + traverse(next, init, onKey, { previous }); + } else { + onEnd?.call(null, previous, obj, key); + } + }); + + return previous; +}; diff --git a/striker-ui-api/src/passport.ts b/striker-ui-api/src/passport.ts new file mode 100644 index 00000000..66dba517 --- /dev/null +++ b/striker-ui-api/src/passport.ts @@ -0,0 +1,127 @@ +import passport from 'passport'; +import { Strategy as LocalStrategy } from 'passport-local'; + +import { DELETED } from './lib/consts'; + +import { query, sub } from './lib/accessModule'; +import { sanitize } from './lib/sanitize'; +import { stdout } from './lib/shell'; + +passport.use( + 'login', + new LocalStrategy(async (username, password, done) => { + stdout(`Attempting passport local strategy "login" for user [${username}]`); + + let rows: [ + userUuid: string, + userName: string, + userPasswordHash: string, + userSalt: string, + userAlgorithm: string, + userHashCount: string, + ][]; + + try { + rows = await query( + `SELECT + user_uuid, + user_name, + user_password_hash, + user_salt, + user_algorithm, + user_hash_count + FROM users + WHERE user_algorithm != 'DELETED' + AND user_name = '${username}' + LIMIT 1;`, + ); + } catch (queryError) { + return done(queryError); + } + + if (!rows.length) { + return done(null, false); + } + + const { + 0: [userUuid, , userPasswordHash, userSalt, userAlgorithm, userHashCount], + } = rows; + + let encryptResult: { + user_password_hash: string; + user_salt: string; + user_hash_count: number; + user_algorithm: string; + }; + + try { + [encryptResult] = await sub('encrypt_password', { + params: [ + { + algorithm: userAlgorithm, + hash_count: userHashCount, + password, + salt: userSalt, + }, + ], + pre: ['Account'], + }); + } catch (subError) { + return done(subError); + } + + const { user_password_hash: inputPasswordHash } = encryptResult; + + if (inputPasswordHash !== userPasswordHash) { + return done(null, false); + } + + const user: Express.User = { + name: username, + uuid: userUuid, + }; + + return done(null, user); + }), +); + +passport.serializeUser((user, done) => { + const { name, uuid } = user; + + stdout(`Serialize user [${name}]`); + + return done(null, uuid); +}); + +passport.deserializeUser(async (id, done) => { + const uuid = sanitize(id, 'string', { modifierType: 'sql' }); + + stdout(`Deserialize user identified by ${uuid}`); + + let rows: [userName: string][]; + + try { + rows = await query( + `SELECT user_name + FROM users + WHERE user_algorithm != '${DELETED}' + AND user_uuid = '${uuid}';`, + ); + } catch (error) { + return done(error); + } + + if (!rows.length) { + return done(null, false); + } + + const { + 0: [userName], + } = rows; + + const user: Express.User = { name: userName, uuid }; + + return done(null, user); +}); + +export default passport; diff --git a/striker-ui-api/src/routes/auth.ts b/striker-ui-api/src/routes/auth.ts new file mode 100644 index 00000000..5ffb8491 --- /dev/null +++ b/striker-ui-api/src/routes/auth.ts @@ -0,0 +1,13 @@ +import express from 'express'; + +import { guardApi } from '../lib/assertAuthentication'; +import { login, logout } from '../lib/request_handlers/auth'; +import passport from '../passport'; + +const router = express.Router(); + +router + .post('/login', passport.authenticate('login'), login) + .put('/logout', guardApi, logout); + +export default router; diff --git a/striker-ui-api/src/routes/echo.ts b/striker-ui-api/src/routes/echo.ts index 929f9880..478dc946 100644 --- a/striker-ui-api/src/routes/echo.ts +++ b/striker-ui-api/src/routes/echo.ts @@ -1,5 +1,7 @@ import express from 'express'; +import { stdoutVar } from '../lib/shell'; + const router = express.Router(); router @@ -7,9 +9,11 @@ router response.status(200).send({ message: 'Empty echo.' }); }) .post('/', (request, response) => { - console.log('echo:post', JSON.stringify(request.body, undefined, 2)); + const { body = {} } = request; + + stdoutVar(body, 'echo:post\n'); - const message = request.body.message ?? 'No message.'; + const { message = 'No message.' } = body; response.status(200).send({ message }); }); diff --git a/striker-ui-api/src/routes/file.ts b/striker-ui-api/src/routes/file.ts index 1d5e1440..29e875ed 100644 --- a/striker-ui-api/src/routes/file.ts +++ b/striker-ui-api/src/routes/file.ts @@ -1,202 +1,21 @@ import express from 'express'; import { - dbJobAnvilSyncShared, - dbQuery, - dbSubRefreshTimestamp, - dbWrite, -} from '../lib/accessModule'; -import getFile from '../lib/request_handlers/file/getFile'; -import getFileDetail from '../lib/request_handlers/file/getFileDetail'; + createFile, + deleteFile, + getFile, + getFileDetail, + updateFile, +} from '../lib/request_handlers/file'; import uploadSharedFiles from '../middlewares/uploadSharedFiles'; const router = express.Router(); router - .delete('/:fileUUID', (request, response) => { - const { fileUUID } = request.params; - const FILE_TYPE_DELETED = 'DELETED'; - - const [[oldFileType]] = dbQuery( - `SELECT file_type FROM files WHERE file_uuid = '${fileUUID}';`, - ).stdout; - - if (oldFileType !== FILE_TYPE_DELETED) { - dbWrite( - `UPDATE files - SET - file_type = '${FILE_TYPE_DELETED}', - modified_date = '${dbSubRefreshTimestamp()}' - WHERE file_uuid = '${fileUUID}';`, - ).stdout; - - dbJobAnvilSyncShared('purge', `file_uuid=${fileUUID}`, '0136', '0137', { - jobHostUUID: 'all', - }); - } - - response.status(204).send(); - }) + .delete('/:fileUUID', deleteFile) .get('/', getFile) .get('/:fileUUID', getFileDetail) - .post('/', uploadSharedFiles.single('file'), ({ file, body }, response) => { - console.log('Receiving shared file.'); - - if (file) { - console.log(`file: ${JSON.stringify(file, null, 2)}`); - console.log(`body: ${JSON.stringify(body, null, 2)}`); - - dbJobAnvilSyncShared( - 'move_incoming', - `file=${file.path}`, - '0132', - '0133', - ); - - response.status(200).send(); - } - }) - .put('/:fileUUID', (request, response) => { - console.log('Begin edit single file.'); - console.dir(request.body); - - const { fileUUID } = request.params; - const { fileName, fileLocations, fileType } = request.body; - const anvilSyncSharedFunctions = []; - - let query = ''; - - if (fileName) { - const [[oldFileName]] = dbQuery( - `SELECT file_name FROM files WHERE file_uuid = '${fileUUID}';`, - ).stdout; - console.log(`oldFileName=[${oldFileName}],newFileName=[${fileName}]`); - - if (fileName !== oldFileName) { - query += ` - UPDATE files - SET - file_name = '${fileName}', - modified_date = '${dbSubRefreshTimestamp()}' - WHERE file_uuid = '${fileUUID}';`; - - anvilSyncSharedFunctions.push(() => - dbJobAnvilSyncShared( - 'rename', - `file_uuid=${fileUUID}\nold_name=${oldFileName}\nnew_name=${fileName}`, - '0138', - '0139', - { jobHostUUID: 'all' }, - ), - ); - } - } - - if (fileType) { - query += ` - UPDATE files - SET - file_type = '${fileType}', - modified_date = '${dbSubRefreshTimestamp()}' - WHERE file_uuid = '${fileUUID}';`; - - anvilSyncSharedFunctions.push(() => - dbJobAnvilSyncShared( - 'check_mode', - `file_uuid=${fileUUID}`, - '0143', - '0144', - { jobHostUUID: 'all' }, - ), - ); - } - - if (fileLocations) { - fileLocations.forEach( - ({ - fileLocationUUID, - isFileLocationActive, - }: { - fileLocationUUID: string; - isFileLocationActive: boolean; - }) => { - let fileLocationActive = 0; - let jobName = 'purge'; - let jobTitle = '0136'; - let jobDescription = '0137'; - - if (isFileLocationActive) { - fileLocationActive = 1; - jobName = 'pull_file'; - jobTitle = '0132'; - jobDescription = '0133'; - } - - query += ` - UPDATE file_locations - SET - file_location_active = '${fileLocationActive}', - modified_date = '${dbSubRefreshTimestamp()}' - WHERE file_location_uuid = '${fileLocationUUID}';`; - - const targetHosts = dbQuery( - `SELECT - anv.anvil_node1_host_uuid, - anv.anvil_node2_host_uuid, - anv.anvil_dr1_host_uuid - FROM anvils AS anv - JOIN file_locations AS fil_loc - ON anv.anvil_uuid = fil_loc.file_location_anvil_uuid - WHERE fil_loc.file_location_uuid = '${fileLocationUUID}';`, - ).stdout; - - targetHosts.flat().forEach((hostUUID: string) => { - if (hostUUID) { - anvilSyncSharedFunctions.push(() => - dbJobAnvilSyncShared( - jobName, - `file_uuid=${fileUUID}`, - jobTitle, - jobDescription, - { jobHostUUID: hostUUID }, - ), - ); - } - }); - }, - ); - } - - console.log(`Query (type=[${typeof query}]): [${query}]`); - - let queryStdout; - - try { - ({ stdout: queryStdout } = dbWrite(query)); - } catch (queryError) { - console.log(`Failed to execute query; CAUSE: ${queryError}`); - - response.status(500).send(); - } - - console.log( - `Query stdout (type=[${typeof queryStdout}]): ${JSON.stringify( - queryStdout, - null, - 2, - )}`, - ); - anvilSyncSharedFunctions.forEach((fn, index) => { - console.log( - `Anvil sync shared [${index}] output: [${JSON.stringify( - fn(), - null, - 2, - )}]`, - ); - }); - - response.status(200).send(queryStdout); - }); + .post('/', uploadSharedFiles.single('file'), createFile) + .put('/:fileUUID', updateFile); export default router; diff --git a/striker-ui-api/src/routes/index.ts b/striker-ui-api/src/routes/index.ts index ef703777..1fb9f91c 100644 --- a/striker-ui-api/src/routes/index.ts +++ b/striker-ui-api/src/routes/index.ts @@ -1,6 +1,5 @@ -import { Router } from 'express'; - import anvilRouter from './anvil'; +import authRouter from './auth'; import commandRouter from './command'; import echoRouter from './echo'; import fenceRouter from './fence'; @@ -11,23 +10,30 @@ import manifestRouter from './manifest'; import networkInterfaceRouter from './network-interface'; import serverRouter from './server'; import sshKeyRouter from './ssh-key'; +import staticRouter from './static'; import upsRouter from './ups'; import userRouter from './user'; -const routes: Readonly> = { - anvil: anvilRouter, - command: commandRouter, - echo: echoRouter, - fence: fenceRouter, - file: fileRouter, - host: hostRouter, - job: jobRouter, - manifest: manifestRouter, - 'network-interface': networkInterfaceRouter, - server: serverRouter, - 'ssh-key': sshKeyRouter, - ups: upsRouter, - user: userRouter, +const routes = { + private: { + anvil: anvilRouter, + command: commandRouter, + fence: fenceRouter, + file: fileRouter, + host: hostRouter, + job: jobRouter, + manifest: manifestRouter, + 'network-interface': networkInterfaceRouter, + server: serverRouter, + 'ssh-key': sshKeyRouter, + ups: upsRouter, + user: userRouter, + }, + public: { + auth: authRouter, + echo: echoRouter, + }, + static: staticRouter, }; export default routes; diff --git a/striker-ui-api/src/routes/manifest.ts b/striker-ui-api/src/routes/manifest.ts index 6d554b43..88550032 100644 --- a/striker-ui-api/src/routes/manifest.ts +++ b/striker-ui-api/src/routes/manifest.ts @@ -13,7 +13,7 @@ const router = express.Router(); router .delete('/', deleteManifest) - .delete('/manifestUuid', deleteManifest) + .delete('/:manifestUuid', deleteManifest) .get('/', getManifest) .get('/template', getManifestTemplate) .get('/:manifestUUID', getManifestDetail) diff --git a/striker-ui-api/src/routes/static.ts b/striker-ui-api/src/routes/static.ts new file mode 100644 index 00000000..414f8195 --- /dev/null +++ b/striker-ui-api/src/routes/static.ts @@ -0,0 +1,51 @@ +import express from 'express'; +import { existsSync } from 'fs'; +import path from 'path'; + +import { SERVER_PATHS } from '../lib/consts'; + +import { assertAuthentication } from '../lib/assertAuthentication'; +import { stdout } from '../lib/shell'; + +const router = express.Router(); + +const htmlDir = SERVER_PATHS.var.www.html.self; + +router.use( + (...args) => { + const { 0: request, 2: next } = args; + const { originalUrl } = request; + + if (/^[/]login/.test(originalUrl)) { + stdout(`Static:login requested`); + + return assertAuthentication({ fail: (rq, rs, nx) => nx(), succeed: '/' })( + ...args, + ); + } + + const parts = originalUrl.replace(/[/]$/, '').split('/'); + const tail = parts.pop() || 'index'; + const extended = /[.]html$/.test(tail) ? tail : `${tail}.html`; + + parts.push(extended); + + const htmlPath = path.posix.join(htmlDir, ...parts); + const isHtmlExists = existsSync(htmlPath); + + if (isHtmlExists) { + stdout(`Static:[${htmlPath}] requested`); + + return assertAuthentication({ fail: '/login', failReturnTo: true })( + ...args, + ); + } + + return next(); + }, + express.static(htmlDir, { + extensions: ['htm', 'html'], + }), +); + +export default router; diff --git a/striker-ui-api/src/routes/user.ts b/striker-ui-api/src/routes/user.ts index a3633efa..4cc94a06 100644 --- a/striker-ui-api/src/routes/user.ts +++ b/striker-ui-api/src/routes/user.ts @@ -1,9 +1,12 @@ import express from 'express'; -import { getUser } from '../lib/request_handlers/user'; +import { deleteUser, getUser } from '../lib/request_handlers/user'; const router = express.Router(); -router.get('/', getUser); +router + .get('/', getUser) + .delete('/', deleteUser) + .delete('/:userUuid', deleteUser); export default router; diff --git a/striker-ui-api/src/session.ts b/striker-ui-api/src/session.ts new file mode 100644 index 00000000..77fd635e --- /dev/null +++ b/striker-ui-api/src/session.ts @@ -0,0 +1,201 @@ +import assert from 'assert'; +import expressSession, { + SessionData, + Store as BaseSessionStore, +} from 'express-session'; + +import { DELETED } from './lib/consts'; + +import { getLocalHostUUID, query, timestamp, write } from './lib/accessModule'; +import { getSessionSecret } from './lib/getSessionSecret'; +import { stderr, stdout, stdoutVar, uuid } from './lib/shell'; + +const DEFAULT_COOKIE_ORIGINAL_MAX_AGE = 28800000; // 8 hours + +export class SessionStore extends BaseSessionStore { + constructor(options = {}) { + super(options); + } + + public async destroy( + sid: string, + done?: ((err?: unknown) => void) | undefined, + ): Promise { + stdout(`Destroy session ${sid}`); + + try { + const wcode = await write( + `UPDATE sessions + SET session_salt = '${DELETED}', modified_date = '${timestamp()}' + WHERE session_uuid = '${sid}';`, + ); + + assert(wcode === 0, `Write exited with code ${wcode}`); + } catch (error) { + stderr( + `Failed to complete DB write in destroy session ${sid}; CAUSE: ${error}`, + ); + + return done?.call(null, error); + } + + return done?.call(null); + } + + public async get( + sid: string, + done: (err: unknown, session?: SessionData | null | undefined) => void, + ): Promise { + stdout(`Get session ${sid}`); + + let rows: [ + sessionUuid: string, + userUuid: string, + sessionModifiedDate: string, + ][]; + + try { + rows = await query( + `SELECT + s.session_uuid, + u.user_uuid, + s.modified_date + FROM sessions AS s + JOIN users AS u + ON s.session_user_uuid = u.user_uuid + WHERE s.session_salt != '${DELETED}' + AND s.session_uuid = '${sid}';`, + ); + } catch (queryError) { + return done(queryError); + } + + if (!rows.length) { + return done(null); + } + + const { + 0: [, userUuid, sessionModifiedDate], + } = rows; + + const cookieMaxAge = + SessionStore.calculateCookieMaxAge(sessionModifiedDate); + + const data: SessionData = { + cookie: { + maxAge: cookieMaxAge, + originalMaxAge: DEFAULT_COOKIE_ORIGINAL_MAX_AGE, + }, + passport: { user: userUuid }, + }; + + return done(null, data); + } + + public async set( + sid: string, + session: SessionData, + done?: ((err?: unknown) => void) | undefined, + ): Promise { + stdoutVar({ session }, `Set session ${sid}`); + + const { passport: { user: userUuid } = {} } = session; + + try { + assert.ok(userUuid, 'Missing user identifier'); + + const localHostUuid = getLocalHostUUID(); + const modifiedDate = timestamp(); + + const wcode = await write( + `INSERT INTO + sessions ( + session_uuid, + session_host_uuid, + session_user_uuid, + session_salt, + modified_date + ) + VALUES + ( + '${sid}', + '${localHostUuid}', + '${userUuid}', + '', + '${modifiedDate}' + ) + ON CONFLICT (session_uuid) + DO UPDATE SET modified_date = '${modifiedDate}';`, + ); + + assert(wcode === 0, `Write exited with code ${wcode}`); + } catch (error) { + stderr( + `Failed to complete DB write in set session ${sid}; CAUSE: ${error}`, + ); + + return done?.call(null, error); + } + + return done?.call(null); + } + + public async touch( + sid: string, + session: SessionData, + done?: ((err?: unknown) => void) | undefined, + ): Promise { + stdoutVar({ session }, `Touch session ${sid}`); + + try { + const wcode = await write( + `UPDATE sessions + SET modified_date = '${timestamp()}' + WHERE session_uuid = '${sid}';`, + ); + + assert(wcode === 0, `Write exited with code ${wcode}`); + } catch (error) { + stderr( + `Failed to complete DB write in touch session ${sid}; CAUSE: ${error}`, + ); + + return done?.call(null, error); + } + + return done?.call(null); + } + + public static calculateCookieMaxAge( + sessionModifiedDate: string, + cookieOriginalMaxAge: number = DEFAULT_COOKIE_ORIGINAL_MAX_AGE, + ) { + const sessionModifiedEpoch = Date.parse(sessionModifiedDate); + const sessionDeadlineEpoch = sessionModifiedEpoch + cookieOriginalMaxAge; + const cookieMaxAge = sessionDeadlineEpoch - Date.now(); + + stdoutVar({ sessionModifiedDate, sessionDeadlineEpoch, cookieMaxAge }); + + return cookieMaxAge; + } +} + +export default (async () => + expressSession({ + cookie: { + httpOnly: true, + maxAge: DEFAULT_COOKIE_ORIGINAL_MAX_AGE, + secure: false, + }, + genid: ({ path }) => { + const sid = uuid(); + + stdout(`Generated session identifier ${sid}; request.path=${path}`); + + return sid; + }, + resave: false, + saveUninitialized: false, + secret: await getSessionSecret(), + store: new SessionStore(), + }))(); diff --git a/striker-ui-api/src/types/AccessModule.d.ts b/striker-ui-api/src/types/AccessModule.d.ts new file mode 100644 index 00000000..2825647d --- /dev/null +++ b/striker-ui-api/src/types/AccessModule.d.ts @@ -0,0 +1,12 @@ +type AccessStartOptions = { + args?: readonly string[]; +} & import('child_process').SpawnOptions; + +type SubroutineCommonParams = { + debug?: number; +}; + +type InsertOrUpdateFunctionCommonParams = SubroutineCommonParams & { + file: string; + line?: number; +}; diff --git a/striker-ui-api/src/types/AnvilOverview.d.ts b/striker-ui-api/src/types/AnvilOverview.d.ts deleted file mode 100644 index 2b37b418..00000000 --- a/striker-ui-api/src/types/AnvilOverview.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -type AnvilOverview = { - anvilName: string; - anvilUUID: string; - hosts: Array<{ - hostName: string; - hostUUID: string; - }>; -}; diff --git a/striker-ui-api/src/types/AnvilSyncSharedFunction.d.ts b/striker-ui-api/src/types/AnvilSyncSharedFunction.d.ts new file mode 100644 index 00000000..12bd12d5 --- /dev/null +++ b/striker-ui-api/src/types/AnvilSyncSharedFunction.d.ts @@ -0,0 +1,3 @@ +type JobAnvilSyncSharedOptions = { + jobHostUUID?: string; +}; diff --git a/striker-ui-api/src/types/AnvilDetail.d.ts b/striker-ui-api/src/types/ApiAn.d.ts similarity index 85% rename from striker-ui-api/src/types/AnvilDetail.d.ts rename to striker-ui-api/src/types/ApiAn.d.ts index 55261544..d49b8d61 100644 --- a/striker-ui-api/src/types/AnvilDetail.d.ts +++ b/striker-ui-api/src/types/ApiAn.d.ts @@ -31,3 +31,12 @@ type AnvilDetailForProvisionServer = { fileName: string; }>; }; + +type AnvilOverview = { + anvilName: string; + anvilUUID: string; + hosts: Array<{ + hostName: string; + hostUUID: string; + }>; +}; diff --git a/striker-ui-api/src/types/ApiAuth.d.ts b/striker-ui-api/src/types/ApiAuth.d.ts new file mode 100644 index 00000000..c85487c8 --- /dev/null +++ b/striker-ui-api/src/types/ApiAuth.d.ts @@ -0,0 +1,4 @@ +type AuthLoginRequestBody = { + username: string; + password: string; +}; diff --git a/striker-ui-api/src/types/ApiCommand.d.ts b/striker-ui-api/src/types/ApiCommand.d.ts new file mode 100644 index 00000000..d3d03adf --- /dev/null +++ b/striker-ui-api/src/types/ApiCommand.d.ts @@ -0,0 +1,15 @@ +type GetHostSshRequestBody = { + password: string; + port?: number; + ipAddress: string; +}; + +type GetHostSshResponseBody = { + badSSHKeys?: DeleteSshKeyConflictRequestBody; + hostName: string; + hostOS: string; + hostUUID: string; + isConnected: boolean; + isInetConnected: boolean; + isOSRegistered: boolean; +}; diff --git a/striker-ui-api/src/types/APIFence.d.ts b/striker-ui-api/src/types/ApiFence.d.ts similarity index 100% rename from striker-ui-api/src/types/APIFence.d.ts rename to striker-ui-api/src/types/ApiFence.d.ts diff --git a/striker-ui-api/src/types/ApiHost.d.ts b/striker-ui-api/src/types/ApiHost.d.ts new file mode 100644 index 00000000..e9127590 --- /dev/null +++ b/striker-ui-api/src/types/ApiHost.d.ts @@ -0,0 +1,89 @@ +type CreateHostConnectionRequestBody = { + dbName?: string; + ipAddress: string; + isPing?: boolean; + /** Host password; same as database password */ + password: string; + port?: number; + sshPort?: number; + /** Database user */ + user?: string; +}; + +type DeleteHostConnectionRequestBody = { + [hostUUID: string]: string[]; +}; + +type HostConnectionOverview = { + inbound: { + ipAddress: { + [ipAddress: string]: { + hostUUID: string; + ipAddress: string; + ipAddressUUID: string; + networkLinkNumber: number; + networkNumber: number; + networkType: string; + }; + }; + port: number; + user: string; + }; + peer: { + [ipAddress: string]: { + hostUUID: string; + ipAddress: string; + isPing: boolean; + port: number; + user: string; + }; + }; +}; + +type HostOverview = { + hostName: string; + hostType: string; + hostUUID: string; + shortHostName: string; +}; + +type InitializeStrikerNetworkForm = { + interfaces: Array; + ipAddress: string; + name: string; + subnetMask: string; + type: string; +}; + +type InitializeStrikerForm = { + adminPassword: string; + domainName: string; + hostName: string; + hostNumber: number; + networkDNS: string; + networkGateway: string; + networks: InitializeStrikerNetworkForm[]; + organizationName: string; + organizationPrefix: string; +}; + +type PrepareHostRequestBody = { + enterpriseUUID?: string; + hostIPAddress: string; + hostName: string; + hostPassword: string; + hostSSHPort?: number; + hostType: string; + hostUser?: string; + hostUUID?: string; + redhatPassword: string; + redhatUser: string; +}; + +type SetHostInstallTargetRequestBody = { + isEnableInstallTarget: boolean; +}; + +type UpdateHostParams = { + hostUUID: string; +}; diff --git a/striker-ui-api/src/types/APIManifest.d.ts b/striker-ui-api/src/types/ApiManifest.d.ts similarity index 100% rename from striker-ui-api/src/types/APIManifest.d.ts rename to striker-ui-api/src/types/ApiManifest.d.ts diff --git a/striker-ui-api/src/types/NetworkInterfaceOverview.d.ts b/striker-ui-api/src/types/ApiNetworkInterface.d.ts similarity index 100% rename from striker-ui-api/src/types/NetworkInterfaceOverview.d.ts rename to striker-ui-api/src/types/ApiNetworkInterface.d.ts diff --git a/striker-ui-api/src/types/ServerOverview.d.ts b/striker-ui-api/src/types/ApiServer.d.ts similarity index 100% rename from striker-ui-api/src/types/ServerOverview.d.ts rename to striker-ui-api/src/types/ApiServer.d.ts diff --git a/striker-ui-api/src/types/ApiSshKey.d.ts b/striker-ui-api/src/types/ApiSshKey.d.ts new file mode 100644 index 00000000..956cf6e1 --- /dev/null +++ b/striker-ui-api/src/types/ApiSshKey.d.ts @@ -0,0 +1,12 @@ +type SshKeyConflict = { + [stateUUID: string]: { + badFile: string; + badLine: number; + hostName: string; + hostUUID: string; + ipAddress: string; + stateUUID: string; + }; +}; + +type DeleteSshKeyConflictRequestBody = { [hostUUID: string]: string[] }; diff --git a/striker-ui-api/src/types/APIUPS.d.ts b/striker-ui-api/src/types/ApiUps.d.ts similarity index 85% rename from striker-ui-api/src/types/APIUPS.d.ts rename to striker-ui-api/src/types/ApiUps.d.ts index 5199e348..c8ded148 100644 --- a/striker-ui-api/src/types/APIUPS.d.ts +++ b/striker-ui-api/src/types/ApiUps.d.ts @@ -1,11 +1,11 @@ -type UPSOverview = { +type UpsOverview = { upsAgent: string; upsIPAddress: string; upsName: string; upsUUID: string; }; -type UPSTemplate = { +type UpsTemplate = { [upsName: string]: AnvilDataUPSHash[string] & { links: { [linkId: string]: { diff --git a/striker-ui-api/src/types/ApiUser.d.ts b/striker-ui-api/src/types/ApiUser.d.ts new file mode 100644 index 00000000..213a6871 --- /dev/null +++ b/striker-ui-api/src/types/ApiUser.d.ts @@ -0,0 +1,7 @@ +type DeleteUserParamsDictionary = { + userUuid: string; +}; + +type DeleteUserRequestBody = { + uuids?: string[]; +}; diff --git a/striker-ui-api/src/types/BuildGetRequestHandlerOptions.d.ts b/striker-ui-api/src/types/BuildGetRequestHandlerFunction.d.ts similarity index 100% rename from striker-ui-api/src/types/BuildGetRequestHandlerOptions.d.ts rename to striker-ui-api/src/types/BuildGetRequestHandlerFunction.d.ts diff --git a/striker-ui-api/src/types/BuildQueryFunction.d.ts b/striker-ui-api/src/types/BuildQueryFunction.d.ts index 78077027..ead3207d 100644 --- a/striker-ui-api/src/types/BuildQueryFunction.d.ts +++ b/striker-ui-api/src/types/BuildQueryFunction.d.ts @@ -7,4 +7,4 @@ type BuildQueryOptions = { type BuildQueryFunction = ( request: import('express').Request, options?: BuildQueryOptions, -) => string; +) => string | Promise; diff --git a/striker-ui-api/src/types/CallOptions.d.ts b/striker-ui-api/src/types/CallFunction.d.ts similarity index 100% rename from striker-ui-api/src/types/CallOptions.d.ts rename to striker-ui-api/src/types/CallFunction.d.ts diff --git a/striker-ui-api/src/types/CreateHostConnectionRequestBody.d.ts b/striker-ui-api/src/types/CreateHostConnectionRequestBody.d.ts deleted file mode 100644 index ce50d3a2..00000000 --- a/striker-ui-api/src/types/CreateHostConnectionRequestBody.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -type CreateHostConnectionRequestBody = { - dbName?: string; - ipAddress: string; - isPing?: boolean; - // Host password; same as database password. - password: string; - port?: number; - sshPort?: number; - // database user. - user?: string; -}; diff --git a/striker-ui-api/src/types/DBInsertOrUpdateFunctionCommon.d.ts b/striker-ui-api/src/types/DBInsertOrUpdateFunctionCommon.d.ts deleted file mode 100644 index 12be0674..00000000 --- a/striker-ui-api/src/types/DBInsertOrUpdateFunctionCommon.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -type DBInsertOrUpdateFunctionCommonParams = ModuleSubroutineCommonParams & { - file: string; - line?: number; -}; - -type DBInsertOrUpdateFunctionCommonOptions = Omit< - ExecModuleSubroutineOptions, - 'subParams' | 'subModuleName' ->; diff --git a/striker-ui-api/src/types/DBJobAnvilSyncSharedOptions.d.ts b/striker-ui-api/src/types/DBJobAnvilSyncSharedOptions.d.ts deleted file mode 100644 index e0251591..00000000 --- a/striker-ui-api/src/types/DBJobAnvilSyncSharedOptions.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -type DBJobAnvilSyncSharedOptions = { - jobHostUUID?: string; -}; diff --git a/striker-ui-api/src/types/DeleteHostConnectionRequestBody.d.ts b/striker-ui-api/src/types/DeleteHostConnectionRequestBody.d.ts deleted file mode 100644 index cea743b6..00000000 --- a/striker-ui-api/src/types/DeleteHostConnectionRequestBody.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -type DeleteHostConnectionRequestBody = { - [hostUUID: string]: string[]; -}; diff --git a/striker-ui-api/src/types/DeleteSSHKeyConflictRequestBody.d.ts b/striker-ui-api/src/types/DeleteSSHKeyConflictRequestBody.d.ts deleted file mode 100644 index f2451843..00000000 --- a/striker-ui-api/src/types/DeleteSSHKeyConflictRequestBody.d.ts +++ /dev/null @@ -1 +0,0 @@ -type DeleteSSHKeyConflictRequestBody = { [hostUUID: string]: string[] }; diff --git a/striker-ui-api/src/types/ExecModuleSubroutineFunction.d.ts b/striker-ui-api/src/types/ExecModuleSubroutineFunction.d.ts deleted file mode 100644 index c151524e..00000000 --- a/striker-ui-api/src/types/ExecModuleSubroutineFunction.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -type ExecModuleSubroutineOptions = { - spawnSyncOptions?: import('child_process').SpawnSyncOptions; - subModuleName?: string; - subParams?: Record; -}; diff --git a/striker-ui-api/src/types/GetAnvilDataFunction.d.ts b/striker-ui-api/src/types/GetAnvilDataFunction.d.ts index 0d8c2a0c..f4e4e775 100644 --- a/striker-ui-api/src/types/GetAnvilDataFunction.d.ts +++ b/striker-ui-api/src/types/GetAnvilDataFunction.d.ts @@ -111,6 +111,6 @@ type AnvilDataUPSHash = { }; }; -type GetAnvilDataOptions = import('child_process').SpawnSyncOptions & { +type GetAnvilDataOptions = { predata?: Array<[string, ...unknown[]]>; }; diff --git a/striker-ui-api/src/types/GetPeerDataFunction.d.ts b/striker-ui-api/src/types/GetPeerDataFunction.d.ts index 17258540..76c16d6e 100644 --- a/striker-ui-api/src/types/GetPeerDataFunction.d.ts +++ b/striker-ui-api/src/types/GetPeerDataFunction.d.ts @@ -6,23 +6,19 @@ type PeerDataHash = { os_registered: string; }; -type GetPeerDataOptions = Omit< - ExecModuleSubroutineOptions, - 'subModuleName' | 'subParams' -> & - ModuleSubroutineCommonParams & { - password?: string; - port?: number; - }; +type GetPeerDataOptions = SubroutineCommonParams & { + password?: string; + port?: number; +}; type GetPeerDataFunction = ( target: string, options?: GetPeerDataOptions, -) => { +) => Promise<{ hostName: string; hostOS: string; hostUUID: string; isConnected: boolean; isInetConnected: boolean; isOSRegistered: boolean; -}; +}>; diff --git a/striker-ui-api/src/types/HostConnectionOverview.d.ts b/striker-ui-api/src/types/HostConnectionOverview.d.ts deleted file mode 100644 index 71c405bd..00000000 --- a/striker-ui-api/src/types/HostConnectionOverview.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -type HostConnectionOverview = { - inbound: { - ipAddress: { - [ipAddress: string]: { - hostUUID: string; - ipAddress: string; - ipAddressUUID: string; - networkLinkNumber: number; - networkNumber: number; - networkType: string; - }; - }; - port: number; - user: string; - }; - peer: { - [ipAddress: string]: { - hostUUID: string; - ipAddress: string; - isPing: boolean; - port: number; - user: string; - }; - }; -}; diff --git a/striker-ui-api/src/types/HostOverview.d.ts b/striker-ui-api/src/types/HostOverview.d.ts deleted file mode 100644 index 8e991dd3..00000000 --- a/striker-ui-api/src/types/HostOverview.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -type HostOverview = { - hostName: string; - hostType: string; - hostUUID: string; - shortHostName: string; -}; diff --git a/striker-ui-api/src/types/InitializeStrikerForm.d.ts b/striker-ui-api/src/types/InitializeStrikerForm.d.ts deleted file mode 100644 index 64a46c7b..00000000 --- a/striker-ui-api/src/types/InitializeStrikerForm.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -type InitializeStrikerNetworkForm = { - interfaces: Array; - ipAddress: string; - name: string; - subnetMask: string; - type: string; -}; - -type InitializeStrikerForm = { - adminPassword: string; - domainName: string; - hostName: string; - hostNumber: number; - networkDNS: string; - networkGateway: string; - networks: InitializeStrikerNetworkForm[]; - organizationName: string; - organizationPrefix: string; -}; diff --git a/striker-ui-api/src/types/DBInsertOrUpdateJobFunction.d.ts b/striker-ui-api/src/types/InsertOrUpdateJobFunction.d.ts similarity index 55% rename from striker-ui-api/src/types/DBInsertOrUpdateJobFunction.d.ts rename to striker-ui-api/src/types/InsertOrUpdateJobFunction.d.ts index 8fff8766..580a7a5a 100644 --- a/striker-ui-api/src/types/DBInsertOrUpdateJobFunction.d.ts +++ b/striker-ui-api/src/types/InsertOrUpdateJobFunction.d.ts @@ -1,4 +1,4 @@ -type DBJobParams = DBInsertOrUpdateFunctionCommonParams & { +type JobParams = InsertOrUpdateFunctionCommonParams & { job_command: string; job_data?: string; job_name: string; @@ -7,5 +7,3 @@ type DBJobParams = DBInsertOrUpdateFunctionCommonParams & { job_host_uuid?: string; job_progress?: number; }; - -type DBInsertOrUpdateJobOptions = DBInsertOrUpdateFunctionCommonOptions; diff --git a/striker-ui-api/src/types/DBInsertOrUpdateVariableFunction.d.ts b/striker-ui-api/src/types/InsertOrUpdateVariableFunction.d.ts similarity index 50% rename from striker-ui-api/src/types/DBInsertOrUpdateVariableFunction.d.ts rename to striker-ui-api/src/types/InsertOrUpdateVariableFunction.d.ts index 07310ed9..83e5c59c 100644 --- a/striker-ui-api/src/types/DBInsertOrUpdateVariableFunction.d.ts +++ b/striker-ui-api/src/types/InsertOrUpdateVariableFunction.d.ts @@ -1,4 +1,4 @@ -type DBVariableParams = DBInsertOrUpdateFunctionCommonParams & { +type VariableParams = InsertOrUpdateFunctionCommonParams & { update_value_only?: 0 | 1; variable_default?: string; varaible_description?: string; @@ -10,9 +10,6 @@ type DBVariableParams = DBInsertOrUpdateFunctionCommonParams & { variable_value?: number | string; }; -type DBInsertOrUpdateVariableOptions = DBInsertOrUpdateFunctionCommonOptions; - -type DBInsertOrUpdateVariableFunction = ( - subParams: DBVariableParams, - options?: DBInsertOrUpdateVariableOptions, -) => string; +type InsertOrUpdateVariableFunction = ( + params: VariableParams, +) => Promise; diff --git a/striker-ui-api/src/types/MapToRouter.d.ts b/striker-ui-api/src/types/MapToRouter.d.ts new file mode 100644 index 00000000..5380d644 --- /dev/null +++ b/striker-ui-api/src/types/MapToRouter.d.ts @@ -0,0 +1,3 @@ +type MapToRouter = { + [uri: string]: MapToRouter | R; +}; diff --git a/striker-ui-api/src/types/ModuleSubroutineCommonParams.d.ts b/striker-ui-api/src/types/ModuleSubroutineCommonParams.d.ts deleted file mode 100644 index 368b28f5..00000000 --- a/striker-ui-api/src/types/ModuleSubroutineCommonParams.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -type ModuleSubroutineCommonParams = { - debug?: number; -}; diff --git a/striker-ui-api/src/types/SessionData.d.ts b/striker-ui-api/src/types/SessionData.d.ts new file mode 100644 index 00000000..e186fad1 --- /dev/null +++ b/striker-ui-api/src/types/SessionData.d.ts @@ -0,0 +1,12 @@ +declare module 'express-session' { + /** + * Extended with passport property. + */ + interface SessionData { + passport: { user: string }; + returnTo?: string; + } +} + +// Required to avoid overwritting the original express-session module. +export {}; diff --git a/striker-ui-api/src/types/UpdateHostRequestBody.d.ts b/striker-ui-api/src/types/UpdateHostRequestBody.d.ts deleted file mode 100644 index a518e2d8..00000000 --- a/striker-ui-api/src/types/UpdateHostRequestBody.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -type SetHostInstallTargetRequestBody = { - isEnableInstallTarget: boolean; -}; - -type PrepareHostRequestBody = { - enterpriseUUID?: string; - hostIPAddress: string; - hostName: string; - hostPassword: string; - hostSSHPort?: number; - hostType: string; - hostUser?: string; - hostUUID?: string; - redhatPassword: string; - redhatUser: string; -}; - -type UpdateHostParams = { - hostUUID: string; -}; diff --git a/striker-ui-api/src/types/User.d.ts b/striker-ui-api/src/types/User.d.ts new file mode 100644 index 00000000..871374ef --- /dev/null +++ b/striker-ui-api/src/types/User.d.ts @@ -0,0 +1,14 @@ +declare global { + namespace Express { + /** + * Extended Express.User object used by express-session and passport to + * identify which user owns a session. + */ + interface User { + name: string; + uuid: string; + } + } +} + +export {}; diff --git a/striker-ui-api/webpack.config.js b/striker-ui-api/webpack.config.js index 8f5261ab..f866c99e 100644 --- a/striker-ui-api/webpack.config.js +++ b/striker-ui-api/webpack.config.js @@ -11,7 +11,10 @@ module.exports = { use: { loader: 'babel-loader', options: { - presets: ['@babel/preset-env', '@babel/preset-typescript'], + presets: [ + ['@babel/preset-env', { corejs: 3, useBuiltIns: 'usage' }], + '@babel/preset-typescript', + ], }, }, }, diff --git a/striker-ui/components/GateForm.tsx b/striker-ui/components/GateForm.tsx index b72de4cf..b7c81841 100644 --- a/striker-ui/components/GateForm.tsx +++ b/striker-ui/components/GateForm.tsx @@ -1,12 +1,5 @@ -import { SxProps, Theme } from '@mui/material'; -import { - forwardRef, - useCallback, - useImperativeHandle, - useMemo, - useRef, - useState, -} from 'react'; +import { Box, BoxProps, SxProps, Theme } from '@mui/material'; +import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react'; import INPUT_TYPES from '../lib/consts/INPUT_TYPES'; @@ -17,42 +10,42 @@ import InputWithRef, { InputForwardedRefContent } from './InputWithRef'; import MessageGroup, { MessageGroupForwardedRefContent } from './MessageGroup'; import OutlinedInputWithLabel from './OutlinedInputWithLabel'; import Spinner from './Spinner'; -import { - buildPeacefulStringTestBatch, - createTestInputFunction, -} from '../lib/test_input'; +import { buildPeacefulStringTestBatch } from '../lib/test_input'; +import useFormUtils from '../hooks/useFormUtils'; +import useProtectedState from '../hooks/useProtectedState'; const INPUT_ROOT_SX: SxProps = { width: '100%' }; -const IT_IDS = { - identifier: 'identifier', - passphrase: 'passphrase', -}; -const MESSAGE_KEY: GateFormMessageKey = { - accessError: 'accessError', - identifierInputError: 'identifierInputError', - passphraseInputError: 'passphraseInputError', -}; + +const INPUT_ID_PREFIX_GATE = 'gate-input'; + +const INPUT_ID_GATE_ID = `${INPUT_ID_PREFIX_GATE}-credential-id`; +const INPUT_ID_GATE_PASSPHRASE = `${INPUT_ID_PREFIX_GATE}-credential-passphrase`; + +const MSG_ID_GATE_ACCESS = 'access'; const GateForm = forwardRef( ( { - allowSubmit: isAllowSubmit = true, + formContainer: isFormContainer = true, gridProps: { columns: gridColumns = { xs: 1, sm: 2 }, layout, spacing: gridSpacing = '1em', ...restGridProps } = {}, + identifierId = INPUT_ID_GATE_ID, + identifierInputTestBatchBuilder: + buildIdentifierInputTestBatch = buildPeacefulStringTestBatch, identifierLabel, identifierOutlinedInputWithLabelProps: { formControlProps: identifierFormControlProps = {}, inputProps: identifierInputProps, ...restIdentifierOutlinedInputWithLabelProps } = {}, - identifierInputTestBatchBuilder: overwriteIdentifierInputTestBatch, onIdentifierBlurAppend, onSubmit, onSubmitAppend, + passphraseId = INPUT_ID_GATE_PASSPHRASE, passphraseLabel, passphraseOutlinedInputWithLabelProps: { formControlProps: passphraseFormControlProps = {}, @@ -60,6 +53,8 @@ const GateForm = forwardRef( ...restPassphraseOutlinedInputWithLabelProps } = {}, submitLabel, + // Props that depend on others. + allowSubmit: isAllowSubmit = isFormContainer, }, ref, ) => { @@ -72,151 +67,93 @@ const GateForm = forwardRef( const inputPassphraseRef = useRef>({}); const messageGroupRef = useRef({}); - const [isInputIdentifierValid, setIsInputIdentifierValid] = - useState(false); - const [isInputPassphraseValid, setIsInputPassphraseValid] = - useState(false); - const [isSubmitting, setIsSubmitting] = useState(false); + const [isSubmitting, setIsSubmitting] = useProtectedState(false); - const setAccessErrorMessage: GateFormMessageSetter = useCallback( - (message?) => { - messageGroupRef.current.setMessage?.call( - null, - MESSAGE_KEY.accessError, - message, - ); - }, - [], - ); - const setIdentifierInputErrorMessage: GateFormMessageSetter = useCallback( - (message?) => { - messageGroupRef.current.setMessage?.call( - null, - MESSAGE_KEY.identifierInputError, - message, - ); - }, - [], - ); - const setPassphraseInputErrorMessage: GateFormMessageSetter = useCallback( - (message?) => { - messageGroupRef.current.setMessage?.call( - null, - MESSAGE_KEY.passphraseInputError, - message, - ); - }, - [], + const formUtils = useFormUtils( + [INPUT_ID_GATE_ID, INPUT_ID_GATE_PASSPHRASE], + messageGroupRef, ); + const { + buildFinishInputTestBatchFunction, + buildInputFirstRenderFunction, + buildInputUnmountFunction, + isFormInvalid, + setMessage, + } = formUtils; - const messagesGroupSxDisplay = useMemo( - () => (isAllowSubmit ? undefined : 'none'), - [isAllowSubmit], - ); - const identifierInputTestBatch = useMemo( - () => - overwriteIdentifierInputTestBatch?.call( - null, - setIdentifierInputErrorMessage, - inputIdentifierRef.current, - ) ?? - buildPeacefulStringTestBatch( - identifierLabel, - () => { - setIdentifierInputErrorMessage(); - }, - { getValue: inputIdentifierRef.current.getValue }, - (message) => { - setIdentifierInputErrorMessage({ - children: message, - type: 'warning', - }); - }, - ), - [ - identifierLabel, - overwriteIdentifierInputTestBatch, - setIdentifierInputErrorMessage, - ], - ); - const inputTests: InputTestBatches = useMemo( - () => ({ - [IT_IDS.identifier]: identifierInputTestBatch, - [IT_IDS.passphrase]: buildPeacefulStringTestBatch( - passphraseLabel, - () => { - setPassphraseInputErrorMessage(); - }, - { getValue: inputPassphraseRef.current.getValue }, - (message) => { - setPassphraseInputErrorMessage({ - children: message, - type: 'warning', - }); - }, - ), - }), - [ - identifierInputTestBatch, - passphraseLabel, - setPassphraseInputErrorMessage, - ], - ); - const submitHandler: ContainedButtonProps['onClick'] = useMemo( + const submitHandler: DivFormEventHandler = useMemo( () => onSubmit ?? ((...args) => { - setAccessErrorMessage(); + const { 0: event } = args; + + event.preventDefault(); + + setMessage(MSG_ID_GATE_ACCESS); setIsSubmitting(true); + + const { target } = event; + const { elements } = target as HTMLFormElement; + + const { value: identifierValue } = elements.namedItem( + INPUT_ID_GATE_ID, + ) as HTMLInputElement; + const { value: passphraseValue } = elements.namedItem( + INPUT_ID_GATE_PASSPHRASE, + ) as HTMLInputElement; + onSubmitAppend?.call( null, - inputIdentifierRef.current, - inputPassphraseRef.current, - setAccessErrorMessage, + identifierValue, + passphraseValue, + (message?) => { + setMessage(MSG_ID_GATE_ACCESS, message); + }, setIsSubmitting, - messageGroupRef.current, ...args, ); }), - [onSubmit, onSubmitAppend, setAccessErrorMessage], + [onSubmit, onSubmitAppend, setIsSubmitting, setMessage], ); + const submitElement = useMemo( () => isSubmitting ? ( ) : ( - + {submitLabel} ), - [ - isInputIdentifierValid, - isInputPassphraseValid, - isSubmitting, - submitHandler, - submitLabel, - ], - ); - const submitGrid = useMemo( - () => - isAllowSubmit - ? { - children: submitElement, - sm: 2, - } - : undefined, - [isAllowSubmit, submitElement], + [isFormInvalid, isSubmitting, submitLabel], ); - const testInput = useMemo( - () => createTestInputFunction(inputTests), - [inputTests], - ); + const submitAreaGridLayout = useMemo(() => { + const result: GridLayout = {}; + + if (isAllowSubmit) { + result['gate-cell-message-group'] = { + children: , + sm: 2, + }; + result['gate-cell-submit'] = { children: submitElement, sm: 2 }; + } + + return result; + }, [isAllowSubmit, submitElement]); + + const containerProps = useMemo(() => { + const result: BoxProps = {}; + + if (isFormContainer) { + result.component = 'form'; + result.onSubmit = submitHandler; + } + + return result; + }, [isFormContainer, submitHandler]); useImperativeHandle(ref, () => ({ get: () => ({ @@ -232,90 +169,98 @@ const GateForm = forwardRef( })); return ( - { - const { - target: { value }, - } = event; - - const valid = testInput({ - inputs: { [IT_IDS.identifier]: { value } }, - }); - setIsInputIdentifierValid(valid); - - onIdentifierBlurAppend?.call(null, event); - }, - onFocus: () => { - setIdentifierInputErrorMessage(); - }, - ...identifierInputProps, - }} - label={identifierLabel} - {...restIdentifierOutlinedInputWithLabelProps} - /> - } - ref={inputIdentifierRef} - /> - ), - }, - 'credential-passphrase': { - children: ( - { - const valid = testInput({ - inputs: { [IT_IDS.passphrase]: { value } }, - }); - setIsInputPassphraseValid(valid); - }, - onFocus: () => { - setPassphraseInputErrorMessage(); - }, - type: INPUT_TYPES.password, - ...passphraseInputProps, - }} - label={passphraseLabel} - {...restPassphraseOutlinedInputWithLabelProps} - /> - } - ref={inputPassphraseRef} - /> - ), - }, - 'credential-message-group': { - children: , - sm: 2, - sx: { display: messagesGroupSxDisplay }, - }, - 'credential-submit': submitGrid, - }} - spacing={gridSpacing} - {...restGridProps} - /> + + + } + inputTestBatch={buildIdentifierInputTestBatch( + identifierLabel, + () => { + setMessage(identifierId); + }, + { + onFinishBatch: + buildFinishInputTestBatchFunction(identifierId), + }, + (message) => { + setMessage(identifierId, { children: message }); + }, + )} + onBlurAppend={(...args) => { + onIdentifierBlurAppend?.call(null, ...args); + }} + onFirstRender={buildInputFirstRenderFunction(identifierId)} + onUnmount={buildInputUnmountFunction(identifierId)} + ref={inputIdentifierRef} + required + /> + ), + }, + 'gate-input-cell-credential-passphrase': { + children: ( + + } + inputTestBatch={buildPeacefulStringTestBatch( + passphraseLabel, + () => { + setMessage(passphraseId); + }, + { + onFinishBatch: + buildFinishInputTestBatchFunction(passphraseId), + }, + (message) => { + setMessage(passphraseId, { + children: message, + }); + }, + )} + onFirstRender={buildInputFirstRenderFunction(passphraseId)} + onUnmount={buildInputUnmountFunction(passphraseId)} + ref={inputPassphraseRef} + required + /> + ), + }, + ...submitAreaGridLayout, + }} + spacing={gridSpacing} + {...restGridProps} + /> + ); }, ); GateForm.displayName = 'GateForm'; +export { INPUT_ID_GATE_ID, INPUT_ID_GATE_PASSPHRASE }; + export default GateForm; diff --git a/striker-ui/components/GatePanel.tsx b/striker-ui/components/GatePanel.tsx new file mode 100644 index 00000000..e3ddd016 --- /dev/null +++ b/striker-ui/components/GatePanel.tsx @@ -0,0 +1,58 @@ +import { useRouter } from 'next/router'; +import { FC } from 'react'; + +import api from '../lib/api'; +import GateForm from './GateForm'; +import handleAPIError from '../lib/handleAPIError'; +import { Panel } from './Panels'; + +const GatePanel: FC = () => { + const router = useRouter(); + + return ( + + { + setIsSubmitting(true); + + api + .post( + '/auth/login', + { username, password }, + { withCredentials: true }, + ) + .then(() => { + router.push('/'); + }) + .catch((error) => { + const emsg = handleAPIError(error, { + onResponseErrorAppend: () => ({ + children: `Credentials mismatched.`, + type: 'warning', + }), + }); + + setMessage(emsg); + }) + .finally(() => { + setIsSubmitting(false); + }); + }} + passphraseLabel="Passphrase" + submitLabel="Login" + /> + + ); +}; + +export default GatePanel; diff --git a/striker-ui/components/InputWithRef.tsx b/striker-ui/components/InputWithRef.tsx index 7c0ad269..00faccd7 100644 --- a/striker-ui/components/InputWithRef.tsx +++ b/striker-ui/components/InputWithRef.tsx @@ -25,7 +25,9 @@ type InputWithRefOptionalPropsWithoutDefault< TypeName extends keyof MapToInputType, > = { inputTestBatch?: InputTestBatch; + onBlurAppend?: InputBaseProps['onBlur']; onFirstRender?: InputFirstRenderFunction; + onFocusAppend?: InputBaseProps['onFocus']; onUnmount?: () => void; valueKey?: CreateInputOnChangeHandlerOptions['valueKey']; }; @@ -69,7 +71,9 @@ const InputWithRef = forwardRef( { input, inputTestBatch, + onBlurAppend, onFirstRender, + onFocusAppend, onUnmount, required: isRequired = INPUT_WITH_REF_DEFAULT_PROPS.required, valueKey, @@ -125,14 +129,20 @@ const InputWithRef = forwardRef( () => initOnBlur ?? (testInput && - (({ target: { value } }) => { + ((...args) => { + const { + 0: { + target: { value }, + }, + } = args; const isValid = testInput({ inputs: { [INPUT_TEST_ID]: { value } }, }); setIsInputValid(isValid); + onBlurAppend?.call(null, ...args); })), - [initOnBlur, testInput], + [initOnBlur, onBlurAppend, testInput], ); const onChange = useMemo( () => @@ -160,10 +170,11 @@ const InputWithRef = forwardRef( () => initOnFocus ?? (inputTestBatch && - (() => { + ((...args) => { inputTestBatch.defaults?.onSuccess?.call(null, { append: {} }); + onFocusAppend?.call(null, ...args); })), - [initOnFocus, inputTestBatch], + [initOnFocus, inputTestBatch, onFocusAppend], ); /** diff --git a/striker-ui/components/ManageManifest/ManageManifestPanel.tsx b/striker-ui/components/ManageManifest/ManageManifestPanel.tsx index a6dbdc3f..4d5f673f 100644 --- a/striker-ui/components/ManageManifest/ManageManifestPanel.tsx +++ b/striker-ui/components/ManageManifest/ManageManifestPanel.tsx @@ -45,7 +45,7 @@ import useFormUtils from '../../hooks/useFormUtils'; import useIsFirstRender from '../../hooks/useIsFirstRender'; import useProtectedState from '../../hooks/useProtectedState'; -const MSG_ID_API = 'api'; +const MSG_ID_MANIFEST_API = 'api'; const getFormData = ( ...[{ target }]: DivFormEventHandlerParameters @@ -237,7 +237,7 @@ const ManageManifestPanel: FC = () => { api[method](url, body) .then(() => { - setMessage(MSG_ID_API, { + setMessage(MSG_ID_MANIFEST_API, { children: successMsg, }); }) @@ -245,7 +245,7 @@ const ManageManifestPanel: FC = () => { const emsg = handleAPIError(apiError); emsg.children = getErrorMsg(emsg.children); - setMessage(MSG_ID_API, emsg); + setMessage(MSG_ID_MANIFEST_API, emsg); }) .finally(() => { setIsSubmittingForm(false); diff --git a/striker-ui/components/PrepareHostForm.tsx b/striker-ui/components/PrepareHostForm.tsx index 8dc1cbea..0ea5017e 100644 --- a/striker-ui/components/PrepareHostForm.tsx +++ b/striker-ui/components/PrepareHostForm.tsx @@ -58,7 +58,6 @@ const PrepareHostForm: FC = () => { const { protect } = useProtect(); const confirmDialogRef = useRef({}); - const gateFormRef = useRef({}); const inputEnterpriseKeyRef = useRef>({}); const inputHostNameRef = useRef>({}); const inputRedhatPassword = useRef>({}); @@ -207,18 +206,7 @@ const PrepareHostForm: FC = () => { }, }, }} - identifierInputTestBatchBuilder={(setMessage) => - buildIPAddressTestBatch( - HOST_IP_LABEL, - () => { - setMessage(); - }, - undefined, - (message) => { - setMessage({ children: message, type: 'warning' }); - }, - ) - } + identifierInputTestBatchBuilder={buildIPAddressTestBatch} identifierLabel={HOST_IP_LABEL} onIdentifierBlurAppend={({ target: { value } }) => { if (connectedHostIPAddress) { @@ -230,26 +218,18 @@ const PrepareHostForm: FC = () => { } }} onSubmitAppend={( - { getValue: getIdentifier }, - { getValue: getPassphrase }, - setMessage, - setIsSubmitting, + ipAddress, + password, + setGateMessage, + setGateIsSubmitting, ) => { - const identifierValue = getIdentifier?.call(null); - const passphraseValue = getPassphrase?.call(null); + const body = { ipAddress, password }; api - .put<{ - hostName: string; - hostOS: string; - hostUUID: string; - isConnected: boolean; - isInetConnected: boolean; - isOSRegistered: boolean; - }>('/command/inquire-host', { - ipAddress: identifierValue, - password: passphraseValue, - }) + .put( + '/command/inquire-host', + body, + ) .then( ({ data: { @@ -277,31 +257,30 @@ const PrepareHostForm: FC = () => { setIsShowRedhatSection(true); } - setConnectedHostIPAddress(identifierValue); - setConnectedHostPassword(passphraseValue); + setConnectedHostIPAddress(ipAddress); + setConnectedHostPassword(password); setConnectedHostUUID(hostUUID); setIsShowAccessSubmit(false); setIsShowOptionalSection(true); } else { - setMessage?.call(null, { + setGateMessage({ children: `Failed to establish a connection with the given host credentials.`, type: 'error', }); } }, ) - .catch((error) => { - const errorMessage = handleAPIError(error); + .catch((apiError) => { + const emsg = handleAPIError(apiError); - setMessage?.call(null, errorMessage); + setGateMessage?.call(null, emsg); }) .finally(() => { - setIsSubmitting(false); + setGateIsSubmitting(false); }); }} passphraseLabel="Host root password" - ref={gateFormRef} submitLabel="Test access" /> ), diff --git a/striker-ui/lib/test_input/buildDomainTestBatch.tsx b/striker-ui/lib/test_input/buildDomainTestBatch.tsx index 9ed17475..0d0eeb7d 100644 --- a/striker-ui/lib/test_input/buildDomainTestBatch.tsx +++ b/striker-ui/lib/test_input/buildDomainTestBatch.tsx @@ -21,8 +21,8 @@ const buildDomainTestBatch: BuildInputTestBatchFunction = ( onDomainTestFailure( <> {inputName} can only contain lowercase alphanumeric, hyphen ( - - ), and dot () characters. + + ), and dot () characters. , ...args, ); diff --git a/striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx b/striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx index e05e0fa2..ab633e7f 100644 --- a/striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx +++ b/striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx @@ -25,12 +25,12 @@ const buildPeacefulStringTestBatch: BuildInputTestBatchFunction = ( onTestPeacefulStringFailureAppend( <> {inputName} cannot contain single-quote ( - - ), double-quote ( - ), slash ( - ), backslash ( - ), angle brackets ( - ), curly brackets ( + + ), double-quote ( + ), slash ( + ), backslash ( + ), angle brackets ( + ), curly brackets ( ). , ...args, diff --git a/striker-ui/pages/login/index.tsx b/striker-ui/pages/login/index.tsx new file mode 100644 index 00000000..358b486a --- /dev/null +++ b/striker-ui/pages/login/index.tsx @@ -0,0 +1,17 @@ +import Head from 'next/head'; +import { FC } from 'react'; + +import GatePanel from '../../components/GatePanel'; +import Header from '../../components/Header'; + +const Login: FC = () => ( + <> + + Login + +
+ + +); + +export default Login; diff --git a/striker-ui/types/APICommand.d.ts b/striker-ui/types/APICommand.d.ts new file mode 100644 index 00000000..e8a084d6 --- /dev/null +++ b/striker-ui/types/APICommand.d.ts @@ -0,0 +1,8 @@ +type APICommandInquireHostResponseBody = { + hostName: string; + hostOS: string; + hostUUID: string; + isConnected: boolean; + isInetConnected: boolean; + isOSRegistered: boolean; +}; diff --git a/striker-ui/types/GateForm.d.ts b/striker-ui/types/GateForm.d.ts index 5505c16b..41d817e5 100644 --- a/striker-ui/types/GateForm.d.ts +++ b/striker-ui/types/GateForm.d.ts @@ -10,27 +10,26 @@ type GateFormMessageSetter = ( type GateFormSubmittingSetter = (value: boolean) => void; type GateFormSubmitHandler = ( - identifierContent: import('../components/InputWithRef').InputForwardedRefContent<'string'>, - passphraseContent: import('../components/InputWithRef').InputForwardedRefContent<'string'>, + identifier: string, + passphrase: string, setMessage: GateFormMessageSetter, setIsSubmitting: GateFormSubmittingSetter, - messageGroupContent: import('../components/MessageGroup').MessageGroupForwardedRefContent, - ...args: Parameters + ...args: Parameters ) => void; type GateFormOptionalProps = { allowSubmit?: boolean; + formContainer?: boolean; gridProps?: Partial; + identifierId?: string; + identifierInputTestBatchBuilder?: BuildInputTestBatchFunction; identifierOutlinedInputWithLabelProps?: Partial< import('../components/OutlinedInputWithLabel').OutlinedInputWithLabelProps >; - identifierInputTestBatchBuilder?: ( - setMessage: GateFormMessageSetter, - identifierContent: import('../components/InputWithRef').InputForwardedRefContent<'string'>, - ) => ReturnType; onIdentifierBlurAppend?: import('../components/OutlinedInput').OutlinedInputProps['onBlur']; - onSubmit?: ContainedButtonProps['onClick']; + onSubmit?: DivFormEventHandler; onSubmitAppend?: GateFormSubmitHandler; + passphraseId?: string; passphraseOutlinedInputWithLabelProps?: Partial< import('../components/OutlinedInputWithLabel').OutlinedInputWithLabelProps >; diff --git a/tools/anvil-access-module b/tools/anvil-access-module index 7e428784..0778caa3 100755 --- a/tools/anvil-access-module +++ b/tools/anvil-access-module @@ -15,6 +15,38 @@ # # # --- Usages --- +# To use interactively or process a script: +# anvil-access-module [--script ] +# +# * Inputs are processed by lines. Each line must satisfy one of the +# following format: +# +# [ ]r[ uuid=] +# +# Performs a data query script (SELECT) on the database. Targets the +# specified database if "uuid=" is provided. +# +# [ ]w[ uuid=] +# +# Performs a data definition or manipulation script on the database. +# +# [ ]x subroutine, or hash available in Anvil::Tools class> [space-separated positional subroutine parameters...] +# +# Executes an Anvil module subroutine OR retrieves a hash value. This is +# designed to expose the most-used parts of "$anvil->..." to the +# interactive/script function of this tool. +# +# * A quoted string is treated as one positional parameter with the +# wrapping quotes removed. +# +# ! The tool will attempt to decode each positional parameter as JSON. +# Parameters that fail the decoding will be passed to the subroutine +# as-is. +# +# * The response will be prefixed with line UUID if provided. Line UUID must +# be followed by a space to be recognized. +# +# * Lines that fail to meet the format above are ignored. # # To read from database: # anvil-access-module --query [--uuid ] @@ -116,114 +148,69 @@ use strict; use warnings; use Anvil::Tools; use JSON; -use Data::Dumper; +use Text::ParseWords; $| = 1; my $THIS_FILE = ($0 =~ /^.*\/(.*)$/)[0]; my $running_directory = ($0 =~ /^(.*?)\/$THIS_FILE$/)[0]; -if (($running_directory =~ /^\./) && ($ENV{PWD})) -{ - $running_directory =~ s/^\./$ENV{PWD}/; -} - -my $anvil = Anvil::Tools->new(); -sub is_array -{ - return ref($_[0]) eq "ARRAY"; -} - -sub is_hash -{ - return ref($_[0]) eq "HASH"; -} +$running_directory =~ s/^\./$ENV{PWD}/ if $running_directory =~ /^\./ && $ENV{PWD}; -sub db_access -{ - my $parameters = shift; - my $db_access_mode = $parameters->{db_access_mode}; - my $db_uuid = $parameters->{db_uuid}; - my $sql_query = $parameters->{sql_query}; - - my $access_parameters = { query => $sql_query, uuid => $db_uuid, source => $THIS_FILE, line => __LINE__ }; - - return ($db_access_mode eq "write") - ? { write_code => $anvil->Database->write($access_parameters) } - : $anvil->Database->query($access_parameters); -} +my $anvil = Anvil::Tools->new(); -sub call_pre_data_fns +sub access_chain { my $parameters = shift; - my $fns = $parameters->{fns}; + # Required: + my $chain_str = $parameters->{chain}; + # Optional: + my $chain_args = $parameters->{chain_args} // []; - if (is_array($fns)) + my @chain = split(/->|[.]/, $chain_str); + my $key_index = 0; + my $intermediate = $anvil; + my @results; + + foreach my $key (@chain) { - foreach my $fn_wrapper ( @{$fns} ) + my $is_intermediate_hash = is_hash($intermediate); + my $is_last_key = $key_index == $#chain; + + if ($is_intermediate_hash) # Left-hand is hash; treat it as reading data { - if (is_array($fn_wrapper)) + last if (not exists $intermediate->{$key}); + + if ($is_last_key) { - # The double dash ( // ) operator is similar to the or ( || ) - # operator; it tests for defined instead of true. + @results = ($intermediate->{$key}); - my $pre_chain = @{$fn_wrapper}[0] // ""; - my @fn_params = @{$fn_wrapper}[1..$#{$fn_wrapper}] // (); - my @chain = split(/->|,/, $pre_chain); - my $intermediate = $anvil; - my $key_index = 0; - - foreach my $key ( @chain ) - { - last if not defined $intermediate->${key}; - - if ($key_index == $#chain && $intermediate->can($key)) - { - eval { $intermediate->${key}(@fn_params); }; - } - else - { - $intermediate = $intermediate->${key}; - } - - $key_index += 1; - } + last; } - } - } -} - -sub get_anvil_data -{ - my $parameters = shift; - my $chain = $parameters->{chain}; - my $source_intermediate = $anvil->data; - my $target_intermediate = $parameters->{data}; - my $key_index = 0; - - foreach my $key ( @{$chain} ) - { - last if not exists $source_intermediate->{$key}; + $intermediate = $intermediate->{$key}; + } + else # Left-hand is not hash; treat it as blessed/class object (module) and try to call a method from it + { + # Key not found in object; stop following the chain + eval { defined $intermediate->${key} ? 1 : 0; } or last; - $source_intermediate = $source_intermediate->{$key}; + # On the last key of the chain; try to execute the subroutine if it exists + if ( $is_last_key && $intermediate->can($key) ) + { + # Trailing 1 means the eval block will return success if the subroutine and assign succeeded + eval { (@results) = $intermediate->${key}(@$chain_args); 1; } or @results = (1); - if (not exists $target_intermediate->{$key}) - { - $target_intermediate->{$key} = {}; - } + last; + } - if ($key_index < $#{$chain}) - { - $target_intermediate = $target_intermediate->{$key}; - } - else - { - $target_intermediate->{$key} = $source_intermediate; + $intermediate = $intermediate->${key}; } $key_index += 1; } + + return (@results); } sub call_fn @@ -248,6 +235,45 @@ sub call_fn } } +sub call_pre_data_fns +{ + my $parameters = shift; + my $fns = $parameters->{fns}; + + if (is_array($fns)) + { + foreach my $fn_wrapper ( @{$fns} ) + { + if (is_array($fn_wrapper)) + { + # The double dash ( // ) operator is similar to the or ( || ) + # operator; it tests for defined instead of true. + + my @cargs = @{$fn_wrapper}[1..$#{$fn_wrapper}]; + + access_chain({ + chain => @{$fn_wrapper}[0], + chain_args => \@cargs, + }); + } + } + } +} + +sub db_access +{ + my $parameters = shift; + my $db_access_mode = $parameters->{db_access_mode} // ""; + my $db_uuid = $parameters->{db_uuid}; + my $sql_query = $parameters->{sql_query}; + + my $access_parameters = { query => $sql_query, uuid => $db_uuid, source => $THIS_FILE, line => __LINE__ }; + + return ($db_access_mode eq "write") + ? { write_code => $anvil->Database->write($access_parameters) } + : $anvil->Database->query($access_parameters); +} + sub foreach_nested { my $parameters = shift; @@ -287,6 +313,137 @@ sub foreach_nested } } +sub get_anvil_data +{ + my $parameters = shift; + my $chain = $parameters->{chain}; + my $target_intermediate = $parameters->{data}; + + my $source_intermediate = $anvil->data; + my $key_index = 0; + + foreach my $key ( @{$chain} ) + { + last if not exists $source_intermediate->{$key}; + + $source_intermediate = $source_intermediate->{$key}; + + if (not exists $target_intermediate->{$key}) + { + $target_intermediate->{$key} = {}; + } + + if ($key_index < $#{$chain}) + { + $target_intermediate = $target_intermediate->{$key}; + } + else + { + $target_intermediate->{$key} = $source_intermediate; + } + + $key_index += 1; + } +} + +sub get_scmd_args +{ + my $parameters = shift; + # Required: + my $input = $parameters->{input}; + my $get_values = $parameters->{get_values}; + # Optional: + my $cmd = $parameters->{cmd}; + my $arg_names = $parameters->{names} // []; + + my $i = 0; + my $args = {}; + my @matches = $get_values->($input, $cmd); + + foreach (@matches) + { + my $arg_name = $arg_names->[$i++] // "$i"; + + $args->{$arg_name} = $_ if defined $arg_name; + } + + return $args; +} + +sub is_array +{ + return ref($_[0]) eq "ARRAY"; +} + +sub is_hash +{ + return ref($_[0]) eq "HASH"; +} + +sub process_scmd_db +{ + my $parameters = shift; + # Required: + my $cmd = $parameters->{cmd}; + my $input = $parameters->{input}; + # Optional: + my $lid = $parameters->{lid} // ""; + my $mode = $parameters->{mode}; + + my $sargs = get_scmd_args({ + cmd => $cmd, + input => $input, + get_values => sub { my $c = $_[1]; return $_[0] =~ /^$c\s+(?:uuid=([^\s]+))?\s*(.*)$/; }, + names => ["uuid", "script"], + }); + + eval { + my $results = db_access({ db_uuid => $sargs->{uuid}, sql_query => $sargs->{script}, db_access_mode => $mode }); + + pstdout($lid.JSON->new->utf8->encode($results)); + } or do { + pstderr("failed to access database; cause: $@"); + } +} + +sub process_scmd_execute +{ + my $parameters = shift; + # Required: + my $input = $parameters->{input}; + # Optional: + my $lid = $parameters->{lid} // ""; + + my @sargs = parse_line('\s+', 0, $input); + + return if $#sargs < 1; + + my $chain_str = $sargs[1]; + my @chain_args = $#sargs > 1 ? @sargs[2..$#sargs] : (); + + for my $i (0..$#chain_args) + { + my $param = $chain_args[$i]; + my $is_decode_success = eval { $param = decode_json($param); }; + + $chain_args[$i] = $param if $is_decode_success; + } + + my (@results) = access_chain({ chain => $chain_str, chain_args => \@chain_args }); + + pstdout($lid.JSON->new->utf8->allow_blessed->encode({ sub_results => \@results })); +} + +sub pstdout +{ + print $_[0]."\n" if defined $_[0]; +} + +sub pstderr +{ + print STDERR "error: ".$_[0]."\n" if defined $_[0]; +} + $anvil->Get->switches; $anvil->Database->connect; @@ -299,18 +456,20 @@ if (not $anvil->data->{sys}{database}{connections}) } my $data_hash = $anvil->data->{switches}{'data'}; -my $db_access_mode = defined $anvil->data->{switches}{'mode'} ? $anvil->data->{switches}{'mode'} : ""; +my $db_access_mode = $anvil->data->{switches}{'mode'} // ""; my $db_uuid = $anvil->data->{switches}{'uuid'}; my $pre_data = $anvil->data->{switches}{'predata'}; +my $script_file = $anvil->data->{switches}{'script'} // "-"; my $sql_query = $anvil->data->{switches}{'query'}; -my $sub_module_name = defined $anvil->data->{switches}{'sub-module'} ? $anvil->data->{switches}{'sub-module'} : "Database"; -my $sub_name = defined $anvil->data->{switches}{'sub'} ? $anvil->data->{switches}{'sub'} : ""; -my $sub_params = defined $anvil->data->{switches}{'sub-params'} ? $anvil->data->{switches}{'sub-params'} : "{}"; +my $sub_module_name = $anvil->data->{switches}{'sub-module'} // "Database"; +my $sub_name = $anvil->data->{switches}{'sub'} // ""; +my $sub_params = $anvil->data->{switches}{'sub-params'} // "{}"; if ($sql_query) { my $results = db_access({ db_uuid => $db_uuid, sql_query => $sql_query, db_access_mode => $db_access_mode }); - print JSON->new->utf8->encode($results)."\n"; + + pstdout(JSON->new->utf8->encode($results)); } elsif ($anvil->${sub_module_name}->can($sub_name)) { @@ -319,12 +478,14 @@ elsif ($anvil->${sub_module_name}->can($sub_name)) if (not $is_decode_sub_params_success) { - print STDERR "error: failed to parse subroutine parameters\n"; + pstderr("failed to parse subroutine parameters"); + $anvil->nice_exit({ exit_code => 1 }); } my (@results) = $anvil->${sub_module_name}->${sub_name}($decoded_sub_params); - print JSON->new->utf8->encode({ sub_results => scalar(@results) > 1 ? \@results : $results[0] })."\n"; + + pstdout(JSON->new->utf8->encode({ sub_results => scalar(@results) > 1 ? \@results : $results[0] })); } elsif ($data_hash) { @@ -344,7 +505,8 @@ elsif ($data_hash) if (not $is_decode_data_hash_success) { - print STDERR "error: failed to parse data structure\n"; + pstderr("failed to parse data structure"); + $anvil->nice_exit({ exit_code => 1 }); } @@ -355,12 +517,63 @@ elsif ($data_hash) on_chain_end => { fn => \&get_anvil_data, params => $get_anvil_data_params }, }); - print JSON->new->utf8->allow_blessed->encode($get_anvil_data_params->{data})."\n"; + pstdout(JSON->new->utf8->allow_blessed->encode($get_anvil_data_params->{data})); } else { - print STDERR "error: missing switches and perhaps their respective parameters; one of --data, --query, or --sub is required\n"; - $anvil->nice_exit({ exit_code => 1 }); + my $script_file_handle; + + eval { + if ($script_file =~ /^#!SET!#|-$/) + { + $script_file = "-"; + + open($script_file_handle, $script_file); + } + else + { + open($script_file_handle, "< :encoding(UTF-8)", $script_file); + } + } or do { + # open() sets $! upon error, different from the database module failure (which sets $@) + pstderr("failed to open $script_file as script input; cause: $!"); + + $anvil->nice_exit({ exit_code => 1 }); + }; + + while (my $script_line = <$script_file_handle>) + { + last if ($script_line =~ /^(?:q|quit)\s+$/); + + $script_line =~ s/\s+$//; + + my $scmd_db_read = "r"; + my $scmd_db_write = "w"; + my $scmd_execute = "x"; + + $script_line =~ s/^([[:xdigit:]]{8}-[[:xdigit:]]{4}-[1-5][[:xdigit:]]{3}-[89ab][[:xdigit:]]{3}-[[:xdigit:]]{12})\s+//; + + my $script_line_id = $1; + + if ($script_line =~ /^$scmd_db_read\s+/) + { + process_scmd_db({ cmd => $scmd_db_read, input => $script_line, lid => $script_line_id }); + } + elsif ($script_line =~ /^$scmd_db_write\s+/) + { + process_scmd_db({ cmd => $scmd_db_write, input => $script_line, lid => $script_line_id, mode => "write" }); + } + elsif ($script_line =~ /^$scmd_execute\s+/) + { + process_scmd_execute({ input => $script_line, lid => $script_line_id }); + } + } + + close($script_file_handle) or do { + pstderr("failed to close $script_file handle; cause: $!"); + + $anvil->nice_exit({ exit_code => 1 }); + }; } $anvil->nice_exit({ exit_code => 0 }); diff --git a/units/striker-ui-api.service b/units/striker-ui-api.service index 4c7d15f6..b568f1f8 100644 --- a/units/striker-ui-api.service +++ b/units/striker-ui-api.service @@ -4,9 +4,9 @@ Wants=network.target [Service] Type=simple -# Run as apache to allow the API to access apache-owned locations; i.e., /mnt/shared. -User=apache -Group=apache +User=root +Group=root +Environment=PORT=80 ExecStart=/usr/bin/node /usr/share/striker-ui-api/index.js ExecStop=/bin/kill -WINCH ${MAINPID} Restart=always