diff --git a/package-lock.json b/package-lock.json index 51d3d75..94c7164 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "js-cookie": "^3.0.5", "jsonwebtoken": "^9.0.2", "postgres": "^3.4.5", + "svelte-preprocess": "^6.0.3", "vitest": "^2.0.4" }, "devDependencies": { @@ -592,9 +593,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", - "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", + "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", "dev": true, "license": "MIT", "engines": { @@ -970,9 +971,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.31.0.tgz", - "integrity": "sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.1.tgz", + "integrity": "sha512-/pqA4DmqyCm8u5YIDzIdlLcEmuvxb0v8fZdFhVMszSpDTgbQKdw3/mB3eMUHIbubtJ6F9j+LtmyCnHTEqIHyzA==", "cpu": [ "arm" ], @@ -983,9 +984,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.31.0.tgz", - "integrity": "sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.32.1.tgz", + "integrity": "sha512-If3PDskT77q7zgqVqYuj7WG3WC08G1kwXGVFi9Jr8nY6eHucREHkfpX79c0ACAjLj3QIWKPJR7w4i+f5EdLH5Q==", "cpu": [ "arm64" ], @@ -996,9 +997,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.31.0.tgz", - "integrity": "sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.32.1.tgz", + "integrity": "sha512-zCpKHioQ9KgZToFp5Wvz6zaWbMzYQ2LJHQ+QixDKq52KKrF65ueu6Af4hLlLWHjX1Wf/0G5kSJM9PySW9IrvHA==", "cpu": [ "arm64" ], @@ -1009,9 +1010,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.31.0.tgz", - "integrity": "sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.32.1.tgz", + "integrity": "sha512-sFvF+t2+TyUo/ZQqUcifrJIgznx58oFZbdHS9TvHq3xhPVL9nOp+yZ6LKrO9GWTP+6DbFtoyLDbjTpR62Mbr3Q==", "cpu": [ "x64" ], @@ -1022,9 +1023,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.31.0.tgz", - "integrity": "sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.32.1.tgz", + "integrity": "sha512-NbOa+7InvMWRcY9RG+B6kKIMD/FsnQPH0MWUvDlQB1iXnF/UcKSudCXZtv4lW+C276g3w5AxPbfry5rSYvyeYA==", "cpu": [ "arm64" ], @@ -1035,9 +1036,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.31.0.tgz", - "integrity": "sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.32.1.tgz", + "integrity": "sha512-JRBRmwvHPXR881j2xjry8HZ86wIPK2CcDw0EXchE1UgU0ubWp9nvlT7cZYKc6bkypBt745b4bglf3+xJ7hXWWw==", "cpu": [ "x64" ], @@ -1048,9 +1049,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.31.0.tgz", - "integrity": "sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.32.1.tgz", + "integrity": "sha512-PKvszb+9o/vVdUzCCjL0sKHukEQV39tD3fepXxYrHE3sTKrRdCydI7uldRLbjLmDA3TFDmh418XH19NOsDRH8g==", "cpu": [ "arm" ], @@ -1061,9 +1062,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.31.0.tgz", - "integrity": "sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.32.1.tgz", + "integrity": "sha512-9WHEMV6Y89eL606ReYowXuGF1Yb2vwfKWKdD1A5h+OYnPZSJvxbEjxTRKPgi7tkP2DSnW0YLab1ooy+i/FQp/Q==", "cpu": [ "arm" ], @@ -1074,9 +1075,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.31.0.tgz", - "integrity": "sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.32.1.tgz", + "integrity": "sha512-tZWc9iEt5fGJ1CL2LRPw8OttkCBDs+D8D3oEM8mH8S1ICZCtFJhD7DZ3XMGM8kpqHvhGUTvNUYVDnmkj4BDXnw==", "cpu": [ "arm64" ], @@ -1087,9 +1088,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.31.0.tgz", - "integrity": "sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.32.1.tgz", + "integrity": "sha512-FTYc2YoTWUsBz5GTTgGkRYYJ5NGJIi/rCY4oK/I8aKowx1ToXeoVVbIE4LGAjsauvlhjfl0MYacxClLld1VrOw==", "cpu": [ "arm64" ], @@ -1100,9 +1101,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.31.0.tgz", - "integrity": "sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.32.1.tgz", + "integrity": "sha512-F51qLdOtpS6P1zJVRzYM0v6MrBNypyPEN1GfMiz0gPu9jN8ScGaEFIZQwteSsGKg799oR5EaP7+B2jHgL+d+Kw==", "cpu": [ "loong64" ], @@ -1113,9 +1114,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.31.0.tgz", - "integrity": "sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.32.1.tgz", + "integrity": "sha512-wO0WkfSppfX4YFm5KhdCCpnpGbtgQNj/tgvYzrVYFKDpven8w2N6Gg5nB6w+wAMO3AIfSTWeTjfVe+uZ23zAlg==", "cpu": [ "ppc64" ], @@ -1126,9 +1127,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.31.0.tgz", - "integrity": "sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.32.1.tgz", + "integrity": "sha512-iWswS9cIXfJO1MFYtI/4jjlrGb/V58oMu4dYJIKnR5UIwbkzR0PJ09O0PDZT0oJ3LYWXBSWahNf/Mjo6i1E5/g==", "cpu": [ "riscv64" ], @@ -1139,9 +1140,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.31.0.tgz", - "integrity": "sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.32.1.tgz", + "integrity": "sha512-RKt8NI9tebzmEthMnfVgG3i/XeECkMPS+ibVZjZ6mNekpbbUmkNWuIN2yHsb/mBPyZke4nlI4YqIdFPgKuoyQQ==", "cpu": [ "s390x" ], @@ -1152,9 +1153,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.31.0.tgz", - "integrity": "sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.32.1.tgz", + "integrity": "sha512-WQFLZ9c42ECqEjwg/GHHsouij3pzLXkFdz0UxHa/0OM12LzvX7DzedlY0SIEly2v18YZLRhCRoHZDxbBSWoGYg==", "cpu": [ "x64" ], @@ -1165,9 +1166,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.31.0.tgz", - "integrity": "sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.32.1.tgz", + "integrity": "sha512-BLoiyHDOWoS3uccNSADMza6V6vCNiphi94tQlVIL5de+r6r/CCQuNnerf+1g2mnk2b6edp5dk0nhdZ7aEjOBsA==", "cpu": [ "x64" ], @@ -1178,9 +1179,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.31.0.tgz", - "integrity": "sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.32.1.tgz", + "integrity": "sha512-w2l3UnlgYTNNU+Z6wOR8YdaioqfEnwPjIsJ66KxKAf0p+AuL2FHeTX6qvM+p/Ue3XPBVNyVSfCrfZiQh7vZHLQ==", "cpu": [ "arm64" ], @@ -1191,9 +1192,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.31.0.tgz", - "integrity": "sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.32.1.tgz", + "integrity": "sha512-Am9H+TGLomPGkBnaPWie4F3x+yQ2rr4Bk2jpwy+iV+Gel9jLAu/KqT8k3X4jxFPW6Zf8OMnehyutsd+eHoq1WQ==", "cpu": [ "ia32" ], @@ -1204,9 +1205,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.31.0.tgz", - "integrity": "sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.1.tgz", + "integrity": "sha512-ar80GhdZb4DgmW3myIS9nRFYcpJRSME8iqWgzH2i44u+IdrzmiXVxeFnExQ5v4JYUSpg94bWjevMG8JHf1Da5Q==", "cpu": [ "x64" ], @@ -1354,19 +1355,27 @@ "license": "MIT" }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", - "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.8.tgz", + "integrity": "sha512-7fx54m60nLFUVYlxAB1xpe9CBWX2vSrk50Y6ogRJ1v5xxtba7qXTg5BgYDN5dq+yuQQ9HaVlHJyAAt1/mxryFg==", "dev": true, "license": "MIT", "dependencies": { + "@types/ms": "*", "@types/node": "*" } }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "22.10.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz", - "integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==", + "version": "22.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.12.0.tgz", + "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -1391,17 +1400,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.21.0.tgz", - "integrity": "sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.22.0.tgz", + "integrity": "sha512-4Uta6REnz/xEJMvwf72wdUnC3rr4jAQf5jnTkeRQ9b6soxLxhDEbS/pfMPoJLDfFPNVRdryqWUIV/2GZzDJFZw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.21.0", - "@typescript-eslint/type-utils": "8.21.0", - "@typescript-eslint/utils": "8.21.0", - "@typescript-eslint/visitor-keys": "8.21.0", + "@typescript-eslint/scope-manager": "8.22.0", + "@typescript-eslint/type-utils": "8.22.0", + "@typescript-eslint/utils": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1421,16 +1430,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.21.0.tgz", - "integrity": "sha512-Wy+/sdEH9kI3w9civgACwabHbKl+qIOu0uFZ9IMKzX3Jpv9og0ZBJrZExGrPpFAY7rWsXuxs5e7CPPP17A4eYA==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.22.0.tgz", + "integrity": "sha512-MqtmbdNEdoNxTPzpWiWnqNac54h8JDAmkWtJExBVVnSrSmi9z+sZUt0LfKqk9rjqmKOIeRhO4fHHJ1nQIjduIQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.21.0", - "@typescript-eslint/types": "8.21.0", - "@typescript-eslint/typescript-estree": "8.21.0", - "@typescript-eslint/visitor-keys": "8.21.0", + "@typescript-eslint/scope-manager": "8.22.0", + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/typescript-estree": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0", "debug": "^4.3.4" }, "engines": { @@ -1446,14 +1455,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.21.0.tgz", - "integrity": "sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.22.0.tgz", + "integrity": "sha512-/lwVV0UYgkj7wPSw0o8URy6YI64QmcOdwHuGuxWIYznO6d45ER0wXUbksr9pYdViAofpUCNJx/tAzNukgvaaiQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.21.0", - "@typescript-eslint/visitor-keys": "8.21.0" + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1464,14 +1473,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.21.0.tgz", - "integrity": "sha512-95OsL6J2BtzoBxHicoXHxgk3z+9P3BEcQTpBKriqiYzLKnM2DeSqs+sndMKdamU8FosiadQFT3D+BSL9EKnAJQ==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.22.0.tgz", + "integrity": "sha512-NzE3aB62fDEaGjaAYZE4LH7I1MUwHooQ98Byq0G0y3kkibPJQIXVUspzlFOmOfHhiDLwKzMlWxaNv+/qcZurJA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.21.0", - "@typescript-eslint/utils": "8.21.0", + "@typescript-eslint/typescript-estree": "8.22.0", + "@typescript-eslint/utils": "8.22.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.0" }, @@ -1488,9 +1497,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.21.0.tgz", - "integrity": "sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.22.0.tgz", + "integrity": "sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A==", "dev": true, "license": "MIT", "engines": { @@ -1502,14 +1511,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.21.0.tgz", - "integrity": "sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.22.0.tgz", + "integrity": "sha512-SJX99NAS2ugGOzpyhMza/tX+zDwjvwAtQFLsBo3GQxiGcvaKlqGBkmZ+Y1IdiSi9h4Q0Lr5ey+Cp9CGWNY/F/w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.21.0", - "@typescript-eslint/visitor-keys": "8.21.0", + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1555,16 +1564,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.21.0.tgz", - "integrity": "sha512-xcXBfcq0Kaxgj7dwejMbFyq7IOHgpNMtVuDveK7w3ZGwG9owKzhALVwKpTF2yrZmEwl9SWdetf3fxNzJQaVuxw==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.22.0.tgz", + "integrity": "sha512-T8oc1MbF8L+Bk2msAvCUzjxVB2Z2f+vXYfcucE2wOmYs7ZUwco5Ep0fYZw8quNwOiw9K8GYVL+Kgc2pETNTLOg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.21.0", - "@typescript-eslint/types": "8.21.0", - "@typescript-eslint/typescript-estree": "8.21.0" + "@typescript-eslint/scope-manager": "8.22.0", + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/typescript-estree": "8.22.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1579,13 +1588,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.21.0.tgz", - "integrity": "sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.22.0.tgz", + "integrity": "sha512-AWpYAXnUgvLNabGTy3uBylkgZoosva/miNd1I8Bz3SjotmQPbVqhO4Cczo8AsZ44XVErEBPr/CRSgaj8sG7g0w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/types": "8.22.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2022,9 +2031,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001695", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", - "integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==", + "version": "1.0.30001696", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz", + "integrity": "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==", "funding": [ { "type": "opencollective", @@ -2332,9 +2341,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.87", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.87.tgz", - "integrity": "sha512-mPFwmEWmRivw2F8x3w3l2m6htAUN97Gy0kwpO++2m9iT1Gt8RCFVUfv9U/sIbHJ6rY4P6/ooqFL/eL7ock+pPg==", + "version": "1.5.88", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.88.tgz", + "integrity": "sha512-K3C2qf1o+bGzbilTDCTBhTQcMS9KW60yTAaTeeXsfvQuTDDwlokLam/AdqlqcSy9u4UainDgsHV23ksXAOgamw==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -2412,9 +2421,9 @@ } }, "node_modules/eslint": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz", - "integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz", + "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==", "dev": true, "license": "MIT", "dependencies": { @@ -2423,7 +2432,7 @@ "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.10.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.18.0", + "@eslint/js": "9.19.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -3323,7 +3332,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -3407,9 +3416,9 @@ "license": "MIT" }, "node_modules/loupe": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", - "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", "license": "MIT" }, "node_modules/lru-cache": { @@ -3978,7 +3987,7 @@ "version": "3.1.4", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "lilconfig": "^2.0.5", @@ -4008,7 +4017,7 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">= 6" @@ -4355,9 +4364,9 @@ } }, "node_modules/rollup": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.31.0.tgz", - "integrity": "sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==", + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.1.tgz", + "integrity": "sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA==", "license": "MIT", "dependencies": { "@types/estree": "1.0.6" @@ -4370,25 +4379,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.31.0", - "@rollup/rollup-android-arm64": "4.31.0", - "@rollup/rollup-darwin-arm64": "4.31.0", - "@rollup/rollup-darwin-x64": "4.31.0", - "@rollup/rollup-freebsd-arm64": "4.31.0", - "@rollup/rollup-freebsd-x64": "4.31.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.31.0", - "@rollup/rollup-linux-arm-musleabihf": "4.31.0", - "@rollup/rollup-linux-arm64-gnu": "4.31.0", - "@rollup/rollup-linux-arm64-musl": "4.31.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.31.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.31.0", - "@rollup/rollup-linux-riscv64-gnu": "4.31.0", - "@rollup/rollup-linux-s390x-gnu": "4.31.0", - "@rollup/rollup-linux-x64-gnu": "4.31.0", - "@rollup/rollup-linux-x64-musl": "4.31.0", - "@rollup/rollup-win32-arm64-msvc": "4.31.0", - "@rollup/rollup-win32-ia32-msvc": "4.31.0", - "@rollup/rollup-win32-x64-msvc": "4.31.0", + "@rollup/rollup-android-arm-eabi": "4.32.1", + "@rollup/rollup-android-arm64": "4.32.1", + "@rollup/rollup-darwin-arm64": "4.32.1", + "@rollup/rollup-darwin-x64": "4.32.1", + "@rollup/rollup-freebsd-arm64": "4.32.1", + "@rollup/rollup-freebsd-x64": "4.32.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.32.1", + "@rollup/rollup-linux-arm-musleabihf": "4.32.1", + "@rollup/rollup-linux-arm64-gnu": "4.32.1", + "@rollup/rollup-linux-arm64-musl": "4.32.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.32.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.32.1", + "@rollup/rollup-linux-riscv64-gnu": "4.32.1", + "@rollup/rollup-linux-s390x-gnu": "4.32.1", + "@rollup/rollup-linux-x64-gnu": "4.32.1", + "@rollup/rollup-linux-x64-musl": "4.32.1", + "@rollup/rollup-win32-arm64-msvc": "4.32.1", + "@rollup/rollup-win32-ia32-msvc": "4.32.1", + "@rollup/rollup-win32-x64-msvc": "4.32.1", "fsevents": "~2.3.2" } }, @@ -4448,9 +4457,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4716,9 +4725,9 @@ } }, "node_modules/svelte": { - "version": "5.19.2", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.19.2.tgz", - "integrity": "sha512-Ww1uLgdX5MdQrAO5zfU1dWUh6zqiPR6uIbwqm8a+4eQ+tNEYHRPgypvKKfHh9lmTkmJ30PWZ2O5qX8aS+PblRQ==", + "version": "5.19.5", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.19.5.tgz", + "integrity": "sha512-vVAntseegJX80sgbY8CxQISSE/VoDSfP7VZHoQaf2+z+2XOPOz/N+k455HJmO9O0g8oxTtuE0TBhC/5LAP4lPg==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -4840,6 +4849,61 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/svelte-preprocess": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-6.0.3.tgz", + "integrity": "sha512-PLG2k05qHdhmRG7zR/dyo5qKvakhm8IJ+hD2eFRQmMLHp7X3eJnjeupUtvuRpbNiF31RjVw45W+abDwHEmP5OA==", + "hasInstallScript": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.10.2", + "coffeescript": "^2.5.1", + "less": "^3.11.3 || ^4.0.0", + "postcss": "^7 || ^8", + "postcss-load-config": ">=3", + "pug": "^3.0.0", + "sass": "^1.26.8", + "stylus": ">=0.55", + "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.100 || ^5.0.0", + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "coffeescript": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "postcss-load-config": { + "optional": true + }, + "pug": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/svelte/node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", @@ -5133,7 +5197,7 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -5144,15 +5208,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.21.0.tgz", - "integrity": "sha512-txEKYY4XMKwPXxNkN8+AxAdX6iIJAPiJbHE/FpQccs/sxw8Lf26kqwC3cn0xkHlW8kEbLhkhCsjWuMveaY9Rxw==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.22.0.tgz", + "integrity": "sha512-Y2rj210FW1Wb6TWXzQc5+P+EWI9/zdS57hLEc0gnyuvdzWo8+Y8brKlbj0muejonhMI/xAZCnZZwjbIfv1CkOw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.21.0", - "@typescript-eslint/parser": "8.21.0", - "@typescript-eslint/utils": "8.21.0" + "@typescript-eslint/eslint-plugin": "8.22.0", + "@typescript-eslint/parser": "8.22.0", + "@typescript-eslint/utils": "8.22.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index fd373bf..05982c5 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "js-cookie": "^3.0.5", "jsonwebtoken": "^9.0.2", "postgres": "^3.4.5", + "svelte-preprocess": "^6.0.3", "vitest": "^2.0.4" } } diff --git a/src/app.css b/src/app.css index 000e973..13968d5 100644 --- a/src/app.css +++ b/src/app.css @@ -14,6 +14,7 @@ --elevated-bg-color: #ffffff; --bg-accent-color: #f4f4f4; --danger-color: #ff2d2f; + --hyperlink-color: #3b82f6; } [data-theme='dark'] { @@ -27,6 +28,7 @@ --elevated-bg-color: #101011; --bg-accent-color: #202020; --danger-color: #ff1d1f; + --hyperlink-color: #3b82f6; } body { @@ -71,6 +73,14 @@ h1 { border-bottom: 1px solid var(--separator-line-color); } +.left-border { + border-left: 1px solid var(--separator-line-color); +} + +.right-border { + border-right: 1px solid var(--separator-line-color); +} + .icon-16 { font-size: 16px !important; font-variation-settings: @@ -89,21 +99,24 @@ h1 { 'opsz' 20 } -input[type='search'], input[type='text'], input[type='password'], input[type='email'], input[type='tel'] { +input[type='search'], input[type='text'], input[type='password'], input[type='email'], input[type='tel'], input[type='number'], textarea, select { background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--separator-line-color); + caret-color: var(--text-color); } -input[type='search']:focus, input[type='text']:focus, input[type='password']:focus, input[type='email']:focus, input[type='tel']:focus { +input[type='search']:focus, input[type='text']:focus, input[type='password']:focus, input[type='email']:focus, input[type='tel']:focus, input[type='number']:focus, textarea:focus, select:focus { outline: 0 solid var(--text-color); border: 1px solid var(--primary-color); box-shadow: 0 0 0 0 var(--primary-color); + caret-color: var(--text-color); } -input[type='search']:-webkit-autofill, input[type='text']:-webkit-autofill, input[type='password']:-webkit-autofill, input[type='email']:-webkit-autofill, input[type='tel']:-webkit-autofill { +input[type='search']:-webkit-autofill, input[type='text']:-webkit-autofill, input[type='password']:-webkit-autofill, input[type='email']:-webkit-autofill, input[type='tel']:-webkit-autofill, input[type='number']:-webkit-autofill, textarea:-webkit-autofill, select:-webkit-autofill { -webkit-text-fill-color: var(--text-color) !important; background-clip: text !important; + caret-color: var(--text-color); } .input-field { @@ -155,6 +168,12 @@ input[type='search']:-webkit-autofill, input[type='text']:-webkit-autofill, inpu margin: 0 auto; } +.base-container-small { + width: 100%; + max-width: 960px; + margin: 0 auto; +} + .signin-container { width: 100%; max-width: 420px; @@ -167,6 +186,7 @@ input[type='search']:-webkit-autofill, input[type='text']:-webkit-autofill, inpu @apply px-6; } + .search-cancel::-webkit-search-cancel-button { -webkit-appearance: none; background-color: var(--text-color); @@ -200,10 +220,18 @@ th.left, td.left { @apply shadow-lg } +.elevated-bg { + background-color: var(--elevated-bg-color); +} + .primary-bg-color { background-color: var(--primary-color); } +.accent-bg-color { + background-color: var(--bg-accent-color); +} + .dull-primary-bg-color { background-color: var(--dull-primary-color); } @@ -241,6 +269,10 @@ input[type='checkbox']:focus { background-color: var(--danger-color); } +.danger-color { + color: var(--danger-color); +} + .danger-border-color { border-color: var(--danger-color); } @@ -259,6 +291,18 @@ input[type='checkbox']:focus { background-color: rgba(0,0,0,0.6); /* Black w/ opacity */ } +.modal-always-display { + position: fixed; /* Stay in place */ + z-index: 1; /* Sit on top */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgb(0,0,0); /* Fallback color */ + background-color: rgba(0,0,0,0.6); /* Black w/ opacity */ +} + /* Modal Content/Box */ .modal-content { background-color: var(--bg-color); @@ -269,33 +313,18 @@ input[type='checkbox']:focus { border-radius: 4px; } -/* The Close Button */ -.close { - color: #aaa; - float: right; - font-size: 28px; - font-weight: bold; -} - -.close:hover, -.close:focus { - color: black; - text-decoration: none; - cursor: pointer; -} - h2 { color: var(--text-color); @apply text-2xl } -.tooltip { - position: relative; - display: inline-block; -} +/*.tooltip {*/ +/* position: relative;*/ +/* display: inline-block;*/ +/*}*/ /* Tooltip text */ -.tooltip .tooltip-text { +.tooltip-text { visibility: hidden; background-color: var(--bg-accent-color); color: var(--text-color); @@ -306,16 +335,16 @@ h2 { /* Position the tooltip text - see examples below! */ position: absolute; z-index: 1; - width: 100px; + width: 120px; bottom: 120%; left: 50%; - margin-left: -50px; /* Use half of the width (120/2 = 60), to center the tooltip */ + margin-left: -60px; /* Use half of the width (120/2 = 60), to center the tooltip */ opacity: 0; transition: opacity 0s linear 0.5s; } -.tooltip .tooltip-text::after { +.tooltip-text::after { content: " "; position: absolute; top: 100%; /* At the bottom of the tooltip */ @@ -338,8 +367,16 @@ h2 { } .hover-hyperlink:hover { - color: var(--dull-primary-color); - text-decoration: underline var(--dull-primary-color);; + color: var(--hyperlink-color); + text-decoration: underline var(--hyperlink-color); +} + +.hyperlink-color { + color: var(--hyperlink-color); +} + +.hyperlink-underline { + text-decoration: underline var(--hyperlink-color); } .max-char-length { @@ -347,3 +384,35 @@ h2 { overflow: hidden; text-overflow: ellipsis; } + +::-webkit-scrollbar-track { + background: var(--bg-color); +} + +::-webkit-scrollbar { + width: 12px; +} + +::-webkit-scrollbar-thumb { + background: var(--bg-accent-color); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--separator-line-color); +} + +.scrollbar-on-elevated::-webkit-scrollbar-track { + background: var(--elevated-bg-color); +} + +.-webkit-scrollbar-thumb::-webkit-scrollbar-track { + background: var(--elevated-bg-color); +} + +.details-height { + max-height: calc(100vh - 200px); +} + +.top-with-navbar { + top: 72px; +} diff --git a/src/lib/db/index.server.ts b/src/lib/db/index.server.ts index ee31974..fe381d7 100644 --- a/src/lib/db/index.server.ts +++ b/src/lib/db/index.server.ts @@ -2,6 +2,14 @@ import bcrypt from 'bcrypt'; import sql from '$lib/db/db.server'; import { error } from '@sveltejs/kit'; import { saveAvatar } from '$lib/index.server'; +import { + EmploymentType, + type User, + type Company, + type Tag, + type Posting, + type Application +} from '$lib/types'; export async function createUser(user: User): Promise { const password_hash: string = await bcrypt.hash(user.password!, 12); @@ -48,7 +56,7 @@ export async function updateUser(user: User): Promise { export async function checkUserCreds(username: string, password: string): Promise { const [user] = await sql` - SELECT id, password_hash, perms, active + SELECT id, username, password_hash, perms, active, company_id AS "companyId" FROM users WHERE username = ${username} `; @@ -57,7 +65,8 @@ export async function checkUserCreds(username: string, password: string): Promis return null; } if (await bcrypt.compare(password, user.password_hash)) { - return { id: user.id, perms: user.perms, active: user.active }; + delete user.password_hash; + return user; } return null; } @@ -87,6 +96,19 @@ export async function getUsers(searchQuery: string | null = null): Promise(users); } +export async function getCompanies(searchQuery: string | null = null): Promise { + return sql` + SELECT id, + name, + description, + website, + company_code AS "companyCode", + created_at AT TIME ZONE 'UTC' AS "createdAt" + FROM companies + WHERE name ILIKE ${searchQuery ? `%${searchQuery}%` : '%'}; + `; +} + // should require MANAGE_USERS permission export async function getUser(id: number): Promise { const [user] = await sql` @@ -152,9 +174,84 @@ export async function getUserWithCompany(id: number): Promise { return user; } +export async function getUserWithCompanyAndApplications( + id: number +): Promise<{ user: User; applications: Application[] }> { + const data = await sql` + WITH company_data AS ( + SELECT + id, + name, + description, + website, + created_at AT TIME ZONE 'UTC' AS "createdAt" + FROM companies + WHERE id = (SELECT company_id FROM users WHERE id = ${id}) + ), + user_data AS ( + SELECT + username, + perms, + email, + phone, + full_name AS "fullName", + created_at AT TIME ZONE 'UTC' AS "createdAt", + last_signin AT TIME ZONE 'UTC' AS "lastSignIn", + active + FROM users + WHERE "id" = ${id} + ), + application_data AS ( + SELECT + id, + posting_id AS "postingId", + (SELECT title FROM postings WHERE id = posting_id) AS "postingTitle", + created_at AT TIME ZONE 'UTC' AS "createdAt" + FROM applications + WHERE "user_id" = ${id} + ) + SELECT + ( + SELECT row_to_json(company_data) + FROM company_data + ) AS company, + ( + SELECT row_to_json(user_data) + FROM user_data + ) AS user, + ( + SELECT json_agg(row_to_json(application_data)) + FROM application_data + ) AS applications; + `; + + if (!data) { + error(404, 'User not found'); + } + let user = data[0].user; + user.company = data[0].company; + + user.createdAt = new Date(user.createdAt); + user.lastSignIn = new Date(user.lastSignIn); + if (user.company) { + user.company.createdAt = new Date(user.company.createdAt); + } + let applications = data[0].applications; + if (applications) { + applications.forEach((application: { createdAt: string | number | Date }) => { + application.createdAt = new Date(application.createdAt); + }); + } + + return { + user: user, + applications: applications + }; +} + // should require MANAGE_USERS permission export async function deleteUser(id: number): Promise { - const response = await sql` + await sql` DELETE FROM users WHERE id = ${id}; `; @@ -177,16 +274,456 @@ export async function updateLastSignin(username: string): Promise { `; } -export async function createCompany( - name: string, - description: string, - website: string -): Promise { +export async function createCompany(company: Company): Promise { const response = await sql` INSERT INTO companies (name, description, website, created_at, company_code) - VALUES (${name}, ${description}, ${website}, NOW(), generate_company_code(CAST(CURRVAL('companies_id_seq') AS INT))) + VALUES (${company.name}, ${company.description}, ${company.website}, NOW(), generate_company_code(CAST(CURRVAL('companies_id_seq') AS INT))) RETURNING id; `; return response[0].id; } + +export async function editCompany(company: Company): Promise { + const response = await sql` + UPDATE companies + SET name = ${company.name}, description = ${company.description}, website = ${company.website} + WHERE id = ${company.id} + RETURNING id; + `; + + return response[0].id; +} + +export async function deleteCompany(id: number): Promise { + await sql` + DELETE FROM companies + WHERE id = ${id}; + `; +} + +export async function getCompany(id: number): Promise { + const [company] = await sql` + SELECT id, name, description, website, created_at AS "createdAt" + FROM companies + WHERE id = ${id}; + `; + + if (!company) { + error(404, 'Company not found'); + } + + return company; +} + +export async function getCompanyFullData( + id: number +): Promise<{ company: Company; users: User[]; postings: Posting[] }> { + const data = await sql` + WITH company_data AS ( + SELECT + id, + name, + description, + website, + created_at AT TIME ZONE 'UTC' AS "createdAt" + FROM companies + WHERE id = ${id} + ), + user_data AS ( + SELECT + username, + email, + phone, + full_name AS "fullName" + FROM users + WHERE "company_id" = ${id} + ), + posting_data AS ( + SELECT + id, + title, + description, + employer_id AS "employerId", + address, + employment_type AS "employmentType", + wage, + link, + tag_ids AS "tagIds", + created_at AT TIME ZONE 'UTC' AS "createdAt", + updated_at AT TIME ZONE 'UTC' AS "updatedAt", + flyer_link AS "flyerLink" + FROM postings + WHERE "company_id" = ${id} + ) + SELECT + ( + SELECT row_to_json(company_data) + FROM company_data + ) AS company, + ( + SELECT json_agg(row_to_json(user_data)) + FROM user_data + ) AS users, + ( + SELECT json_agg(row_to_json(posting_data)) + FROM posting_data + ) AS postings; + `; + + if (!data) { + error(404, 'Company not found'); + } + if (data[0].company) { + data[0].company.createdAt = new Date(data[0].company.createdAt); + } + if (data[0].postings) { + data[0].postings.forEach( + (posting: { + createdAt: string | number | Date; + updatedAt: string | number | Date; + tagIds: number[] | undefined; + tags: { id: number; displayName: null; createdAt: null }[]; + }) => { + posting.createdAt = new Date(posting.createdAt); + posting.updatedAt = new Date(posting.updatedAt); + if (posting.tagIds) { + posting.tagIds?.forEach((tagId: number) => { + posting.tags.push({ id: tagId, displayName: null, createdAt: null }); + }); + } + delete posting.tagIds; + } + ); + } + + return { + company: data[0].company, + users: data[0].users, + postings: data[0].postings + }; +} + +export async function createPosting(posting: Posting): Promise { + if (posting.tagIds === null || posting.tagIds === undefined) { + posting.tagIds = []; + } + posting.tags?.forEach((tag) => { + posting.tagIds?.push(tag.id); + }); + if (posting.companyId === null || posting.companyId === undefined) { + if (posting.company) { + posting.companyId = posting.company.id; + } else { + posting.companyId = null; + } + } + const response = await sql` + INSERT INTO postings (title, description, employer_id, address, employment_type, wage, link, tag_ids, created_at, updated_at, flyer_link, company_id) + VALUES (${posting.title}, ${posting.description}, ${posting.employerId}, ${posting.address}, ${posting.employmentType}, ${posting.wage}, ${posting.link}, ${posting.tagIds}, NOW(), NOW(), ${posting.flyerLink}, ${posting.companyId}) + RETURNING id; + `; + + return response[0].id; +} + +export async function editPosting(posting: Posting): Promise { + if (posting.tagIds === null || posting.tagIds === undefined) { + posting.tagIds = []; + } + posting.tags?.forEach((tag) => { + posting.tagIds?.push(tag.id); + }); + if (posting.companyId === null || posting.companyId === undefined) { + if (posting.company) { + posting.companyId = posting.company.id; + } else { + posting.companyId = null; + } + } + + const response = await sql` + UPDATE postings + SET title = ${posting.title}, description = ${posting.description}, employer_id = ${posting.employerId}, address = ${posting.address}, employment_type = ${posting.employmentType}, wage = ${posting.wage}, link = ${posting.link}, tag_ids = ${posting.tagIds}, updated_at = NOW(), flyer_link = ${posting.flyerLink}, company_id = ${posting.companyId} + WHERE id = ${posting.id} + RETURNING id; + `; + + return response[0].id; +} + +export async function deletePosting(id: number): Promise { + await sql` + DELETE FROM postings + WHERE id = ${id}; + `; +} + +export async function getCompanyEmployers( + id: number +): Promise<{ company: Company; users: User[] }> { + const data = await sql` + WITH company_data AS ( + SELECT + id, + name, + description, + website, + created_at AT TIME ZONE 'UTC' AS "createdAt", + company_code AS "companyCode" + FROM companies + WHERE id = ${id} + ), + user_data AS (SELECT id, + username, + email, + phone, + full_name AS "fullName", + created_at AT TIME ZONE 'UTC' AS "createdAt", + last_signin AT TIME ZONE 'UTC' AS "lastSignIn", + company_id as "companyId" + FROM users + WHERE "company_code" = (SELECT company_code FROM companies WHERE id = ${id})) + SELECT + ( + SELECT row_to_json(company_data) + FROM company_data + ) AS company, + ( + SELECT json_agg(row_to_json(user_data)) + FROM user_data + ) AS users; + `; + + if (!data) { + error(404, 'Company not found'); + } + if (data[0].users) { + data[0].users.forEach( + (user: { + company: { id: any }; + companyId: any; + createdAt: string | number | Date; + lastSignIn: string | number | Date; + }) => { + user.company = { + id: user.companyId + }; + user.createdAt = new Date(user.createdAt); + user.lastSignIn = new Date(user.lastSignIn); + delete user.companyId; + } + ); + } + + return { + company: data[0].company, + users: data[0].users + }; +} + +export async function removeEmployerFromCompany(companyId: number, userId: number): Promise { + await sql` + UPDATE users + SET company_id = NULL, + company_code = NULL + WHERE id = ${userId}; + `; +} + +export async function addEmployerToCompany(companyId: number, userId: number): Promise { + await sql` + UPDATE users + SET company_id = ${companyId} + WHERE id = ${userId}; + `; +} + +export async function getPostings(searchQuery: string | null = null): Promise { + const postings = await sql` + SELECT p.id, + p.title, + p.description, + p.employer_id AS "employerId", + p.address, + p.employment_type AS "employmentType", + p.wage, + p.link, + p.tag_ids AS "tagIds", + p.created_at AT TIME ZONE 'UTC' AS "createdAt", + p.updated_at AT TIME ZONE 'UTC' AS "updatedAt", + p.flyer_link AS "flyerLink", + p.company_id AS "companyId", + c.name AS "companyName" + FROM postings p + LEFT JOIN companies c ON p.company_id = c.id + WHERE title ILIKE ${searchQuery ? `%${searchQuery}%` : '%'}; + `; + postings.forEach((posting) => { + posting.company = {}; + if (posting.companyName) { + posting.company.name = posting.companyName; + } + delete posting.companyName; + posting.tags = []; + + posting.employmentType = EmploymentType[posting.employmentType as keyof typeof EmploymentType]; + if (posting.tagIds) { + posting.tagIds?.forEach((tagId: number) => { + posting.tags.push({ id: tagId, displayName: null, createdAt: null }); + }); + } + delete posting.tagIds; + }); + return (postings); +} + +export async function getPosting(id: number): Promise { + const data = await sql` + SELECT id, + title, + description, + employer_id AS "employerId", + address, + employment_type AS "employmentType", + wage, + link, + tag_ids AS "tagIds", + created_at AT TIME ZONE 'UTC' AS "createdAt", + updated_at AT TIME ZONE 'UTC' AS "updatedAt", + flyer_link AS "flyerLink", + company_id AS "companyId" + FROM postings + WHERE id = ${id}; + `; + const posting = data[0]; + posting.tags = []; + posting.employmentType = EmploymentType[posting.employmentType as keyof typeof EmploymentType]; + if (posting.tagIds) { + posting.tagIds?.forEach((tagId: number) => { + posting.tags.push({ id: tagId, displayName: null, createdAt: null }); + }); + } + delete posting.tagIds; + + if (posting.createdAt) { + posting.createdAt = new Date(posting.createdAt); + } + if (posting.updatedAt) { + posting.updatedAt = new Date(posting.updatedAt); + } + + return posting; +} + +export async function getPostingFullData(id: number): Promise { + const data = await sql` + WITH company_data AS ( + SELECT + id, + name, + description, + website, + created_at AS "createdAt" + FROM companies + WHERE id = (SELECT company_id FROM postings WHERE id = ${id}) + ), + user_data AS ( + SELECT + username, + email, + phone, + full_name AS "fullName" + FROM users + WHERE "company_id" = (SELECT company_id FROM postings WHERE id = ${id}) + ), + posting_data AS ( + SELECT + id, + title, + description, + employer_id AS "employerId", + address, + employment_type AS "employmentType", + wage, + link, + tag_ids AS "tagIds", + created_at AT TIME ZONE 'UTC' AS "createdAt", + updated_at AT TIME ZONE 'UTC' AS "updatedAt", + flyer_link AS "flyerLink" + FROM postings + WHERE id = ${id} + ) + SELECT + ( + SELECT row_to_json(company_data) + FROM company_data + ) AS company, + ( + SELECT row_to_json(user_data) + FROM user_data + ) AS user, + ( + SELECT row_to_json(posting_data) + FROM posting_data + ) AS posting; + `; + + if (!data) { + error(404, 'Posting not found'); + } + let posting = data[0].posting; + posting.company = data[0].company; + posting.employer = data[0].user; + + if (posting.createdAt) { + posting.createdAt = new Date(posting.createdAt); + } + if (posting.updatedAt) { + posting.updatedAt = new Date(posting.updatedAt); + } + + return posting; +} + +export async function createApplication(application: Application): Promise { + const response = await sql` + INSERT INTO applications (posting_id, user_id, candidate_statement, created_at) + VALUES (${application.postingId}, ${application.userId}, ${application.candidateStatement}, NOW()) + RETURNING id; + `; + + return response[0].id; +} + +export async function deleteApplication(id: number): Promise { + const response = await sql` + DELETE FROM applications + WHERE id = ${id}; + `; +} + +export async function deleteApplicationWithUser( + applicationId: number, + userId: number +): Promise { + console.log(applicationId, userId); + const response = await sql` + DELETE FROM applications + WHERE id = ${applicationId} AND user_id = ${userId}; + `; +} + +export async function getApplications(postingId: number): Promise { + const applications = await sql` + SELECT id, posting_id AS "postingId", user_id AS "userId", candidate_statement AS "candidateStatement", created_at AS "createdAt" + FROM applications + WHERE posting_id = ${postingId}; + `; + + applications.forEach((application) => { + application.createdAt = new Date(application.createdAt); + }); + + return applications; +} diff --git a/src/lib/index.server.ts b/src/lib/index.server.ts index 89c9675..79d748b 100644 --- a/src/lib/index.server.ts +++ b/src/lib/index.server.ts @@ -3,7 +3,7 @@ import path from 'path'; import fetch from 'node-fetch'; import { type Cookies, error } from '@sveltejs/kit'; import jwt from 'jsonwebtoken'; -import { getUserWithCompany } from '$lib/db/index.server'; +import type { User } from '$lib/types'; // TODO: Handle saving custom avatar uploads export async function saveAvatar(user: User): Promise { @@ -14,6 +14,7 @@ export async function saveAvatar(user: User): Promise { fs.writeFileSync(filePath, avatar); } +// TODO: change to return null instead of -1 export function getUserPerms(cookies: Cookies): number { if (process.env.JWT_SECRET === undefined) { throw new Error('JWT_SECRET not defined'); @@ -51,3 +52,22 @@ export function getUserId(cookies: Cookies): number { } error(403, 'Unauthorized'); } + +export function getUserCompanyId(cookies: Cookies): number | null { + if (process.env.JWT_SECRET === undefined) { + throw new Error('JWT_SECRET not defined'); + } + + const JWT = cookies.get('jwt'); + if (JWT) { + try { + const decoded = jwt.verify(JWT, process.env.JWT_SECRET); + if (typeof decoded === 'object' && 'companyId' in decoded) { + return decoded['companyId']; + } + } catch (err) { + return null; + } + } + error(403, 'Unauthorized'); +} diff --git a/src/lib/shared.server.ts b/src/lib/shared.server.ts new file mode 100644 index 0000000..3a70b53 --- /dev/null +++ b/src/lib/shared.server.ts @@ -0,0 +1,20 @@ +import type { Cookies } from '@sveltejs/kit'; +import jwt from 'jsonwebtoken'; +import type { User } from '$lib/types'; + +export function setJWT(cookies: Cookies, user: User) { + const payload = { + username: user.username, + perms: user.perms, + id: user.id, + companyId: user.companyId + }; + + if (process.env.JWT_SECRET === undefined) { + throw new Error('JWT_SECRET not defined'); + } + + const maxAge = 60 * 60 * 24 * 30; // 30 days + const JWT = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '30d' }); + cookies.set('jwt', JWT, { maxAge, path: '/', httpOnly: false }); +} diff --git a/src/lib/shared.svelte.ts b/src/lib/shared.svelte.ts index bb1cbd3..c32d728 100644 --- a/src/lib/shared.svelte.ts +++ b/src/lib/shared.svelte.ts @@ -1,6 +1,12 @@ import { PERMISSIONS } from '$lib/consts'; +import type { EmploymentType } from '$lib/types'; -export let userState = $state({ perms: PERMISSIONS.VIEW, username: null, id: null }); +export let userState = $state({ + perms: PERMISSIONS.VIEW, + username: null, + id: null, + companyId: null +}); export const userPerms = PERMISSIONS.VIEW | PERMISSIONS.APPLY_FOR_JOBS; export const employerPerms = PERMISSIONS.SUBMIT_POSTINGS | PERMISSIONS.MANAGE_EMPLOYERS; @@ -19,3 +25,28 @@ export function telFormatter(initial: string) { (num.length > 6 ? '-' + num.substring(6, 10) : ''); return initial; } + +export const getCookieValue = (name: String) => + document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || ''; + +export function updateUserState() { + const JWT = getCookieValue('jwt'); + if (JWT !== '') { + const state = JSON.parse(atob(JWT.split('.')[1])); + userState.perms = state.perms; + userState.username = state.username; + userState.id = state.id; + userState.companyId = state.companyId; + } +} + +export function employmentTypeDisplayName(type: EmploymentType) { + switch (type) { + case 'full_time': + return 'Full Time'; + case 'part_time': + return 'Part Time'; + case 'internship': + return 'Internship'; + } +} diff --git a/src/lib/types.ts b/src/lib/types.ts index be125e7..ac51ac0 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,4 +1,4 @@ -interface User { +export interface User { id: number | null; username: string; password: string | null; @@ -11,18 +11,58 @@ interface User { fullName: string | null; company: Company | null; companyCode: string | null; + companyId: number | null | undefined; } -interface Tag { +export interface Company { id: number; - displayName: string; + name: string | null; + description: string | null; + website: string | null; createdAt: Date | null; + companyCode: string | null; } -interface Company { +export interface Posting { id: number; - name: string; + title: string; description: string; - website: string; + employerId: number; + address: string; + employmentType: EmploymentType; + wage: string; + link: string; + tags: Tag[]; + tagIds: number[] | null | undefined; + createdAt: Date | null; + updatedAt: Date | null; + flyerLink: string | null; + company: Company; + companyId: number | null | undefined; + companyName: string | null | undefined; + employer: User | null | undefined; +} + +export interface Tag { + id: number; + displayName: string | null; createdAt: Date | null; } + +export enum EmploymentType { + full_time = 'full_time', + part_time = 'part_time', + internship = 'internship' + // contract = 'Contract', + // temporary = 'Temporary', + // seasonal = 'Seasonal' +} + +export interface Application { + id: number; + userId: number; + postingId: number; + postingTitle: string | null; + candidateStatement: string; + createdAt: Date; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 35e1f99..a7bcd83 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,9 +1,8 @@ -
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 23af1a1..56b0a9c 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,6 +1,7 @@ @@ -39,7 +49,9 @@ User avatar {/if} -
-
-
User Details
-
- Edit account - -
-
-
-
- ID: {data.user.id} -
-
- Account active: {data.user.active ? 'check' : 'cancel'} -
-
- Last sign-in: {data.user.lastSignIn?.toLocaleDateString('en-US', dateFormatOptions)} -
- {#if !data.user.company?.id} -
- Employer company: N/A +
+
+
+
User Details
+
+ Edit account +
- {/if} - {#if data.user.company?.id} +
+
- Employer company: -
- -
-
{data.user.company.name}
-
{data.user.company.description}
+ ID: {data.user.id} +
+
+ Account active: {data.user.active ? 'check' : 'cancel'} +
+
+ Last sign-in: {data.user.lastSignIn?.toLocaleDateString('en-US', dateFormatOptions)} +
+ {#if !data.user.company?.id} +
+ Employer company: N/A +
+ {/if} + {#if data.user.company?.id} +
+ Employer company: +
+ +
+
{data.user.company.name}
+
{data.user.company.description}
+
+ {/if} +
+ Permissions: {data.user.perms}
- {/if} -
- Permissions: {data.user.perms}
+ {#if data.applications} +
+
Pending applications
+ {#each data.applications as application} +
+
+
+ Applied to: {application.postingTitle} +
+
+ Applied on: {data.applications[0].createdAt.toLocaleDateString( + 'en-US', + dateFormatOptions + )} +
+
+ +
+ {/each} +
+ {/if}
+ diff --git a/src/routes/account/settings/+page.server.ts b/src/routes/account/settings/+page.server.ts index 87eea94..5f96554 100644 --- a/src/routes/account/settings/+page.server.ts +++ b/src/routes/account/settings/+page.server.ts @@ -3,6 +3,7 @@ import { deleteUser, getUser, getUserWithCompany, updateUser } from '$lib/db/ind import { type Actions, fail, redirect } from '@sveltejs/kit'; import { PERMISSIONS } from '$lib/consts'; import { getUserId } from '$lib/index.server'; +import type { User } from '$lib/types'; export const load: PageServerLoad = async ({ cookies }) => { const id = getUserId(cookies); @@ -23,9 +24,7 @@ export const actions: Actions = { .toUpperCase() .trim(); - if (email === '' || email == undefined) email = null; if (phone === '' || phone == undefined) phone = null; - if (fullName === '' || fullName == undefined) fullName = null; if (companyCode === '' || companyCode == undefined) companyCode = null; if (email && !email.includes('@')) { @@ -36,7 +35,7 @@ export const actions: Actions = { return fail(400, { errorMessage: 'Invalid phone number' }); } - if (username && username !== '') { + if (username && username !== '' && fullName && fullName !== '' && email && email !== '') { try { await updateUser({ id: id, diff --git a/src/routes/account/settings/+page.svelte b/src/routes/account/settings/+page.svelte index acf4010..366eec4 100644 --- a/src/routes/account/settings/+page.svelte +++ b/src/routes/account/settings/+page.svelte @@ -6,6 +6,10 @@ let permsAccordions: boolean[] = [false, false, false]; onMount(() => { + if (!document.cookie.includes('jwt=')) { + window.location.href = '/signin'; + } + let acc = document.getElementsByClassName('accordion'); for (let i = 0; i < acc.length; i++) { acc[i].addEventListener('click', function (this: HTMLElement, event: Event) { @@ -89,7 +93,7 @@
- Username + Username *
- Email (optional) + Full name * + +
+
+ Email *
@@ -122,17 +139,6 @@ pattern="([0-9]\{3}) [0-9]\{3}-[0-9]\{3}" />
-
- Full name (optional) - -
Company code (optional) - import '../../app.css'; import { page } from '$app/state'; import { userState } from '$lib/shared.svelte'; import { PERMISSIONS } from '$lib/consts'; @@ -8,7 +7,7 @@
- {#if (userState.perms & PERMISSIONS.MANAGE_POSTINGS) !== 0} + {#if (userState.perms & PERMISSIONS.MANAGE_POSTINGS) > 0} work Postings {/if} - {#if (userState.perms & PERMISSIONS.MANAGE_USERS) !== 0} + {#if (userState.perms & PERMISSIONS.MANAGE_USERS) > 0} group Users {/if} - {#if (userState.perms & PERMISSIONS.MANAGE_TAGS) !== 0} - sell Tags - {/if} - {#if (userState.perms & PERMISSIONS.MANAGE_COMPANIES) !== 0} + + + + + + + + + + + {#if (userState.perms & PERMISSIONS.MANAGE_COMPANIES) > 0} + let { data } = $props(); + + const dateFormatOptions: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'short', + day: 'numeric' + }; + + +
+
+ + +
+ + + +
+ +
+ + + + + + + + + + + + {#if data.companies !== undefined} + {#each data.companies as company} + + + + + + + + {/each} + {/if} + +
IDNameWebsiteCreated
{company.id}{company.name}{company.website}{company.createdAt?.toLocaleDateString('en-US', dateFormatOptions) || + 'unknown'} + storeView company + editEdit company +
+
+
+
diff --git a/src/routes/admin/companies/create/+page.svelte b/src/routes/admin/companies/create/+page.svelte deleted file mode 100644 index e69de29..0000000 diff --git a/src/routes/admin/companies/edit/+page.svelte b/src/routes/admin/companies/edit/+page.svelte deleted file mode 100644 index e69de29..0000000 diff --git a/src/routes/admin/postings/+page.server.ts b/src/routes/admin/postings/+page.server.ts new file mode 100644 index 0000000..d9b1b19 --- /dev/null +++ b/src/routes/admin/postings/+page.server.ts @@ -0,0 +1,16 @@ +import type { PageServerLoad } from './$types'; +import { getPostings, getUsers } from '$lib/db/index.server'; +import { PERMISSIONS } from '$lib/consts'; +import { error } from '@sveltejs/kit'; +import { getUserPerms } from '$lib/index.server'; + +export const load: PageServerLoad = async ({ cookies, url }) => { + const search = url.searchParams.get('searchUsers'); + const perms = getUserPerms(cookies); + if (perms >= 0 && (perms & PERMISSIONS.MANAGE_POSTINGS) > 0) { + return { + postings: await getPostings(search) + }; + } + error(403, 'Unauthorized'); +}; diff --git a/src/routes/admin/postings/+page.svelte b/src/routes/admin/postings/+page.svelte index e69de29..f0d2217 100644 --- a/src/routes/admin/postings/+page.svelte +++ b/src/routes/admin/postings/+page.svelte @@ -0,0 +1,93 @@ + + +
+
+
+
+ Posting Management (Total: {data.postings?.length || 0}) +
+ Create new posting +
+
+
+ + + +
+
+
+ + + + + + + + + + + + + + {#if data.postings !== undefined} + {#each data.postings as posting} + + + + + + + + + + {/each} + {/if} + +
IDTitleCompanyCreatedUpdatedEmployment Type
{posting.id}{posting.title}{posting.company.name || 'unknown'}{posting.createdAt?.toLocaleDateString('en-US', dateFormatOptions) || + 'unknown'}{posting.updatedAt?.toLocaleDateString('en-US', dateFormatOptions) || + 'unknown'}{employmentTypeDisplayName(posting.employmentType)} + workView posting + editEdit posting +
+
+
+
diff --git a/src/routes/admin/tags/+page.svelte b/src/routes/admin/tags/+page.svelte index 3a2ea02..832ead6 100644 --- a/src/routes/admin/tags/+page.svelte +++ b/src/routes/admin/tags/+page.svelte @@ -59,11 +59,11 @@ {tag.createdAt?.toLocaleDateString('en-US', dateFormatOptions)} info edit diff --git a/src/routes/admin/users/+page.svelte b/src/routes/admin/users/+page.svelte index c937201..8f7bacf 100644 --- a/src/routes/admin/users/+page.svelte +++ b/src/routes/admin/users/+page.svelte @@ -75,17 +75,17 @@ >{user.lastSignIn?.toLocaleDateString('en-US', dateFormatOptions) || 'unknown'} - + {user.active ? 'check' : 'close'} personView account editEdit account diff --git a/src/routes/admin/users/[user]/+page.svelte b/src/routes/admin/users/[user]/+page.svelte index 8dd2838..a36c59e 100644 --- a/src/routes/admin/users/[user]/+page.svelte +++ b/src/routes/admin/users/[user]/+page.svelte @@ -110,36 +110,36 @@
Permissions: {data.user.perms}
- {#if (data.user.perms & userPerms) !== 0} + {#if (data.user.perms & userPerms) > 0}

User permissions:

- {#if (data.user.perms & PERMISSIONS.VIEW) !== 0} + {#if (data.user.perms & PERMISSIONS.VIEW) > 0}

View access

{/if} - {#if (data.user.perms & PERMISSIONS.APPLY_FOR_JOBS) !== 0} + {#if (data.user.perms & PERMISSIONS.APPLY_FOR_JOBS) > 0}

Apply for jobs

{/if} {/if} - {#if (data.user.perms & employerPerms) !== 0} + {#if (data.user.perms & employerPerms) > 0}

Employer permissions:

- {#if (data.user.perms & PERMISSIONS.SUBMIT_POSTINGS) !== 0} + {#if (data.user.perms & PERMISSIONS.SUBMIT_POSTINGS) > 0}

Submit postings

{/if} - {#if (data.user.perms & PERMISSIONS.MANAGE_EMPLOYERS) !== 0} + {#if (data.user.perms & PERMISSIONS.MANAGE_EMPLOYERS) > 0}

Manage employers (of their company)

{/if} {/if} - {#if (data.user.perms & adminPerms) !== 0} + {#if (data.user.perms & adminPerms) > 0}

Admin permissions:

- {#if (data.user.perms & PERMISSIONS.MANAGE_POSTINGS) !== 0} + {#if (data.user.perms & PERMISSIONS.MANAGE_POSTINGS) > 0}

Manage postings

{/if} - {#if (data.user.perms & PERMISSIONS.MANAGE_USERS) !== 0} + {#if (data.user.perms & PERMISSIONS.MANAGE_USERS) > 0}

Manage users

{/if} - {#if (data.user.perms & PERMISSIONS.MANAGE_TAGS) !== 0} + {#if (data.user.perms & PERMISSIONS.MANAGE_TAGS) > 0}

Manage tags

{/if} - {#if (data.user.perms & PERMISSIONS.MANAGE_COMPANIES) !== 0} + {#if (data.user.perms & PERMISSIONS.MANAGE_COMPANIES) > 0}

Manage companies

{/if} {/if} diff --git a/src/routes/admin/users/[user]/edit/+page.server.ts b/src/routes/admin/users/[user]/edit/+page.server.ts index ce5ab79..f86fec0 100644 --- a/src/routes/admin/users/[user]/edit/+page.server.ts +++ b/src/routes/admin/users/[user]/edit/+page.server.ts @@ -3,7 +3,8 @@ import { deleteUser, getUser, updateUser } from '$lib/db/index.server'; import { PERMISSIONS } from '$lib/consts'; import type { PageServerLoad } from './$types'; import { getUserPerms } from '$lib/index.server'; -import { userPerms } from '$lib/shared.svelte'; +import { userPerms, employerPerms } from '$lib/shared.svelte'; +import type { User } from '$lib/types'; export const load: PageServerLoad = async ({ cookies, params }) => { const id = parseInt(params.user); @@ -66,34 +67,38 @@ export const actions: Actions = { const requestPerms = getUserPerms(cookies); if (!(requestPerms >= 0 && (requestPerms & PERMISSIONS.MANAGE_USERS) > 0)) { return fail(403, { errorMessage: 'Unauthorized' }); - } else { - if (((requestPerms | userPerms) & newUserPerms) !== newUserPerms) { - return fail(403, { - errorMessage: 'Cannot give a user higher permissions than yourself!' - }); - } else { - if (username && username !== '') { - try { - await updateUser({ - id: id, - username: username, - password: password, - perms: newUserPerms, - active: accountActive === 'on', - email: email, - phone: phone, - fullName: fullName, - companyCode: companyCode - }); - } catch (err) { - return fail(500, { errorMessage: `Internal Server Error: ${err}` }); - } - return redirect(301, `/admin/users/${id}`); - } else { - return fail(400, { errorMessage: 'Missing username or password' }); - } - } } + if (((requestPerms | userPerms | employerPerms) & newUserPerms) !== newUserPerms) { + return fail(403, { + errorMessage: 'Cannot give a user higher permissions than yourself!' + }); + } + if (!username) { + return fail(400, { errorMessage: 'Missing username' }); + } + if (password && password.length < 8) { + return fail(400, { errorMessage: 'Password must be at least 8 characters' }); + } + if (username.length < 4) { + return fail(400, { errorMessage: 'Username must be at least 4 characters' }); + } + + try { + await updateUser({ + id: id, + username: username, + password: password, + perms: newUserPerms, + active: accountActive === 'on', + email: email, + phone: phone, + fullName: fullName, + companyCode: companyCode + }); + } catch (err) { + return fail(500, { errorMessage: `Internal Server Error: ${err}` }); + } + return redirect(301, `/admin/users/${id}`); }, delete: async ({ cookies, params }) => { const id = parseInt(params.user!); diff --git a/src/routes/admin/users/[user]/edit/+page.svelte b/src/routes/admin/users/[user]/edit/+page.svelte index fb4e516..2745ace 100644 --- a/src/routes/admin/users/[user]/edit/+page.svelte +++ b/src/routes/admin/users/[user]/edit/+page.svelte @@ -100,7 +100,9 @@
-
Update User
+
+ Edit User {data.user.username}{data.user.fullName ? ` (${data.user.fullName})` : ''} +
@@ -112,10 +114,11 @@ value={data.user?.username} placeholder="Username" class="w-full rounded font-normal" + required />
- Password + New password (optional)
- Email (optional) + Full name * + +
+
+ Email *
@@ -156,17 +172,6 @@ pattern="([0-9]\{3}) [0-9]\{3}-[0-9]\{3}" />
-
- Full name (optional) - -
Company code (optional) 0} />User Permissions @@ -212,7 +217,7 @@ name="view" id="view" class="permCheckbox mx-1" - checked={(perms & PERMISSIONS.VIEW) !== 0} + checked={(perms & PERMISSIONS.VIEW) > 0} /> View access @@ -224,7 +229,7 @@ name="apply" id="apply" class="permCheckbox mx-1" - checked={(perms & PERMISSIONS.APPLY_FOR_JOBS) !== 0} + checked={(perms & PERMISSIONS.APPLY_FOR_JOBS) > 0} /> Apply for jobs @@ -246,7 +251,7 @@ class="select-all" checked={(perms & employerPerms) === employerPerms} indeterminate={(perms & employerPerms) !== employerPerms && - (perms & employerPerms) !== 0} + (perms & employerPerms) > 0} />Company Permissions @@ -262,7 +267,7 @@ type="checkbox" name="submitPostings" id="submitPostings" - checked={(perms & PERMISSIONS.SUBMIT_POSTINGS) !== 0} + checked={(perms & PERMISSIONS.SUBMIT_POSTINGS) >= 0} class="permCheckbox mx-1" /> Submit postings 0} class="permCheckbox mx-1" /> Manage employers (within their company) 0} />Admin Permissions @@ -312,7 +317,7 @@ type="checkbox" name="manageTags" id="manageTags" - checked={(perms & PERMISSIONS.MANAGE_TAGS) !== 0} + checked={(perms & PERMISSIONS.MANAGE_TAGS) > 0} class="permCheckbox mx-1" /> Manage tags 0} class="permCheckbox mx-1" /> Manage postings 0} class="permCheckbox mx-1" /> Manage users 0} class="permCheckbox mx-1" /> Manage companiesUpdate userSave user
- Email (optional) + Full name * + +
+
+ Email *
@@ -138,16 +150,6 @@ pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}" />
-
- Full name (optional) - -
Company code (optional) { + return { companies: await getCompanies() }; +}; diff --git a/src/routes/companies/+page.svelte b/src/routes/companies/+page.svelte new file mode 100644 index 0000000..de18187 --- /dev/null +++ b/src/routes/companies/+page.svelte @@ -0,0 +1,35 @@ + + + diff --git a/src/routes/companies/[company]/+page.server.ts b/src/routes/companies/[company]/+page.server.ts new file mode 100644 index 0000000..bd26eba --- /dev/null +++ b/src/routes/companies/[company]/+page.server.ts @@ -0,0 +1,7 @@ +import type { PageServerLoad } from './$types'; +import { getCompanyFullData } from '$lib/db/index.server'; + +export const load: PageServerLoad = async ({ params }) => { + const id = parseInt(params.company!); + return await getCompanyFullData(id); +}; diff --git a/src/routes/companies/[company]/+page.svelte b/src/routes/companies/[company]/+page.svelte new file mode 100644 index 0000000..a55beba --- /dev/null +++ b/src/routes/companies/[company]/+page.svelte @@ -0,0 +1,109 @@ + + +
+
+
+
+ User avatar +
+

{data.company.name}

+

{data.company.description}

+
+
+ {#if (userState.perms & PERMISSIONS.MANAGE_COMPANIES) > 0 || ((userState.perms & PERMISSIONS.MANAGE_EMPLOYERS) !== 0 && userState.companyId === data.company.id)} + + {/if} +
+
+
+
+ {data.postings + ? data.company.name + "'s Postings" + : data.company.name + ' has no current postings!'} +
+ {#each data.postings as posting} + + Company Logo +
+

{posting.title}

+

{posting.description}

+
+
+ {/each} +
+
+
+
Employers
+
+ {#each data.users as user} +
+
+ User avatar avatarFallback(e, user)} + height="32" + width="32" + /> +
+
+ {user.username}{user.fullName ? ` (${user.fullName})` : ''} +
+ {#if user.email} +
+ mail + {user.email} +
+ {/if} + {#if user.phone} +
+ call + {user.phone} +
+ {/if} +
+
+
+ {/each} +
+
+
+
diff --git a/src/routes/companies/[company]/edit/+layout.svelte b/src/routes/companies/[company]/edit/+layout.svelte new file mode 100644 index 0000000..7c10af9 --- /dev/null +++ b/src/routes/companies/[company]/edit/+layout.svelte @@ -0,0 +1,27 @@ + + + + +
+ {@render children()} +
diff --git a/src/routes/companies/[company]/edit/+layout.ts b/src/routes/companies/[company]/edit/+layout.ts new file mode 100644 index 0000000..2bd9a6a --- /dev/null +++ b/src/routes/companies/[company]/edit/+layout.ts @@ -0,0 +1,5 @@ +import type { LayoutLoad } from './$types'; + +export const load: LayoutLoad = async ({ params }) => { + return { id: parseInt(params.company) }; +}; diff --git a/src/routes/companies/[company]/edit/+page.server.ts b/src/routes/companies/[company]/edit/+page.server.ts new file mode 100644 index 0000000..a126beb --- /dev/null +++ b/src/routes/companies/[company]/edit/+page.server.ts @@ -0,0 +1,70 @@ +import { type Actions, error, fail, redirect } from '@sveltejs/kit'; +import { deleteCompany, editCompany, getCompany } from '$lib/db/index.server'; +import { PERMISSIONS } from '$lib/consts'; +import { getUserCompanyId, getUserPerms } from '$lib/index.server'; +import type { PageServerLoad } from './$types'; +import type { Company } from '$lib/types'; + +export const load: PageServerLoad = async ({ cookies, params }) => { + const id = parseInt(params.company); + const perms = getUserPerms(cookies); + if (perms >= 0 && (perms & PERMISSIONS.MANAGE_COMPANIES) > 0) { + return { + company: await getCompany(id) + }; + } + error(403, 'Unauthorized'); +}; + +export const actions: Actions = { + submit: async ({ request, cookies, params }) => { + const id = parseInt(params.company!); + const data = await request.formData(); + const name = data.get('name')?.toString().trim(); + let website = data.get('website')?.toString().trim(); + const description = data.get('description')?.toString().trim(); + + const requestPerms = getUserPerms(cookies); + if ( + !( + requestPerms >= 0 && + ((requestPerms & PERMISSIONS.MANAGE_COMPANIES) > 0 || + ((requestPerms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && getUserCompanyId(cookies) === id)) + ) + ) { + return fail(403, { errorMessage: 'You cannot preform this action!' }); + } + if (!name || name === '' || !website || website === '' || !description || description === '') { + return fail(400, { errorMessage: 'All fields are required' }); + } + if (!website.includes('.')) { + return fail(400, { errorMessage: 'Invalid website' }); + } + if (!website.startsWith('http://') && !website.startsWith('https://')) + website = `https://${website}`; + + try { + await editCompany({ + id: id, + name: name, + website: website, + description: description + }); + } catch (err) { + return fail(500, { errorMessage: `Internal Server Error: ${err}` }); + } + redirect(301, `/companies/${id}`); + }, + delete: async ({ cookies, params }) => { + const id = parseInt(params.company!); + const requestPerms = getUserPerms(cookies); + if (!(requestPerms >= 0 && (requestPerms & PERMISSIONS.MANAGE_COMPANIES) > 0)) { + return fail(403, { errorMessage: 'You cannot preform this action!' }); + } + try { + await deleteCompany(id); + } catch (err) { + return fail(500, { errorMessage: `Internal Server Error: ${err}` }); + } + } +}; diff --git a/src/routes/companies/[company]/edit/+page.svelte b/src/routes/companies/[company]/edit/+page.svelte new file mode 100644 index 0000000..6dc8653 --- /dev/null +++ b/src/routes/companies/[company]/edit/+page.svelte @@ -0,0 +1,108 @@ + + +
+
+
+
+
Edit Company {data.company.name}
+
+ +
+ Name * + +
+
+ Description * + +
+
+ Website * + +
+ + {#if form?.errorMessage} +
{form.errorMessage}
+ {/if} +
+ + +
+ + +
+
+
diff --git a/src/routes/companies/[company]/edit/employers/+page.server.ts b/src/routes/companies/[company]/edit/employers/+page.server.ts new file mode 100644 index 0000000..d65de8e --- /dev/null +++ b/src/routes/companies/[company]/edit/employers/+page.server.ts @@ -0,0 +1,56 @@ +import { type Actions, error, fail, redirect } from '@sveltejs/kit'; +import { + addEmployerToCompany, + deleteCompany, + editCompany, + getCompany, + getCompanyEmployers, + removeEmployerFromCompany +} from '$lib/db/index.server'; +import { PERMISSIONS } from '$lib/consts'; +import { getUserCompanyId, getUserPerms } from '$lib/index.server'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, params }) => { + const id = parseInt(params.company); + const perms = getUserPerms(cookies); + if (perms >= 0 && (perms & PERMISSIONS.MANAGE_COMPANIES) > 0) { + return await getCompanyEmployers(id); + } + error(403, 'Unauthorized'); +}; + +export const actions: Actions = { + removeEmployer: async ({ params, url, cookies }) => { + const id = parseInt(params.company!); + const data = url.searchParams; + + const requestPerms = getUserPerms(cookies); + if ( + !( + (requestPerms >= 0 && (requestPerms & PERMISSIONS.MANAGE_POSTINGS) > 0) || + ((requestPerms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && getUserCompanyId(cookies) === id) + ) + ) { + return fail(403, { errorMessage: 'You cannot preform this action!' }); + } + + await removeEmployerFromCompany(id, parseInt(data.get('userId')!)); + }, + addEmployer: async ({ params, url, cookies }) => { + const id = parseInt(params.company!); + const data = url.searchParams; + + const requestPerms = getUserPerms(cookies); + if ( + !( + (requestPerms >= 0 && (requestPerms & PERMISSIONS.MANAGE_POSTINGS) > 0) || + ((requestPerms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && getUserCompanyId(cookies) === id) + ) + ) { + return fail(403, { errorMessage: 'You cannot preform this action!' }); + } + + await addEmployerToCompany(id, parseInt(data.get('userId')!)); + } +}; diff --git a/src/routes/companies/[company]/edit/employers/+page.svelte b/src/routes/companies/[company]/edit/employers/+page.svelte new file mode 100644 index 0000000..a865fb0 --- /dev/null +++ b/src/routes/companies/[company]/edit/employers/+page.svelte @@ -0,0 +1,174 @@ + + +
+
+ User avatar +
+

{data.company.name}

+

Company code: {data.company.companyCode}

+
+
+
+
+
Current Employers
+
+
+ + + + + + + + + + + + + + {#if data.users !== undefined} + {#each data.users as user} + {#if user.company?.id === data.id} + + + + + + + + + + {/if} + {/each} + {/if} + +
IDUsernameFull NameEmailCreatedLast Sign-InRemove
{user.id}{user.username}{user.fullName || 'N/A'}{user.email || 'N/A'}{user.createdAt?.toLocaleDateString('en-US', dateFormatOptions) || + 'unknown'}{user.lastSignIn?.toLocaleDateString('en-US', dateFormatOptions) || + 'unknown'}
+
+
+
+{#if idToRemove !== null} + +{/if} +{#if data.users && data.users.some((user) => { + return user.company?.id !== data.id; + })} +
+
+
+
Pending requests
+
+
+ + + + + + + + + + + + + + {#if data.users !== undefined} + {#each data.users as user} + {#if user.company?.id !== data.id} + + + + + + + + + + {/if} + {/each} + {/if} + +
IDUsernameFull NameEmailCreatedLast Sign-InApprove
{user.id}{user.username}{user.fullName || 'N/A'}{user.email || 'N/A'}{user.createdAt?.toLocaleDateString('en-US', dateFormatOptions) || + 'unknown'}{user.lastSignIn?.toLocaleDateString('en-US', dateFormatOptions) || + 'unknown'}
+ + +
+
+
+
+{/if} diff --git a/src/routes/companies/create/+page.server.ts b/src/routes/companies/create/+page.server.ts new file mode 100644 index 0000000..b55ade6 --- /dev/null +++ b/src/routes/companies/create/+page.server.ts @@ -0,0 +1,45 @@ +import { type Actions, fail, redirect } from '@sveltejs/kit'; +import { createCompany } from '$lib/db/index.server'; +import { PERMISSIONS } from '$lib/consts'; +import { getUserCompanyId, getUserPerms } from '$lib/index.server'; +import type { Company } from '$lib/types'; + +export const actions: Actions = { + submit: async ({ request, cookies }) => { + const data = await request.formData(); + const name = data.get('name')?.toString().trim(); + let website = data.get('website')?.toString().trim(); + const description = data.get('description')?.toString().trim(); + + const requestPerms = getUserPerms(cookies); + if ( + !( + requestPerms >= 0 && + ((requestPerms & PERMISSIONS.MANAGE_COMPANIES) > 0 || + ((requestPerms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && getUserCompanyId(cookies))) + ) + ) { + return fail(403, { errorMessage: 'You cannot preform this action!' }); + } + if (!name || name === '' || !website || website === '' || !description || description === '') { + return fail(400, { errorMessage: 'All fields are required' }); + } + if (!website.includes('.')) { + return fail(400, { errorMessage: 'Invalid website' }); + } + if (!website.startsWith('http://') && !website.startsWith('https://')) + website = `https://${website}`; + + let id: number; + try { + id = await createCompany({ + name: name, + website: website, + description: description + }); + } catch (err) { + return fail(500, { errorMessage: `Internal Server Error: ${err}` }); + } + redirect(301, `/companies/${id}`); + } +}; diff --git a/src/routes/companies/create/+page.svelte b/src/routes/companies/create/+page.svelte new file mode 100644 index 0000000..dab796b --- /dev/null +++ b/src/routes/companies/create/+page.svelte @@ -0,0 +1,60 @@ + + +
+
+
+
+
Create new company
+
+
+
+ Name * + +
+
+ Description * + +
+
+ Website * + +
+ + {#if form?.errorMessage} +
{form.errorMessage}
+ {/if} + +
+
+
+
diff --git a/src/routes/info/+page.svelte b/src/routes/info/+page.svelte new file mode 100644 index 0000000..f7f92b3 --- /dev/null +++ b/src/routes/info/+page.svelte @@ -0,0 +1,23 @@ +
+
+

Info

+

This page contains additional info about different parts of the app

+

Company Codes

+

+ Company codes are unique identifiers of companies in order for an employer to associate + themselves to a company. +

+

If you are an applicant, you can safely ignore them.

+

+ If you are an employer, get a code from your company admin, and input it into your account + page to request access to your company. Once approved by your administrator, you will have + access to create job postings. +

+

+ If you are your company admin, first create your account (without inputting a code). Then, go + to the company page, and use the button in the top right to create a new company. Once created + and approved by a CareerConnect admin, you will be able to see the company code, which you can + then give to your employees. +

+
+
diff --git a/src/routes/listings/+page.svelte b/src/routes/listings/+page.svelte deleted file mode 100644 index e69de29..0000000 diff --git a/src/routes/postings/+page.server.ts b/src/routes/postings/+page.server.ts new file mode 100644 index 0000000..a9a2ce7 --- /dev/null +++ b/src/routes/postings/+page.server.ts @@ -0,0 +1,8 @@ +import type { PageServerLoad } from './$types'; +import { getPostings } from '$lib/db/index.server'; + +export const load: PageServerLoad = async ({ url }) => { + return { + postings: await getPostings(url.searchParams.get('searchQuery') as string) + }; +}; diff --git a/src/routes/postings/+page.svelte b/src/routes/postings/+page.svelte new file mode 100644 index 0000000..d5144f3 --- /dev/null +++ b/src/routes/postings/+page.svelte @@ -0,0 +1,140 @@ + + +
+
+
+ {#each data.postings as posting} + + {/each} +
+ {#if details !== undefined} +
+
+
+ Company Logo logoFallback(e, details)} + /> +
+

{details.title}

+

Company: {details.company.name}

+
+
+ {#if userState.perms >= 0 && ((userState.perms & PERMISSIONS.MANAGE_POSTINGS) > 0 || ((userState.perms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && userState.companyId === details.company.id))} + Manage posting + {:else if (userState.perms & PERMISSIONS.APPLY_FOR_JOBS) > 0} + Apply + {/if} +
+
+

Contact

+

{details.employer?.fullName} ({details.employer?.username})

+ {details.employer?.email} + {details.employer?.phone} +

Details

+ {#if details.employmentType} +

{employmentTypeDisplayName(details.employmentType)}

+ {/if} + {#if details.address} + Address: {details.address} + {/if} + {#if details.wage} +

Wage: {details.wage}

+ {/if} + {#if details.createdAt} +

Posted: {details.createdAt.toLocaleDateString('en-US', dateFormatOptions)}

+ {/if} + {#if details.link} + More information: {details.link} + {/if} + {#if details.flyerLink} + Flyer: {details.flyerLink} + {/if} +

Job Description

+

{details.description}

+
+
+ {/if} +
+
diff --git a/src/routes/postings/[posting]/+page.server.ts b/src/routes/postings/[posting]/+page.server.ts new file mode 100644 index 0000000..0fcba73 --- /dev/null +++ b/src/routes/postings/[posting]/+page.server.ts @@ -0,0 +1,8 @@ +import type { PageServerLoad } from './$types'; +import { getPostingFullData, getPostings } from '$lib/db/index.server'; + +export const load: PageServerLoad = async ({ params }) => { + return { + posting: await getPostingFullData(parseInt(params.posting)) + }; +}; diff --git a/src/routes/postings/[posting]/+page.svelte b/src/routes/postings/[posting]/+page.svelte new file mode 100644 index 0000000..b6c7274 --- /dev/null +++ b/src/routes/postings/[posting]/+page.svelte @@ -0,0 +1,95 @@ + + +
+
+
+
+
+ Company Logo logoFallback(e, data.posting)} + /> +
+

{data.posting.title}

+

Company: {data.posting.company.name}

+
+
+ {#if userState.perms >= 0 && ((userState.perms & PERMISSIONS.MANAGE_POSTINGS) > 0 || ((userState.perms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && userState.companyId === details.company.id))} + Manage posting + {:else if (userState.perms & PERMISSIONS.APPLY_FOR_JOBS) > 0} + Apply + {/if} +
+
+

Contact

+

{data.posting.employer?.fullName} ({data.posting.employer?.username})

+ {data.posting.employer?.email} + {data.posting.employer?.phone} +

Details

+ {#if data.posting.employmentType} +

{employmentTypeDisplayName(data.posting.employmentType)}

+ {/if} + {#if data.posting.address} + Address: {data.posting.address} + {/if} + {#if data.posting.wage} +

Wage: {data.posting.wage}

+ {/if} + {#if data.posting.createdAt} +

Posted: {data.posting.createdAt.toLocaleDateString('en-US', dateFormatOptions)}

+ {/if} + {#if data.posting.link} + More information: {data.posting.link} + {/if} + {#if data.posting.flyerLink} + Flyer: {data.posting.flyerLink} + {/if} +

Job Description

+

{data.posting.description}

+
+
+
+
diff --git a/src/routes/postings/[posting]/apply/+page.server.ts b/src/routes/postings/[posting]/apply/+page.server.ts new file mode 100644 index 0000000..e57e57b --- /dev/null +++ b/src/routes/postings/[posting]/apply/+page.server.ts @@ -0,0 +1,37 @@ +import { type Actions, error, fail, redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import { createApplication, getPostingFullData, getUserWithCompany } from '$lib/db/index.server'; +import { getUserId, getUserPerms } from '$lib/index.server'; +import { PERMISSIONS } from '$lib/consts'; +import type { Application } from '$lib/types'; + +export const load: PageServerLoad = async ({ params, cookies }) => { + const id = parseInt(params.posting); + const perms = getUserPerms(cookies); + if (perms >= 0 && (perms & PERMISSIONS.MANAGE_USERS) > 0) { + return { + posting: await getPostingFullData(id) + }; + } + error(403, 'Unauthorized'); +}; + +export const actions: Actions = { + submit: async ({ request, cookies, params }) => { + if (!((getUserPerms(cookies) & PERMISSIONS.APPLY_FOR_JOBS) > 0)) { + return fail(403, { errorMessage: 'Unauthorized' }); + } + + const data = await request.formData(); + const candidateStatement = data.get('candidateStatement')?.toString().trim(); + + const postingId = parseInt(params.posting!); + const userId = getUserId(cookies); + + if (!candidateStatement || candidateStatement === '') { + return fail(400, { errorMessage: 'Candidate statement is required' }); + } + await createApplication({ userId, postingId, candidateStatement }); + redirect(301, `/postings`); + } +}; diff --git a/src/routes/postings/[posting]/apply/+page.svelte b/src/routes/postings/[posting]/apply/+page.svelte new file mode 100644 index 0000000..f402912 --- /dev/null +++ b/src/routes/postings/[posting]/apply/+page.svelte @@ -0,0 +1,116 @@ + + +
+
+
+
+
+ Company Logo logoFallback(e, data.posting)} + /> +
+

{data.posting.title}

+

Company: {data.posting.company.name}

+
+
+
+
+

Contact

+

{data.posting.employer?.fullName} ({data.posting.employer?.username})

+ {data.posting.employer?.email} + {data.posting.employer?.phone} +

Details

+ {#if data.posting.employmentType} +

{employmentTypeDisplayName(data.posting.employmentType)}

+ {/if} + {#if data.posting.address} + Address: {data.posting.address} + {/if} + {#if data.posting.wage} +

Wage: {data.posting.wage}

+ {/if} + {#if data.posting.createdAt} +

Posted: {data.posting.createdAt.toLocaleDateString('en-US', dateFormatOptions)}

+ {/if} + {#if data.posting.link} + More information: {data.posting.link} + {/if} + {#if data.posting.flyerLink} + Flyer: {data.posting.flyerLink} + {/if} +

Job Description

+

{data.posting.description}

+
+
+
+
+
Apply
+
+
+
+ Why do you believe you are the best fit for this role? * + +
+

+ Your account information will be submitted along with this application. If there is any + other information you would like the employer to know, please add it in the box above. +

+ + {#if form?.errorMessage} +
{form.errorMessage}
+ {/if} + +
+
+
+
diff --git a/src/routes/postings/[posting]/manage/+layout.svelte b/src/routes/postings/[posting]/manage/+layout.svelte new file mode 100644 index 0000000..f207e07 --- /dev/null +++ b/src/routes/postings/[posting]/manage/+layout.svelte @@ -0,0 +1,27 @@ + + + + +
+ {@render children()} +
diff --git a/src/routes/postings/[posting]/manage/+page.svelte b/src/routes/postings/[posting]/manage/+page.svelte new file mode 100644 index 0000000..f6738c8 --- /dev/null +++ b/src/routes/postings/[posting]/manage/+page.svelte @@ -0,0 +1,7 @@ + diff --git a/src/routes/admin/companies/[company]/+page.svelte b/src/routes/postings/[posting]/manage/applications/+page.svelte similarity index 100% rename from src/routes/admin/companies/[company]/+page.svelte rename to src/routes/postings/[posting]/manage/applications/+page.svelte diff --git a/src/routes/postings/[posting]/manage/edit/+page.server.ts b/src/routes/postings/[posting]/manage/edit/+page.server.ts new file mode 100644 index 0000000..a6a750b --- /dev/null +++ b/src/routes/postings/[posting]/manage/edit/+page.server.ts @@ -0,0 +1,123 @@ +import { type Actions, error, fail, redirect } from '@sveltejs/kit'; +import { deletePosting, editPosting, getPosting, getPostings, getUser } from '$lib/db/index.server'; +import { PERMISSIONS } from '$lib/consts'; +import { getUserCompanyId, getUserId, getUserPerms } from '$lib/index.server'; +import type { Posting } from '$lib/types'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, params }) => { + const id = parseInt(params.posting); + const perms = getUserPerms(cookies); + + if ( + perms >= 0 && + ((perms & PERMISSIONS.MANAGE_POSTINGS) > 0 || + ((perms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && getUserCompanyId(cookies) === id)) + ) { + return { + posting: await getPosting(id) + }; + } + error(403, 'Unauthorized'); +}; + +export const actions: Actions = { + submit: async ({ request, cookies, params }) => { + const id = parseInt(params.posting!); + const employerId = getUserId(cookies); + + const data = await request.formData(); + let companyId: number | null | undefined = getUserCompanyId(cookies); + if (!companyId) { + if ( + data.get('companyId')?.toString().trim() === undefined || + data.get('companyId')?.toString().trim() === null || + data.get('companyId')?.toString().trim() === '' + ) { + return fail(400, { errorMessage: 'Company ID is required' }); + } else { + companyId = parseInt(data.get('companyId')?.toString().trim()!); + } + } + + const title = data.get('title')?.toString().trim(); + const description = data.get('description')?.toString().trim(); + const address = data.get('address')?.toString().trim(); + const employmentType = data.get('employmentType')?.toString().trim(); + const wage = data.get('wage')?.toString().trim(); + let link = data.get('link')?.toString().trim(); + const tagIds = data.get('tagIds')?.toString().trim() || null; + let flyerLink = data.get('flyerLink')?.toString().trim(); + + if (link && !link?.includes('.')) { + return fail(400, { errorMessage: 'Invalid link' }); + } + if (flyerLink && !flyerLink?.includes('.')) { + return fail(400, { errorMessage: 'Invalid flyer link' }); + } + if (link && !link.startsWith('http://') && !link.startsWith('https://')) + link = `https://${link}`; + if (flyerLink && !flyerLink.startsWith('http://') && !flyerLink.startsWith('https://')) + flyerLink = `https://${flyerLink}`; + + const requestPerms = getUserPerms(cookies); + if ( + !( + requestPerms >= 0 && + ((requestPerms & PERMISSIONS.MANAGE_POSTINGS) > 0 || + ((requestPerms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && + getUserCompanyId(cookies) === employerId)) + ) + ) { + return fail(403, { errorMessage: 'You cannot preform this action!' }); + } + if ( + !title || + title === '' || + !description || + description === '' || + !address || + address === '' || + !employmentType || + employmentType === '' + ) { + return fail(400, { errorMessage: 'All fields are required' }); + } + + try { + await editPosting({ + id, + title, + description, + employerId, + address, + employmentType, + wage, + link, + tagIds: tagIds?.split(',').map((tag) => parseInt(tag)), + companyId, + flyerLink + }); + } catch (err) { + return fail(500, { errorMessage: `Internal Server Error: ${err}` }); + } + redirect(301, `/postings/${id}`); + }, + delete: async ({ cookies, params }) => { + const id = parseInt(params.posting!); + const perms = getUserPerms(cookies); + if ( + perms >= 0 && + ((perms & PERMISSIONS.MANAGE_POSTINGS) > 0 || + ((perms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && getUserCompanyId(cookies) === id)) + ) { + try { + await deletePosting(id); + } catch (err) { + return fail(500, { errorMessage: `Internal Server Error: ${err}` }); + } + redirect(301, '/postings'); + } + error(403, 'Unauthorized'); + } +}; diff --git a/src/routes/postings/[posting]/manage/edit/+page.svelte b/src/routes/postings/[posting]/manage/edit/+page.svelte new file mode 100644 index 0000000..9e04bc7 --- /dev/null +++ b/src/routes/postings/[posting]/manage/edit/+page.svelte @@ -0,0 +1,160 @@ + + +
+
+
+
+
Edit {data.posting.title}
+
+
+ {#if !userState.companyId} +
+ Company ID * + +
+ {/if} +
+ Title * + +
+
+ Description * + +
+
+ Address * + +
+
+ + +
+
+ Wage (optional) + +
+
+ Link to external posting information (optional) + +
+
+ Link to flyer (optional) + +
+ + {#if form?.errorMessage} +
{form.errorMessage}
+ {/if} +
+ + +
+
+
+
+
+ diff --git a/src/routes/postings/create/+page.server.ts b/src/routes/postings/create/+page.server.ts new file mode 100644 index 0000000..6f1e3c4 --- /dev/null +++ b/src/routes/postings/create/+page.server.ts @@ -0,0 +1,90 @@ +import { type Actions, fail, redirect } from '@sveltejs/kit'; +import { createPosting } from '$lib/db/index.server'; +import { PERMISSIONS } from '$lib/consts'; +import { getUserCompanyId, getUserId, getUserPerms } from '$lib/index.server'; +import type { Posting } from '$lib/types'; + +export const actions: Actions = { + submit: async ({ request, cookies }) => { + const employerId = getUserId(cookies); + + const data = await request.formData(); + let companyId: number | null | undefined = getUserCompanyId(cookies); + if (!companyId) { + if ( + data.get('companyId')?.toString().trim() === undefined || + data.get('companyId')?.toString().trim() === null || + data.get('companyId')?.toString().trim() === '' + ) { + return fail(400, { errorMessage: 'Company ID is required' }); + } else { + companyId = parseInt(data.get('companyId')?.toString().trim()!); + } + } + + const title = data.get('title')?.toString().trim(); + const description = data.get('description')?.toString().trim(); + const address = data.get('address')?.toString().trim(); + const employmentType = data.get('employmentType')?.toString().trim(); + const wage = data.get('wage')?.toString().trim(); + let link = data.get('link')?.toString().trim(); + const tagIds = data.get('tagIds')?.toString().trim() || null; + let flyerLink = data.get('flyerLink')?.toString().trim(); + + if (link && !link?.includes('.')) { + return fail(400, { errorMessage: 'Invalid link' }); + } + if (flyerLink && !flyerLink?.includes('.')) { + return fail(400, { errorMessage: 'Invalid flyer link' }); + } + if (link && !link.startsWith('http://') && !link.startsWith('https://')) + link = `https://${link}`; + if (flyerLink && !flyerLink.startsWith('http://') && !flyerLink.startsWith('https://')) + flyerLink = `https://${flyerLink}`; + + const requestPerms = getUserPerms(cookies); + if ( + !( + requestPerms >= 0 && + ((requestPerms & PERMISSIONS.MANAGE_POSTINGS) > 0 || + ((requestPerms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && + getUserCompanyId(cookies) === companyId)) + ) + ) { + return fail(403, { errorMessage: 'You cannot preform this action!' }); + } + if ( + !title || + title === '' || + !address || + address === '' || + !description || + description === '' || + !employmentType || + employmentType === '' || + !address || + address === '' + ) { + return fail(400, { errorMessage: 'All fields are required' }); + } + + let id: number; + try { + id = await createPosting({ + title, + description, + employerId, + address, + employmentType, + wage, + link, + tagIds: tagIds?.split(',').map((tag) => parseInt(tag)), + companyId, + flyerLink + }); + } catch (err) { + return fail(500, { errorMessage: `Internal Server Error: ${err}` }); + } + redirect(301, `/postings/${id}`); + } +}; diff --git a/src/routes/postings/create/+page.svelte b/src/routes/postings/create/+page.svelte new file mode 100644 index 0000000..5d3c526 --- /dev/null +++ b/src/routes/postings/create/+page.svelte @@ -0,0 +1,112 @@ + + +
+
+
+
+
Create new posting
+
+
+ {#if !userState.companyId} +
+ Company ID * + +
+ {/if} +
+ Title * + +
+
+ Description * + +
+
+ Address * + +
+
+ + +
+
+ Wage (optional) + +
+
+ Link to external posting information (optional) + +
+
+ Link to flyer (optional) + +
+ + {#if form?.errorMessage} +
{form.errorMessage}
+ {/if} + +
+
+
+
diff --git a/src/routes/register/user/+page.server.ts b/src/routes/register/+page.server.ts similarity index 61% rename from src/routes/register/user/+page.server.ts rename to src/routes/register/+page.server.ts index f818485..7b51068 100644 --- a/src/routes/register/user/+page.server.ts +++ b/src/routes/register/+page.server.ts @@ -3,24 +3,11 @@ import { type Actions, type Cookies, fail, redirect } from '@sveltejs/kit'; import jwt from 'jsonwebtoken'; import { createUser } from '$lib/db/index.server'; +import { setJWT } from '$lib/shared.server'; +import type { User } from '$lib/types'; + dotenv.config({ path: '.env' }); -function setJWT(cookies: Cookies, username: string, perms: number) { - const payload = { - username: username, - perms: perms - }; - - if (process.env.JWT_SECRET === undefined) { - throw new Error('JWT_SECRET not defined'); - } - - const maxAge = 60 * 60 * 24 * 30; // 30 days - const JWT = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '30d' }); - cookies.set('jwt', JWT, { maxAge, path: '/', httpOnly: false }); - console.log(cookies.get('jwt')); -} - export const actions: Actions = { register: async ({ request, cookies }) => { const data = await request.formData(); @@ -28,9 +15,11 @@ export const actions: Actions = { const password = data.get('password')?.toString().trim(); const confirmPassword = data.get('confirmPassword')?.toString().trim(); let email: string | undefined | null = data.get('email')?.toString().trim(); + let phone: string | undefined | null = data.get('phone')?.toString().trim(); + let companyCode: string | undefined | null = data.get('companyCode')?.toString().trim(); let fullName: string | undefined | null = data.get('fullName')?.toString().trim(); - if (email === '') email = null; - if (fullName === '') fullName = null; + if (phone === '') phone = null; + if (companyCode === '') companyCode = null; if (email && !email.includes('@')) { return fail(400, { errorMessage: 'Invalid email' }); @@ -40,33 +29,39 @@ export const actions: Actions = { username && password && confirmPassword && + email && + fullName && username !== '' && password !== '' && - confirmPassword !== '' + confirmPassword !== '' && + email !== '' && + fullName !== '' ) { if (password.length < 8) { return fail(400, { errorMessage: 'Password must be at least 8 characters' }); } + const user: User = { + username: username, + password: password, + perms: 3, + active: true, + email: email, + phone: phone, + fullName: fullName + }; if (password === confirmPassword) { try { - await createUser({ - username: username, - password: password, - perms: 3, - active: true, - email: email, - fullName: fullName - }); + await createUser(user); } catch (err) { return fail(400, { errorMessage: `Internal Server Error: ${err}` }); } - setJWT(cookies, username, 1); + setJWT(cookies, user); throw redirect(303, '/'); } else { return fail(400, { errorMessage: 'Passwords do not match' }); } } else { - return fail(400, { errorMessage: 'Missing username or password' }); + return fail(400, { errorMessage: 'Please fill out all required fields' }); } } }; diff --git a/src/routes/register/+page.svelte b/src/routes/register/+page.svelte index ef2c392..2658d8f 100644 --- a/src/routes/register/+page.svelte +++ b/src/routes/register/+page.svelte @@ -1,27 +1,123 @@ diff --git a/src/routes/register/employer/+page.server.ts b/src/routes/register/employer/+page.server.ts deleted file mode 100644 index 5b60250..0000000 --- a/src/routes/register/employer/+page.server.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as dotenv from 'dotenv'; -import { type Actions, type Cookies, fail, redirect } from '@sveltejs/kit'; -import jwt from 'jsonwebtoken'; -import { createUser } from '$lib/db/index.server'; - -dotenv.config({ path: '.env' }); - -function setJWT(cookies: Cookies, username: string, perms: number) { - const payload = { - username: username, - perms: perms - }; - - if (process.env.JWT_SECRET === undefined) { - throw new Error('JWT_SECRET not defined'); - } - - const maxAge = 60 * 60 * 24 * 30; // 30 days - const JWT = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '30d' }); - cookies.set('jwt', JWT, { maxAge, path: '/', httpOnly: false }); - console.log(cookies.get('jwt')); -} - -export const actions: Actions = { - register: async ({ request, cookies }) => { - const data = await request.formData(); - const username = data.get('username')?.toString().trim(); - const password = data.get('password')?.toString().trim(); - const confirmPassword = data.get('confirmPassword')?.toString().trim(); - - if ( - username && - password && - confirmPassword && - username !== '' && - password !== '' && - confirmPassword !== '' - ) { - if (password.length < 8) { - return fail(400, { errorMessage: 'Password must be at least 8 characters' }); - } - if (password === confirmPassword) { - try { - await createUser({ username: username, password: password, perms: 3 }); - } catch (err) { - return fail(400, { errorMessage: `Internal Server Error: ${err}` }); - } - setJWT(cookies, username, 1); - throw redirect(303, '/'); - } else { - return fail(400, { errorMessage: 'Passwords do not match' }); - } - } else { - return fail(400, { errorMessage: 'Missing username or password' }); - } - } -}; diff --git a/src/routes/register/employer/+page.svelte b/src/routes/register/employer/+page.svelte deleted file mode 100644 index 3d5510a..0000000 --- a/src/routes/register/employer/+page.svelte +++ /dev/null @@ -1,55 +0,0 @@ - - - diff --git a/src/routes/register/user/+page.svelte b/src/routes/register/user/+page.svelte deleted file mode 100644 index 4d29639..0000000 --- a/src/routes/register/user/+page.svelte +++ /dev/null @@ -1,85 +0,0 @@ - - - diff --git a/src/routes/signin/+page.server.ts b/src/routes/signin/+page.server.ts index dca408a..ff2f6b9 100644 --- a/src/routes/signin/+page.server.ts +++ b/src/routes/signin/+page.server.ts @@ -1,26 +1,12 @@ -import { checkUserCreds, createUser, updateLastSignin } from '$lib/db/index.server'; -import { fail, redirect, type Actions, type Cookies } from '@sveltejs/kit'; -import jwt from 'jsonwebtoken'; +import { checkUserCreds, updateLastSignin } from '$lib/db/index.server'; +import { fail, redirect, type Actions } from '@sveltejs/kit'; import * as dotenv from 'dotenv'; +import { setJWT } from '$lib/shared.server'; +import type { User } from '$lib/types'; + dotenv.config({ path: '.env' }); -function setJWT(cookies: Cookies, user: User) { - const payload = { - username: user.username, - perms: user.perms, - id: user.id - }; - - if (process.env.JWT_SECRET === undefined) { - throw new Error('JWT_SECRET not defined'); - } - - const maxAge = 60 * 60 * 24 * 30; // 30 days - const JWT = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '30d' }); - cookies.set('jwt', JWT, { maxAge, path: '/', httpOnly: false }); -} - export const actions: Actions = { signin: async ({ request, cookies }) => { const data = await request.formData(); diff --git a/src/routes/utils.client.ts b/src/routes/utils.client.ts deleted file mode 100644 index 0d07d90..0000000 --- a/src/routes/utils.client.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { userState } from '$lib/shared.svelte'; - -export const getCookieValue = (name: String) => - document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || ''; - -export function updateUserState() { - const JWT = getCookieValue('jwt'); - if (JWT !== '') { - const state = JSON.parse(atob(JWT.split('.')[1])); - userState.perms = state.perms; - userState.username = state.username; - userState.id = state.id; - } -} diff --git a/svelte.config.js b/svelte.config.js index f94eeef..ce72894 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,9 +1,10 @@ import adapter from '@sveltejs/adapter-node'; +import { sveltePreprocess } from 'svelte-preprocess'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { - preprocess: vitePreprocess(), + preprocess: sveltePreprocess(), kit: { adapter: adapter()