working full end-to-end with encryption
This commit is contained in:
43
bun.lock
43
bun.lock
@@ -7,6 +7,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/adapter-pg": "^7.2.0",
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
"@prisma/client": "^7.2.0",
|
"@prisma/client": "^7.2.0",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/cookie-parser": "^1.4.10",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
@@ -73,12 +78,16 @@
|
|||||||
|
|
||||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"@types/bcrypt": ["@types/bcrypt@6.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ=="],
|
||||||
|
|
||||||
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
||||||
|
|
||||||
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||||
|
|
||||||
|
"@types/cookie-parser": ["@types/cookie-parser@1.4.10", "", { "peerDependencies": { "@types/express": "*" } }, "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg=="],
|
||||||
|
|
||||||
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
||||||
|
|
||||||
"@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="],
|
"@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="],
|
||||||
@@ -105,8 +114,14 @@
|
|||||||
|
|
||||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||||
|
|
||||||
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||||
|
|
||||||
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
|
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
|
||||||
|
|
||||||
|
"axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="],
|
||||||
|
|
||||||
|
"bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="],
|
||||||
|
|
||||||
"body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="],
|
"body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
||||||
@@ -125,6 +140,8 @@
|
|||||||
|
|
||||||
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
||||||
|
|
||||||
|
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||||
|
|
||||||
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
||||||
|
|
||||||
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||||
@@ -135,7 +152,9 @@
|
|||||||
|
|
||||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
"cookie-parser": ["cookie-parser@1.4.7", "", { "dependencies": { "cookie": "0.7.2", "cookie-signature": "1.0.6" } }, "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw=="],
|
||||||
|
|
||||||
|
"cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
|
||||||
|
|
||||||
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||||
|
|
||||||
@@ -149,6 +168,8 @@
|
|||||||
|
|
||||||
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||||
|
|
||||||
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
|
||||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||||
|
|
||||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||||
@@ -173,6 +194,8 @@
|
|||||||
|
|
||||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||||
|
|
||||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||||
|
|
||||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||||
@@ -185,8 +208,12 @@
|
|||||||
|
|
||||||
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||||
|
|
||||||
|
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||||
|
|
||||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||||
|
|
||||||
|
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||||
|
|
||||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||||
|
|
||||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||||
@@ -211,6 +238,8 @@
|
|||||||
|
|
||||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
"hono": ["hono@4.10.6", "", {}, "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g=="],
|
"hono": ["hono@4.10.6", "", {}, "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g=="],
|
||||||
@@ -259,10 +288,14 @@
|
|||||||
|
|
||||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||||
|
|
||||||
|
"node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="],
|
||||||
|
|
||||||
"node-cron": ["node-cron@4.2.1", "", {}, "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg=="],
|
"node-cron": ["node-cron@4.2.1", "", {}, "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg=="],
|
||||||
|
|
||||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||||
|
|
||||||
|
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
|
||||||
|
|
||||||
"nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="],
|
"nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="],
|
||||||
|
|
||||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
@@ -319,6 +352,8 @@
|
|||||||
|
|
||||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||||
|
|
||||||
|
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||||
|
|
||||||
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
|
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
|
||||||
|
|
||||||
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||||
@@ -409,8 +444,14 @@
|
|||||||
|
|
||||||
"@prisma/get-platform/@prisma/debug": ["@prisma/debug@6.8.2", "", {}, "sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg=="],
|
"@prisma/get-platform/@prisma/debug": ["@prisma/debug@6.8.2", "", {}, "sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg=="],
|
||||||
|
|
||||||
|
"express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||||
|
|
||||||
|
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
"pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||||
|
|
||||||
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||||
|
|
||||||
|
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/adapter-pg": "^7.2.0",
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
"@prisma/client": "^7.2.0",
|
"@prisma/client": "^7.2.0",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/cookie-parser": "^1.4.10",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
|
|||||||
22
prisma/migrations/20251222191415_add_sessions/migration.sql
Normal file
22
prisma/migrations/20251222191415_add_sessions/migration.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Session" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Session_token_key" ON "Session"("token");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Session_token_idx" ON "Session"("token");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `skywardPassword` to the `User` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "skywardPassword" TEXT NOT NULL;
|
||||||
@@ -6,15 +6,30 @@ datasource db {
|
|||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Session {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
token String @unique
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
expiresAt DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([token])
|
||||||
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
username String @unique
|
username String @unique
|
||||||
password String
|
password String // Bcrypt hash for login auth
|
||||||
|
skywardPassword String // Encrypted password for Skyward API calls
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
classes Class[]
|
classes Class[]
|
||||||
fetches Fetch[]
|
fetches Fetch[]
|
||||||
finalGrades FinalGrade[]
|
finalGrades FinalGrade[]
|
||||||
|
sessions Session[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Class {
|
model Class {
|
||||||
|
|||||||
13
src/index.ts
13
src/index.ts
@@ -1,19 +1,28 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import { disconnectDB } from './db';
|
import { disconnectDB } from './db';
|
||||||
|
import cookieParser from 'cookie-parser'; // ADD THIS
|
||||||
import { startSyncScheduler, stopSyncScheduler } from './jobs/sync-scheduler';
|
import { startSyncScheduler, stopSyncScheduler } from './jobs/sync-scheduler';
|
||||||
|
|
||||||
// Import routes
|
// Import routes
|
||||||
import usersRoutes from './routes/users.routes';
|
import usersRoutes from './routes/users.routes';
|
||||||
import gradesRoutes from './routes/grades.routes';
|
import gradesRoutes from './routes/grades.routes';
|
||||||
import statsRoutes from './routes/stats.routes';
|
import statsRoutes from './routes/stats.routes';
|
||||||
|
import authRoutes from './routes/auth.routes'; // ADD THIS
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 4000;
|
const PORT = process.env.PORT || 4000;
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(cors());
|
|
||||||
|
app.use(cors({
|
||||||
|
origin: 'http://localhost:5173',
|
||||||
|
credentials: true,
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization']
|
||||||
|
}));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
app.use(cookieParser()); // ADD THIS
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
@@ -28,6 +37,8 @@ app.get('/health', (req, res) => {
|
|||||||
app.use('/api/users', usersRoutes);
|
app.use('/api/users', usersRoutes);
|
||||||
app.use('/api', gradesRoutes);
|
app.use('/api', gradesRoutes);
|
||||||
app.use('/api/stats', statsRoutes);
|
app.use('/api/stats', statsRoutes);
|
||||||
|
app.use('/api/auth', authRoutes); // ADD THIS
|
||||||
|
|
||||||
|
|
||||||
// 404 handler
|
// 404 handler
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
|
|||||||
231
src/routes/auth.routes.ts
Normal file
231
src/routes/auth.routes.ts
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
import { prisma } from '../db';
|
||||||
|
import axios from 'axios';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { encrypt, decrypt } from '../utils/encryption';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
const SALT_ROUNDS = 12;
|
||||||
|
const SESSION_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||||
|
|
||||||
|
// Middleware to check if user is authenticated
|
||||||
|
export async function requireAuth(req: any, res: any, next: any) {
|
||||||
|
const sessionToken = req.cookies?.sessionToken;
|
||||||
|
|
||||||
|
if (!sessionToken) {
|
||||||
|
return res.status(401).json({ error: 'Not authenticated' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await prisma.session.findUnique({
|
||||||
|
where: { token: sessionToken },
|
||||||
|
include: { user: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session || session.expiresAt < new Date()) {
|
||||||
|
return res.status(401).json({ error: 'Session expired' });
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = session.user;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(500).json({ error: 'Authentication error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register new user
|
||||||
|
router.post('/register', async (req, res) => {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return res.status(400).json({ error: 'Username and password required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
return res.status(400).json({ error: 'Password must be at least 8 characters' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedUsername = username.toLowerCase().trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingUser = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
username: {
|
||||||
|
equals: normalizedUsername,
|
||||||
|
mode: 'insensitive'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
return res.status(409).json({ error: 'Username already exists' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify credentials with Skyward via port 3000 API
|
||||||
|
console.log('Verifying Skyward credentials...');
|
||||||
|
try {
|
||||||
|
const authCheck = await axios.post('http://localhost:3000/check-auth', {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
}, {
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!authCheck.data.success) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Invalid Skyward credentials. Please verify your username and password.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log('Skyward credentials verified successfully');
|
||||||
|
} catch (authError: any) {
|
||||||
|
if (authError.response?.status === 401) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Invalid Skyward credentials. Please verify your username and password.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.error('Skyward auth check failed:', authError.message);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'Unable to verify credentials with Skyward. Please try again.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password for login authentication
|
||||||
|
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
|
||||||
|
|
||||||
|
// Encrypt password for Skyward API calls
|
||||||
|
const encryptedSkywardPassword = encrypt(password);
|
||||||
|
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
username: normalizedUsername,
|
||||||
|
password: hashedPassword,
|
||||||
|
skywardPassword: encryptedSkywardPassword
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('User created successfully');
|
||||||
|
|
||||||
|
const sessionToken = crypto.randomBytes(32).toString('hex');
|
||||||
|
const expiresAt = new Date(Date.now() + SESSION_DURATION);
|
||||||
|
|
||||||
|
await prisma.session.create({
|
||||||
|
data: {
|
||||||
|
token: sessionToken,
|
||||||
|
userId: user.id,
|
||||||
|
expiresAt
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.cookie('sessionToken', sessionToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: false, // set to true in production
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge: SESSION_DURATION
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Registration error:', error);
|
||||||
|
return res.status(500).json({ error: 'Registration failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Login existing user
|
||||||
|
router.post('/login', async (req, res) => {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return res.status(400).json({ error: 'Username and password required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedUsername = username.toLowerCase().trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
username: {
|
||||||
|
equals: normalizedUsername,
|
||||||
|
mode: 'insensitive'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const validPassword = await bcrypt.compare(password, user.password);
|
||||||
|
|
||||||
|
if (!validPassword) {
|
||||||
|
return res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionToken = crypto.randomBytes(32).toString('hex');
|
||||||
|
const expiresAt = new Date(Date.now() + SESSION_DURATION);
|
||||||
|
|
||||||
|
await prisma.session.create({
|
||||||
|
data: {
|
||||||
|
token: sessionToken,
|
||||||
|
userId: user.id,
|
||||||
|
expiresAt
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.cookie('sessionToken', sessionToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: false,
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge: SESSION_DURATION
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
return res.status(500).json({ error: 'Login failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
router.post('/logout', async (req, res) => {
|
||||||
|
const sessionToken = req.cookies?.sessionToken;
|
||||||
|
|
||||||
|
if (sessionToken) {
|
||||||
|
try {
|
||||||
|
await prisma.session.delete({
|
||||||
|
where: { token: sessionToken }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Session might not exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.clearCookie('sessionToken');
|
||||||
|
return res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check auth status
|
||||||
|
router.get('/me', requireAuth, async (req: any, res) => {
|
||||||
|
return res.json({
|
||||||
|
user: {
|
||||||
|
id: req.user.id,
|
||||||
|
username: req.user.username
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -2,13 +2,24 @@ import { Router } from 'express';
|
|||||||
import { getGradesForUser, getFetchHistory, getFinalGrades } from '../services/grades.service';
|
import { getGradesForUser, getFetchHistory, getFinalGrades } from '../services/grades.service';
|
||||||
import { syncUserGrades } from '../services/sync.service';
|
import { syncUserGrades } from '../services/sync.service';
|
||||||
import { prisma } from '../db';
|
import { prisma } from '../db';
|
||||||
|
import { requireAuth } from './auth.routes';
|
||||||
|
import { getSkywardCredentials } from '../utils/user-helpers';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// Get all grades for a user
|
// Get all grades for a user (protected)
|
||||||
router.get('/users/:username/grades', async (req, res) => {
|
router.get('/users/:username/grades', requireAuth, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
const { username } = req.params;
|
const { username } = req.params;
|
||||||
|
|
||||||
|
// Ensure user can only access their own grades
|
||||||
|
if (req.user.username !== username) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Access denied'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const user = await getGradesForUser(username);
|
const user = await getGradesForUser(username);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -28,10 +39,19 @@ router.get('/users/:username/grades', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get classes for a user
|
// Get classes for a user (protected)
|
||||||
router.get('/users/:username/classes', async (req, res) => {
|
router.get('/users/:username/classes', requireAuth, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
const { username } = req.params;
|
const { username } = req.params;
|
||||||
|
|
||||||
|
// Ensure user can only access their own classes
|
||||||
|
if (req.user.username !== username) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Access denied'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { username },
|
where: { username },
|
||||||
include: {
|
include: {
|
||||||
@@ -69,11 +89,19 @@ router.get('/users/:username/classes', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get assignments for a specific class
|
// Get assignments for a specific class (protected)
|
||||||
router.get('/users/:username/classes/:category/assignments', async (req, res) => {
|
router.get('/users/:username/classes/:category/assignments', requireAuth, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
const { username, category } = req.params;
|
const { username, category } = req.params;
|
||||||
|
|
||||||
|
// Ensure user can only access their own assignments
|
||||||
|
if (req.user.username !== username) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Access denied'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { username },
|
where: { username },
|
||||||
});
|
});
|
||||||
@@ -85,7 +113,6 @@ router.get('/users/:username/classes/:category/assignments', async (req, res) =>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all classes with this category (there might be multiple)
|
|
||||||
const classes = await prisma.class.findMany({
|
const classes = await prisma.class.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -105,7 +132,6 @@ router.get('/users/:username/classes/:category/assignments', async (req, res) =>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return all classes with this category
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
category,
|
category,
|
||||||
@@ -126,10 +152,19 @@ router.get('/users/:username/classes/:category/assignments', async (req, res) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get fetch history for a user
|
// Get fetch history for a user (protected)
|
||||||
router.get('/users/:username/history', async (req, res) => {
|
router.get('/users/:username/history', requireAuth, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
const { username } = req.params;
|
const { username } = req.params;
|
||||||
|
|
||||||
|
// Ensure user can only access their own history
|
||||||
|
if (req.user.username !== username) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Access denied'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const history = await getFetchHistory(username);
|
const history = await getFetchHistory(username);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -147,10 +182,19 @@ router.get('/users/:username/history', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get final grades for a user
|
// Get final grades for a user (protected)
|
||||||
router.get('/users/:username/final-grades', async (req, res) => {
|
router.get('/users/:username/final-grades', requireAuth, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
const { username } = req.params;
|
const { username } = req.params;
|
||||||
|
|
||||||
|
// Ensure user can only access their own grades
|
||||||
|
if (req.user.username !== username) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Access denied'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const finalGrades = await getFinalGrades(username);
|
const finalGrades = await getFinalGrades(username);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -168,23 +212,21 @@ router.get('/users/:username/final-grades', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Manual sync for one user
|
// Manual sync for authenticated user (protected)
|
||||||
router.post('/sync/manual', async (req, res) => {
|
router.post('/sync/manual', requireAuth, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
const { username, password } = req.body;
|
console.log(`[Manual Sync] User ${req.user.username} requested manual sync`);
|
||||||
|
|
||||||
if (!username || !password) {
|
// Get decrypted Skyward credentials
|
||||||
return res.status(400).json({
|
const { username, password } = await getSkywardCredentials(req.user.id);
|
||||||
success: false,
|
|
||||||
error: 'Username and password are required',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Sync grades using the decrypted password
|
||||||
const result = await syncUserGrades(username, password);
|
const result = await syncUserGrades(username, password);
|
||||||
|
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`[Manual Sync] Error:`, errorMsg);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: errorMsg,
|
error: errorMsg,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { getOverallStats, getUserStats, getClassStats } from '../services/stats.service';
|
import { getOverallStats, getUserStats, getClassStats } from '../services/stats.service';
|
||||||
|
import { requireAuth } from './auth.routes';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// Overall system stats
|
// Overall system stats (protected)
|
||||||
router.get('/overall', async (req, res) => {
|
router.get('/overall', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const stats = await getOverallStats();
|
const stats = await getOverallStats();
|
||||||
res.json({
|
res.json({
|
||||||
@@ -20,10 +21,19 @@ router.get('/overall', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// User-specific stats
|
// User-specific stats (protected)
|
||||||
router.get('/users/:username', async (req, res) => {
|
router.get('/users/:username', requireAuth, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
const { username } = req.params;
|
const { username } = req.params;
|
||||||
|
|
||||||
|
// Ensure user can only access their own stats
|
||||||
|
if (req.user.username !== username) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Access denied'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const stats = await getUserStats(username);
|
const stats = await getUserStats(username);
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -38,10 +48,19 @@ router.get('/users/:username', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Class-specific stats
|
// Class-specific stats (protected)
|
||||||
router.get('/users/:username/classes/:category', async (req, res) => {
|
router.get('/users/:username/classes/:category', requireAuth, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
const { username, category } = req.params;
|
const { username, category } = req.params;
|
||||||
|
|
||||||
|
// Ensure user can only access their own stats
|
||||||
|
if (req.user.username !== username) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Access denied'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const stats = await getClassStats(username, category);
|
const stats = await getClassStats(username, category);
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,58 +1,15 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { prisma } from '../db';
|
import { prisma } from '../db';
|
||||||
import { getAllUsers, deleteUser } from '../services/grades.service';
|
import { getAllUsers, deleteUser } from '../services/grades.service';
|
||||||
|
import { requireAuth } from './auth.routes';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// Register a new user
|
// NOTE: User registration is now handled by /api/auth/register
|
||||||
router.post('/register', async (req, res) => {
|
// This old endpoint is kept for backward compatibility but should be removed
|
||||||
try {
|
|
||||||
const { username, password } = req.body;
|
|
||||||
|
|
||||||
if (!username || !password) {
|
// Get all users (protected, admin only - you can add admin check later)
|
||||||
return res.status(400).json({
|
router.get('/', requireAuth, async (req, res) => {
|
||||||
success: false,
|
|
||||||
error: 'Username and password are required',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user exists
|
|
||||||
const existing = await prisma.user.findUnique({
|
|
||||||
where: { username },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
return res.status(409).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Username already exists',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create user
|
|
||||||
const user = await prisma.user.create({
|
|
||||||
data: { username, password },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
createdAt: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
user,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: errorMsg,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get all users
|
|
||||||
router.get('/', async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const users = await getAllUsers();
|
const users = await getAllUsers();
|
||||||
res.json({
|
res.json({
|
||||||
@@ -69,10 +26,19 @@ router.get('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete user
|
// Delete user (protected)
|
||||||
router.delete('/:username', async (req, res) => {
|
router.delete('/:username', requireAuth, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
const { username } = req.params;
|
const { username } = req.params;
|
||||||
|
|
||||||
|
// Ensure user can only delete their own account
|
||||||
|
if (req.user.username !== username) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Access denied'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const result = await deleteUser(username);
|
const result = await deleteUser(username);
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -17,24 +17,15 @@ interface GradeData {
|
|||||||
|
|
||||||
export async function syncGradesToDatabase(
|
export async function syncGradesToDatabase(
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
|
||||||
gradesData: GradeData[]
|
gradesData: GradeData[]
|
||||||
) {
|
) {
|
||||||
// Step 1: Find or create user
|
// Find user (must already exist from registration)
|
||||||
let user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { username },
|
where: { username },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await prisma.user.create({
|
throw new Error('User not found. Please register first.');
|
||||||
data: { username, password },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Update password in case it changed
|
|
||||||
user = await prisma.user.update({
|
|
||||||
where: { username },
|
|
||||||
data: { password },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let classesProcessed = 0;
|
let classesProcessed = 0;
|
||||||
@@ -42,7 +33,7 @@ export async function syncGradesToDatabase(
|
|||||||
let assignmentsUpdated = 0;
|
let assignmentsUpdated = 0;
|
||||||
let finalGradesUpdated = 0;
|
let finalGradesUpdated = 0;
|
||||||
|
|
||||||
// Step 2: Process each class
|
// Process each class
|
||||||
for (const classData of gradesData) {
|
for (const classData of gradesData) {
|
||||||
// Find or create class
|
// Find or create class
|
||||||
let classRecord = await prisma.class.findUnique({
|
let classRecord = await prisma.class.findUnique({
|
||||||
@@ -80,7 +71,7 @@ export async function syncGradesToDatabase(
|
|||||||
|
|
||||||
classesProcessed++;
|
classesProcessed++;
|
||||||
|
|
||||||
// Step 2.5: Save/Update Final Grade if present
|
// Save/Update Final Grade if present
|
||||||
if (classData.overallGrade) {
|
if (classData.overallGrade) {
|
||||||
const existingFinalGrade = await prisma.finalGrade.findUnique({
|
const existingFinalGrade = await prisma.finalGrade.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -164,7 +155,7 @@ export async function syncGradesToDatabase(
|
|||||||
console.log(`[DB] Class ${classData.className} (${classData.category}): ${assignmentsAdded} new, ${assignmentsUpdated} updated`);
|
console.log(`[DB] Class ${classData.className} (${classData.category}): ${assignmentsAdded} new, ${assignmentsUpdated} updated`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Log this fetch
|
// Log this fetch
|
||||||
await prisma.fetch.create({
|
await prisma.fetch.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -194,7 +185,7 @@ export async function getGradesForUser(username: string) {
|
|||||||
assignments: {
|
assignments: {
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
},
|
},
|
||||||
finalGrades: true, // Include final grades
|
finalGrades: true,
|
||||||
},
|
},
|
||||||
orderBy: { category: 'asc' },
|
orderBy: { category: 'asc' },
|
||||||
},
|
},
|
||||||
@@ -250,7 +241,6 @@ export async function getFetchHistory(username: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// New function to get final grades
|
|
||||||
export async function getFinalGrades(username: string) {
|
export async function getFinalGrades(username: string) {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { username },
|
where: { username },
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { fetchGradesFromScraper } from './scraper.service';
|
import { fetchGradesFromScraper } from './scraper.service';
|
||||||
import { syncGradesToDatabase, getAllUsers } from './grades.service';
|
import { syncGradesToDatabase, getAllUsers } from './grades.service';
|
||||||
|
import { getSkywardCredentials } from '../utils/user-helpers';
|
||||||
|
import { prisma } from '../db';
|
||||||
|
|
||||||
interface SyncResult {
|
interface SyncResult {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -20,8 +22,8 @@ export async function syncUserGrades(
|
|||||||
// Step 1: Fetch from scraper
|
// Step 1: Fetch from scraper
|
||||||
const gradesData = await fetchGradesFromScraper(username, password);
|
const gradesData = await fetchGradesFromScraper(username, password);
|
||||||
|
|
||||||
// Step 2: Save to database
|
// Step 2: Save to database (no longer needs password param)
|
||||||
const result = await syncGradesToDatabase(username, password, gradesData);
|
const result = await syncGradesToDatabase(username, gradesData);
|
||||||
|
|
||||||
console.log(`[Sync] ✓ Completed for ${username}: ${result.classesProcessed} classes, ${result.assignmentsAdded} new, ${result.assignmentsUpdated} updated`);
|
console.log(`[Sync] ✓ Completed for ${username}: ${result.classesProcessed} classes, ${result.assignmentsAdded} new, ${result.assignmentsUpdated} updated`);
|
||||||
|
|
||||||
@@ -56,8 +58,13 @@ export async function syncMultipleUsers(): Promise<{
|
|||||||
|
|
||||||
console.log(`\n[Sync Job] Starting batch sync (max ${maxConcurrent} concurrent)`);
|
console.log(`\n[Sync Job] Starting batch sync (max ${maxConcurrent} concurrent)`);
|
||||||
|
|
||||||
// Get all users with their credentials
|
// Get all users with IDs
|
||||||
const users = await getAllUsers();
|
const users = await prisma.user.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
console.log('[Sync Job] No users to sync');
|
console.log('[Sync Job] No users to sync');
|
||||||
@@ -70,25 +77,30 @@ export async function syncMultipleUsers(): Promise<{
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to get passwords, so fetch full user records
|
|
||||||
const { prisma } = await import('../db');
|
|
||||||
const usersWithPasswords = await prisma.user.findMany({
|
|
||||||
select: {
|
|
||||||
username: true,
|
|
||||||
password: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const results: SyncResult[] = [];
|
const results: SyncResult[] = [];
|
||||||
|
|
||||||
// Process in batches
|
// Process in batches
|
||||||
for (let i = 0; i < usersWithPasswords.length; i += maxConcurrent) {
|
for (let i = 0; i < users.length; i += maxConcurrent) {
|
||||||
const batch = usersWithPasswords.slice(i, i + maxConcurrent);
|
const batch = users.slice(i, i + maxConcurrent);
|
||||||
console.log(`[Sync Job] Processing batch ${Math.floor(i / maxConcurrent) + 1}: ${batch.map(u => u.username).join(', ')}`);
|
console.log(`[Sync Job] Processing batch ${Math.floor(i / maxConcurrent) + 1}: ${batch.map(u => u.username).join(', ')}`);
|
||||||
|
|
||||||
const batchPromises = batch.map(user =>
|
const batchPromises = batch.map(async (user) => {
|
||||||
syncUserGrades(user.username, user.password)
|
try {
|
||||||
);
|
// Get decrypted credentials for each user
|
||||||
|
const { username, password } = await getSkywardCredentials(user.id);
|
||||||
|
|
||||||
|
// Sync with decrypted password
|
||||||
|
return await syncUserGrades(username, password);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`[Sync Job] Failed to get credentials for ${user.username}:`, errorMsg);
|
||||||
|
return {
|
||||||
|
username: user.username,
|
||||||
|
success: false,
|
||||||
|
error: errorMsg,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const batchResults = await Promise.all(batchPromises);
|
const batchResults = await Promise.all(batchPromises);
|
||||||
results.push(...batchResults);
|
results.push(...batchResults);
|
||||||
|
|||||||
31
src/utils/encryption.ts
Normal file
31
src/utils/encryption.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
// IMPORTANT: Add this to your .env file
|
||||||
|
// Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||||
|
const IV_LENGTH = 16;
|
||||||
|
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || '';
|
||||||
|
|
||||||
|
export function encrypt(text: string): string {
|
||||||
|
const key = Buffer.from(ENCRYPTION_KEY.slice(0, 32), 'utf8');
|
||||||
|
const iv = crypto.randomBytes(IV_LENGTH);
|
||||||
|
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
||||||
|
|
||||||
|
let encrypted = cipher.update(text, 'utf8', 'hex');
|
||||||
|
encrypted += cipher.final('hex');
|
||||||
|
|
||||||
|
return iv.toString('hex') + ':' + encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decrypt(text: string): string {
|
||||||
|
const key = Buffer.from(ENCRYPTION_KEY.slice(0, 32), 'utf8');
|
||||||
|
const parts = text.split(':');
|
||||||
|
const iv = Buffer.from(parts.shift()!, 'hex');
|
||||||
|
const encryptedText = parts.join(':');
|
||||||
|
|
||||||
|
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
||||||
|
|
||||||
|
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
|
||||||
|
decrypted += decipher.final('utf8');
|
||||||
|
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
18
src/utils/user-helpers.ts
Normal file
18
src/utils/user-helpers.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { prisma } from '../db';
|
||||||
|
import { decrypt } from './encryption';
|
||||||
|
|
||||||
|
export async function getSkywardCredentials(userId: string): Promise<{ username: string; password: string }> {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: { username: true, skywardPassword: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
username: user.username,
|
||||||
|
password: decrypt(user.skywardPassword)
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user