diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..6e23049 --- /dev/null +++ b/.cursorignore @@ -0,0 +1,3 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) +.env.local +.env \ No newline at end of file diff --git a/package.json b/package.json index cb0c663..d681897 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "lodash": "^4.17.23", "lucide-react": "^0.553.0", "next": "^16.0.8", - "next-auth": "5.0.0-beta.30", + "next-auth": "^4.24.13", "radix-ui": "^1.4.3", "react": "19.2.0", "react-dom": "19.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d42bfd9..45dd897 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,8 +105,8 @@ importers: specifier: ^16.0.8 version: 16.0.8(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) next-auth: - specifier: 5.0.0-beta.30 - version: 5.0.0-beta.30(next@16.0.8(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) + specifier: ^4.24.13 + version: 4.24.13(next@16.0.8(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -157,20 +157,6 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@auth/core@0.41.0': - resolution: {integrity: sha512-Wd7mHPQ/8zy6Qj7f4T46vg3aoor8fskJm6g2Zyj064oQ3+p0xNZXAV60ww0hY+MbTesfu29kK14Zk5d5JTazXQ==} - peerDependencies: - '@simplewebauthn/browser': ^9.0.1 - '@simplewebauthn/server': ^9.0.2 - nodemailer: ^6.8.0 - peerDependenciesMeta: - '@simplewebauthn/browser': - optional: true - '@simplewebauthn/server': - optional: true - nodemailer: - optional: true - '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -226,6 +212,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -367,105 +357,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -606,28 +580,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@16.0.8': resolution: {integrity: sha512-JaXFAlqn8fJV+GhhA9lpg6da/NCN/v9ub98n3HoayoUSPOVdoxEEt86iT58jXqQCs/R3dv5ZnxGkW8aF4obMrQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@16.0.8': resolution: {integrity: sha512-O7M9it6HyNhsJp3HNAsJoHk5BUsfj7hRshfptpGcVsPZ1u0KQ/oVy8oxF7tlwxA5tR43VUP0yRmAGm1us514ng==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@16.0.8': resolution: {integrity: sha512-8+KClEC/GLI2dLYcrWwHu5JyC5cZYCFnccVIvmxpo6K+XQt4qzqM5L4coofNDZYkct/VCCyJWGbZZDsg6w6LFA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@16.0.8': resolution: {integrity: sha512-rpQ/PgTEgH68SiXmhu/cJ2hk9aZ6YgFvspzQWe2I9HufY6g7V02DXRr/xrVqOaKm2lenBFPNQ+KAaeveywqV+A==} @@ -1480,28 +1450,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.16': resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.16': resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.16': resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.16': resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==} @@ -1661,49 +1627,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -1872,6 +1830,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2402,8 +2364,8 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - jose@6.1.3: - resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + jose@4.15.9: + resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2496,28 +2458,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -2552,6 +2510,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + lucide-react@0.553.0: resolution: {integrity: sha512-BRgX5zrWmNy/lkVAe0dXBgd7XQdZ3HTf+Hwe3c9WK6dqgnj9h+hxV+MDncM88xDWlCq27+TKvHGE70ViODNILw==} peerDependencies: @@ -2598,18 +2560,16 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next-auth@5.0.0-beta.30: - resolution: {integrity: sha512-+c51gquM3F6nMVmoAusRJ7RIoY0K4Ts9HCCwyy/BRoe4mp3msZpOzYMyb5LAYc1wSo74PMQkGDcaghIO7W6Xjg==} + next-auth@4.24.13: + resolution: {integrity: sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==} peerDependencies: - '@simplewebauthn/browser': ^9.0.1 - '@simplewebauthn/server': ^9.0.2 - next: ^14.0.0-0 || ^15.0.0 || ^16.0.0 + '@auth/core': 0.34.3 + next: ^12.2.5 || ^13 || ^14 || ^15 || ^16 nodemailer: ^7.0.7 - react: ^18.2.0 || ^19.0.0 + react: ^17.0.2 || ^18 || ^19 + react-dom: ^17.0.2 || ^18 || ^19 peerDependenciesMeta: - '@simplewebauthn/browser': - optional: true - '@simplewebauthn/server': + '@auth/core': optional: true nodemailer: optional: true @@ -2638,13 +2598,17 @@ packages: node-releases@2.0.26: resolution: {integrity: sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==} - oauth4webapi@3.8.5: - resolution: {integrity: sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==} + oauth@0.9.15: + resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-hash@2.2.0: + resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} + engines: {node: '>= 6'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -2673,6 +2637,13 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + oidc-token-hash@5.2.0: + resolution: {integrity: sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==} + engines: {node: ^10.13.0 || >=12.0.0} + + openid-client@5.7.1: + resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2727,18 +2698,21 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - preact-render-to-string@6.5.11: - resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} + preact-render-to-string@5.2.6: + resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} peerDependencies: preact: '>=10' - preact@10.24.3: - resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==} + preact@10.28.0: + resolution: {integrity: sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA==} prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + pretty-format@3.8.0: + resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} + prismjs@1.30.0: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} @@ -3088,6 +3062,10 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -3116,6 +3094,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yjs@13.6.29: resolution: {integrity: sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} @@ -3137,14 +3118,6 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@auth/core@0.41.0': - dependencies: - '@panva/hkdf': 1.2.1 - jose: 6.1.3 - oauth4webapi: 3.8.5 - preact: 10.24.3 - preact-render-to-string: 6.5.11(preact@10.24.3) - '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -3222,6 +3195,8 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@babel/runtime@7.28.4': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -4928,6 +4903,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@0.7.2: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -5613,7 +5590,7 @@ snapshots: jiti@2.6.1: {} - jose@6.1.3: {} + jose@4.15.9: {} js-tokens@4.0.0: {} @@ -5728,6 +5705,10 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + lucide-react@0.553.0(react@19.2.0): dependencies: react: 19.2.0 @@ -5763,11 +5744,20 @@ snapshots: natural-compare@1.4.0: {} - next-auth@5.0.0-beta.30(next@16.0.8(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0): + next-auth@4.24.13(next@16.0.8(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - '@auth/core': 0.41.0 + '@babel/runtime': 7.28.4 + '@panva/hkdf': 1.2.1 + cookie: 0.7.2 + jose: 4.15.9 next: 16.0.8(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + oauth: 0.9.15 + openid-client: 5.7.1 + preact: 10.28.0 + preact-render-to-string: 5.2.6(preact@10.28.0) react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + uuid: 8.3.2 next@16.0.8(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: @@ -5794,10 +5784,12 @@ snapshots: node-releases@2.0.26: {} - oauth4webapi@3.8.5: {} + oauth@0.9.15: {} object-assign@4.1.1: {} + object-hash@2.2.0: {} + object-inspect@1.13.4: {} object-keys@1.1.1: {} @@ -5838,6 +5830,15 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + oidc-token-hash@5.2.0: {} + + openid-client@5.7.1: + dependencies: + jose: 4.15.9 + lru-cache: 6.0.0 + object-hash: 2.2.0 + oidc-token-hash: 5.2.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -5891,14 +5892,17 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - preact-render-to-string@6.5.11(preact@10.24.3): + preact-render-to-string@5.2.6(preact@10.28.0): dependencies: - preact: 10.24.3 + preact: 10.28.0 + pretty-format: 3.8.0 - preact@10.24.3: {} + preact@10.28.0: {} prelude-ls@1.2.1: {} + pretty-format@3.8.0: {} + prismjs@1.30.0: {} prop-types@15.8.1: @@ -6395,6 +6399,8 @@ snapshots: dependencies: react: 19.2.0 + uuid@8.3.2: {} + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -6444,6 +6450,8 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: {} + yjs@13.6.29: dependencies: lib0: 0.2.117 diff --git a/sample.env b/sample.env index d3f145f..78b1b57 100644 --- a/sample.env +++ b/sample.env @@ -2,9 +2,9 @@ NEXT_PUBLIC_API_URL = http://localhost:8000 NEXTAUTH_URL=http://localhost:3000 # NEXTAUTH_SECRET is required by the Auth.js library to handle sessions, to generate a secret you can run this command (openssl rand -base64 32) or just set to a random string like "mysecret" -AUTH_SECRET= +NEXTAUTH_SECRET= -# These are all values that can be found in the teams chat provided by UTS -AUTH_MICROSOFT_ENTRA_ID_ID= -AUTH_MICROSOFT_ENTRA_ID_SECRET= -AUTH_MICROSOFT_ENTRA_ID_TENANT_ID= \ No newline at end of file +# These are all values that can be found in the Auth0 client, in the future will be provided by UTS +AUTH_CLIENT_SECRET= +AUTH_CLIENT_ID= +AUTH_ISSUER= \ No newline at end of file diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 7c62e2d..7b38c1b 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,2 +1,6 @@ -import { handlers } from "@/auth"; -export const { GET, POST } = handlers; +import NextAuth from "next-auth"; +import { authOptions } from "@/lib/auth"; + +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST }; diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx index a2bd5a5..5508a1b 100644 --- a/src/app/auth/signin/page.tsx +++ b/src/app/auth/signin/page.tsx @@ -1,34 +1,25 @@ "use client"; -import { useEffect, Suspense } from "react"; +import { useEffect } from "react"; import { signIn } from "next-auth/react"; import { useSearchParams } from "next/navigation"; -function SignInContent() { +export default function SignInPage() { const searchParams = useSearchParams(); - - const redirectTo = searchParams.get("callbackUrl") || "/"; + // If there is no callbackUrl (like if the user went directly to /auth/signin), redirect to home "/" + const callbackUrl = searchParams.get("callbackUrl") || "/"; useEffect(() => { - signIn("microsoft-entra-id", { redirectTo }); - }, [redirectTo]); + // "auth0" is the provider ID. This might be changed in the future when we swap to Azure Active Directory / Entra (whatever Microsoft calls it these days) + signIn("auth0", { callbackUrl }); + }, [callbackUrl]); return (

Redirecting to Login...

-

- Please wait while we connect to McMaster SSO -

+

Please wait while we send you to SSO

); } - -export default function SignInPage() { - return ( - Loading...}> - - - ); -} diff --git a/src/app/courses/[courseCode]/[unitName]/[subtopicName]/test/page.tsx b/src/app/courses/[courseCode]/[unitName]/[subtopicName]/test/page.tsx index b6bcc4e..9edc2d7 100644 --- a/src/app/courses/[courseCode]/[unitName]/[subtopicName]/test/page.tsx +++ b/src/app/courses/[courseCode]/[unitName]/[subtopicName]/test/page.tsx @@ -132,11 +132,15 @@ function QuestionTestPage({ params: paramsPromise }: QuestionTestPageProps) { }; const updateContinueActions = () => { - getActiveTestSession(authFetch).then((session) => { - setContinueActions({ - use_skipped_questions: session.skipped_questions.length > 0, + getActiveTestSession(authFetch) + .then((session) => { + setContinueActions({ + use_skipped_questions: session.skipped_questions.length > 0, + }); + }) + .catch(() => { + setContinueActions({ use_skipped_questions: false }); }); - }); }; useEffect(() => { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2b9c64d..4ab0d58 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,10 +1,10 @@ import type { Metadata } from "next"; import { Inter, Poppins } from "next/font/google"; -import "@/styles/globals.css"; -import "@/styles/globals.css"; +import "../styles/globals.css"; -import { SessionProvider } from "next-auth/react"; -import { auth } from "@/auth"; +import { authOptions } from "@/lib/auth"; +import { getServerSession } from "next-auth"; +import { Providers } from "./providers"; const inter = Inter({ variable: "--font-inter", @@ -27,17 +27,17 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const session = await auth(); + const session = await getServerSession(authOptions); return ( - + {children} - + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index f9bed6d..61477ec 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,14 +1,30 @@ "use client"; import Image from "next/image"; +import { useRouter } from "next/navigation"; import { MacFastHeader } from "@/components/ui/custom/macfast-header"; import CourseCard from "@/components/ui/custom/course-card"; import { useUserCourses } from "@/hooks/useUserCourses"; +import { useAuthFetch } from "@/hooks/useFetchWithAuth"; +import { getResumeTarget } from "@/lib/resume-api"; import ErrorMessage from "@/components/ui/custom/error-message"; export default function Home() { + const router = useRouter(); + const authFetch = useAuthFetch(); const { courses: userCourses, isLoading, error } = useUserCourses(); + const handleResume = async (courseCode: string) => { + try { + const target = await getResumeTarget(courseCode, authFetch); + const url = `/courses/${encodeURIComponent(target.course_code)}/${encodeURIComponent(target.unit_name)}/${encodeURIComponent(target.subtopic_name)}/test`; + router.push(url); + } catch { + // Fallback to course page if resume fails (e.g. when backend not ready) + router.push(`/courses/${encodeURIComponent(courseCode)}/coursepage`); + } + }; + console.log("Error:", error); if (error && (error as any).status === 403) { @@ -42,7 +58,12 @@ export default function Home() {
{userCourses.map((course, index) => ( - + ))}
diff --git a/src/app/providers.tsx b/src/app/providers.tsx new file mode 100644 index 0000000..d29f39f --- /dev/null +++ b/src/app/providers.tsx @@ -0,0 +1,6 @@ +"use client"; +import { SessionProvider } from "next-auth/react"; + +export function Providers({ children, session }: any) { + return {children}; +} diff --git a/src/auth.ts b/src/auth.ts deleted file mode 100644 index c2acdf1..0000000 --- a/src/auth.ts +++ /dev/null @@ -1,88 +0,0 @@ -import NextAuth from "next-auth"; -import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id"; - -// followed example from https://authjs.dev/guides/refresh-token-rotation and https://authjs.dev/reference/core/providers/microsoft-entra-id - -export const { handlers, auth } = NextAuth({ - providers: [ - MicrosoftEntraID({ - clientId: process.env.AUTH_MICROSOFT_ENTRA_ID_ID, - clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET, - issuer: `https://login.microsoftonline.com/${process.env.AUTH_MICROSOFT_ENTRA_ID_TENANT_ID}/v2.0`, - authorization: { - params: { - scope: "openid profile email offline_access", - }, - }, - }), - ], - callbacks: { - async jwt({ token, account }) { - if (account) { - console.log("new sign-in, saving tokens"); - return { - ...token, - access_token: account.access_token, - id_token: account.id_token, - expires_at: account.expires_at, - refresh_token: account.refresh_token, - }; - } - - const shouldRefresh = - !token.expires_at || Date.now() >= (token.expires_at - 60) * 1000; - - if (!shouldRefresh) { - const secondsLeft = token.expires_at - ? Math.floor(token.expires_at - Date.now() / 1000) - : "unknown"; - console.log(`token still valid, ${secondsLeft}s until refresh`); - return token; - } - - console.log("token expired, attempting refresh..."); - - if (!token.refresh_token) { - console.error("no refresh token available"); - return { ...token, error: "RefreshTokenError" }; - } - - try { - const response = await fetch( - `https://login.microsoftonline.com/${process.env.AUTH_MICROSOFT_ENTRA_ID_TENANT_ID}/oauth2/v2.0/token`, - { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - client_id: process.env.AUTH_MICROSOFT_ENTRA_ID_ID!, - client_secret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET!, - grant_type: "refresh_token", - refresh_token: token.refresh_token as string, - }), - }, - ); - - const tokensOrError = await response.json(); - if (!response.ok) throw tokensOrError; - - return { - ...token, - access_token: tokensOrError.access_token, - id_token: tokensOrError.id_token ?? token.id_token, - expires_at: Math.floor(Date.now() / 1000) + tokensOrError.expires_in, - refresh_token: tokensOrError.refresh_token ?? token.refresh_token, - error: undefined, - }; - } catch (error) { - console.error("token refresh failed:", error); - return { ...token, error: "RefreshTokenError" }; - } - }, - - async session({ session, token }) { - session.id_token = token.id_token; - session.error = token.error; - return session; - }, - }, -}); diff --git a/src/components/ui/custom/course-card.tsx b/src/components/ui/custom/course-card.tsx index 87564d3..ac77a3e 100644 --- a/src/components/ui/custom/course-card.tsx +++ b/src/components/ui/custom/course-card.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Card, CardContent, @@ -22,9 +24,11 @@ export type Course = { type CourseCardProps = { course: Course; progress: number; + /** Called when user clicks Resume; if provided, Resume uses this instead of a static link. */ + onResume?: (courseCode: string) => void; }; -function CourseCard({ course, progress }: CourseCardProps) { +function CourseCard({ course, progress, onResume }: CourseCardProps) { return (
@@ -71,9 +75,20 @@ function CourseCard({ course, progress }: CourseCardProps) { - + {onResume ? ( + + ) : ( + + )} ); diff --git a/src/components/ui/custom/macfast-header.tsx b/src/components/ui/custom/macfast-header.tsx index 39049f1..cbab4c0 100644 --- a/src/components/ui/custom/macfast-header.tsx +++ b/src/components/ui/custom/macfast-header.tsx @@ -139,7 +139,7 @@ export function MacFastHeader() { signOut({ redirectTo: "/" })} + onClick={() => signOut({ callbackUrl: "/" })} > Log out @@ -148,7 +148,7 @@ export function MacFastHeader() { ) : (