diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b8659e6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,77 @@ +name: AIS Portal CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +# Cancel in-progress runs for the same PR/branch when new commits are pushed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + pr-validation: + name: PR validation + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v7.0.1 + + - name: Setup Node.js environment + uses: actions/setup-node@v7.0.0 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Validate Prisma schema + run: npx prisma validate + + - name: Generate Prisma client + run: npx prisma generate + + - name: Run ESLint + run: npm run lint + + - name: Type check + run: npx tsc --noEmit + + # Add unit tests here if applicable: + # - name: Run tests + # run: npm test + + - name: Production build + run: npm run build + + db-smoke: + name: Database smoke check + if: github.event_name == 'push' + runs-on: ubuntu-latest + + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + DIRECT_URL: ${{ secrets.DIRECT_URL }} + + steps: + - name: Checkout repository + uses: actions/checkout@v7.0.1 + + - name: Setup Node.js environment + uses: actions/setup-node@v7.0.0 + with: + node-version: 24 + cache: npm + + - name: Install Dependencies + run: npm ci + + - name: Check Database Connectivity & Migration Status + run: npx prisma migrate status \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b6dfe80..ecb98e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "@prisma/adapter-neon": "^7.8.0", "@prisma/client": "^7.8.0", "@sentry/nextjs": "^10.58.0", + "@vercel/analytics": "^2.0.1", + "jose": "^6.2.4", "next": "16.2.9", "react": "19.2.4", "react-dom": "19.2.4", @@ -522,7 +524,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -803,8 +804,7 @@ "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", "devOptional": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@electric-sql/pglite-socket": { "version": "0.1.1", @@ -829,6 +829,28 @@ "@electric-sql/pglite": "0.4.1" } }, + "node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -1563,6 +1585,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -1828,7 +1851,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -1850,7 +1872,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -1866,7 +1887,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.214.0.tgz", "integrity": "sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.214.0", "import-in-the-middle": "^3.0.0", @@ -1900,7 +1920,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", @@ -1918,7 +1937,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } @@ -2351,7 +2369,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3625,7 +3642,6 @@ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3634,9 +3650,8 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3696,7 +3711,6 @@ "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/types": "8.61.0", @@ -4294,11 +4308,54 @@ "win32" ] }, + "node_modules/@vercel/analytics": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-2.0.1.tgz", + "integrity": "sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g==", + "license": "MIT", + "peerDependencies": { + "@remix-run/react": "^2", + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "nuxt": ">= 3", + "react": "^18 || ^19 || ^19.0.0-rc", + "svelte": ">= 4", + "vue": "^3", + "vue-router": "^4" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "nuxt": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -4308,25 +4365,29 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -4337,13 +4398,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4356,6 +4419,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "license": "MIT", + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -4365,6 +4429,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -4373,13 +4438,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4396,6 +4463,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -4409,6 +4477,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4421,6 +4490,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -4435,6 +4505,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -4444,20 +4515,21 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4479,6 +4551,7 @@ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" }, @@ -4530,6 +4603,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -4547,6 +4621,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4562,7 +4637,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/ansi-styles": { "version": "4.3.0", @@ -4907,7 +4983,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -4926,7 +5001,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/c12": { "version": "3.3.4", @@ -5101,6 +5177,7 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.0" } @@ -5141,7 +5218,8 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/commondir": { "version": "1.0.1", @@ -5586,7 +5664,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/es-object-atoms": { "version": "1.1.2", @@ -5676,7 +5755,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5862,7 +5940,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6087,6 +6164,7 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.x" } @@ -6758,7 +6836,6 @@ "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", "devOptional": true, "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -7334,6 +7411,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -7348,6 +7426,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -7368,6 +7447,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.4.tgz", + "integrity": "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-cookie": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.7.tgz", @@ -7777,6 +7865,7 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.11.5" }, @@ -7875,7 +7964,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/merge2": { "version": "1.4.1", @@ -7906,6 +7996,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -8033,14 +8124,14 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/next": { "version": "16.2.9", "resolved": "https://registry.npmjs.org/next/-/next-16.2.9.tgz", "integrity": "sha512-MEOJiq/UvuezAdqVSceHbqDgZt1kDw2tpGVOlsdIoJsQdbN2JY2hpVG4xnXGkbdJUOEWhnRfiu/O4Hpc9Juwww==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "16.2.9", "@swc/helpers": "0.5.15", @@ -8564,7 +8655,6 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "7.8.0", "@prisma/dev": "0.24.3", @@ -8702,7 +8792,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8712,7 +8801,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -8887,7 +8975,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.9" }, @@ -9024,6 +9111,7 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -9060,6 +9148,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -9071,7 +9160,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/semver": { "version": "6.3.1", @@ -9318,6 +9408,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9336,6 +9427,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -9636,6 +9728,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -9654,6 +9747,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -9750,7 +9844,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -9928,7 +10021,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10084,6 +10176,7 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2" }, @@ -10102,6 +10195,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz", "integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", @@ -10148,6 +10242,7 @@ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" } @@ -10157,6 +10252,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -10170,6 +10266,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -10369,7 +10466,6 @@ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 265d228..ea46622 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "@prisma/adapter-neon": "^7.8.0", "@prisma/client": "^7.8.0", "@sentry/nextjs": "^10.58.0", + "@vercel/analytics": "^2.0.1", + "jose": "^6.2.4", "next": "16.2.9", "react": "19.2.4", "react-dom": "19.2.4", diff --git a/prisma/migrations/0_init/migration.sql b/prisma/migrations/0_init/migration.sql new file mode 100644 index 0000000..bfee190 Binary files /dev/null and b/prisma/migrations/0_init/migration.sql differ diff --git a/prisma/migrations/20260616074329_init/migration.sql b/prisma/migrations/20260616074329_init/migration.sql deleted file mode 100644 index 9ca9105..0000000 --- a/prisma/migrations/20260616074329_init/migration.sql +++ /dev/null @@ -1,279 +0,0 @@ --- CreateEnum -CREATE TYPE "UserRole" AS ENUM ('MEMBER', 'REVIEWER', 'ORGANIZER', 'SUPER_ADMIN'); - --- CreateEnum -CREATE TYPE "ProfileCompletionStatus" AS ENUM ('INCOMPLETE', 'COMPLETE'); - --- CreateEnum -CREATE TYPE "EventStatus" AS ENUM ('UPCOMING', 'LIVE', 'CLOSED', 'ARCHIVED'); - --- CreateEnum -CREATE TYPE "RSVPStatus" AS ENUM ('GOING', 'CANCELED'); - --- CreateEnum -CREATE TYPE "AttendanceMethod" AS ENUM ('QR_SCAN', 'MANUAL'); - --- CreateEnum -CREATE TYPE "ApplicationStatus" AS ENUM ('DRAFT', 'SUBMITTED', 'IN_REVIEW', 'IN_CONSIDERATION', 'ACCEPTED', 'REJECTED', 'WAITLISTED', 'COMPLETED', 'ARCHIVED'); - --- CreateEnum -CREATE TYPE "ProgramType" AS ENUM ('AI_ACADEMY', 'AI_INNOVATION', 'AI_MENTORSHIP_MENTOR', 'AI_MENTORSHIP_MENTEE'); - --- CreateEnum -CREATE TYPE "MembershipType" AS ENUM ('NON_MEMBER', 'AI_STUDENT', 'AIM_MENTOR', 'AIM_MENTEE', 'AI_INNOVATOR', 'ALUMNUS'); - --- CreateTable -CREATE TABLE "User" ( - "id" TEXT NOT NULL, - "clerkId" TEXT NOT NULL, - "email" TEXT NOT NULL, - "role" "UserRole" NOT NULL DEFAULT 'MEMBER', - "verifiedAt" TIMESTAMP(3), - "lastLoginAt" TIMESTAMP(3), - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Profile" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "firstName" TEXT NOT NULL, - "lastName" TEXT NOT NULL, - "middleName" TEXT NOT NULL, - "prefName" TEXT NOT NULL, - "year" TEXT NOT NULL, - "degree" TEXT NOT NULL, - "major" TEXT NOT NULL, - "utdEmail" TEXT, - "utdNetId" TEXT, - "githubUrl" TEXT, - "linkedinUrl" TEXT, - "portfolioUrl" TEXT, - "profileCompletionStatus" "ProfileCompletionStatus" NOT NULL, - "resumeFileId" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Profile_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "File" ( - "id" TEXT NOT NULL, - "uploadedById" TEXT, - "fileName" TEXT NOT NULL, - "mimeType" TEXT NOT NULL, - "fileSize" INTEGER NOT NULL, - "storageKey" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "File_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Event" ( - "id" TEXT NOT NULL, - "title" TEXT NOT NULL, - "description" TEXT NOT NULL, - "location" TEXT NOT NULL, - "startTime" TIMESTAMP(3) NOT NULL, - "endTime" TIMESTAMP(3) NOT NULL, - "status" "EventStatus" NOT NULL, - "capacity" INTEGER, - "visibility" TEXT, - "eligibilityRules" JSONB, - "createdById" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Event_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "RSVP" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "eventId" TEXT NOT NULL, - "status" "RSVPStatus" NOT NULL, - "qrToken" TEXT NOT NULL, - "qrExpiresAt" TIMESTAMP(3), - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "RSVP_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Attendance" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "eventId" TEXT NOT NULL, - "checkedInById" TEXT, - "checkedInAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "method" "AttendanceMethod" NOT NULL, - - CONSTRAINT "Attendance_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "ProgramApplication" ( - "id" TEXT NOT NULL, - "title" TEXT NOT NULL, - "description" TEXT NOT NULL, - "programType" "ProgramType" NOT NULL, - "openAt" TIMESTAMP(3) NOT NULL, - "closeAt" TIMESTAMP(3) NOT NULL, - "decisionDate" TIMESTAMP(3), - "visibleToUsers" BOOLEAN NOT NULL DEFAULT true, - "retentionUntil" TIMESTAMP(3), - "createdById" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "ProgramApplication_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "ApplicationDraft" ( - "id" TEXT NOT NULL, - "applicationId" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "formPayloadJson" JSONB NOT NULL, - "stepIndex" INTEGER NOT NULL, - "isSubmitted" BOOLEAN NOT NULL DEFAULT false, - "lastSavedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "ApplicationDraft_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "ApplicationSubmission" ( - "id" TEXT NOT NULL, - "applicationId" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "versionNumber" INTEGER NOT NULL, - "formPayloadJson" JSONB NOT NULL, - "pdfFileId" TEXT, - "status" "ApplicationStatus" NOT NULL, - "submittedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "ApplicationSubmission_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "ApplicationReview" ( - "id" TEXT NOT NULL, - "submissionId" TEXT NOT NULL, - "reviewerId" TEXT NOT NULL, - "status" "ApplicationStatus" NOT NULL, - "notesInternal" TEXT, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "ApplicationReview_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Membership" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "membershipType" "MembershipType" NOT NULL, - "startDate" TIMESTAMP(3) NOT NULL, - "endDate" TIMESTAMP(3), - "activeFlag" BOOLEAN NOT NULL DEFAULT false, - "sourceApplicationId" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Membership_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "AuditLog" ( - "id" TEXT NOT NULL, - "actorUserId" TEXT, - "actionType" TEXT NOT NULL, - "entityType" TEXT NOT NULL, - "entityId" TEXT NOT NULL, - "metadataJson" JSONB, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "User_clerkId_key" ON "User"("clerkId"); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- CreateIndex -CREATE UNIQUE INDEX "Profile_userId_key" ON "Profile"("userId"); - --- CreateIndex -CREATE UNIQUE INDEX "RSVP_userId_eventId_key" ON "RSVP"("userId", "eventId"); - --- CreateIndex -CREATE UNIQUE INDEX "Attendance_userId_eventId_key" ON "Attendance"("userId", "eventId"); - --- CreateIndex -CREATE UNIQUE INDEX "ApplicationDraft_applicationId_userId_key" ON "ApplicationDraft"("applicationId", "userId"); - --- AddForeignKey -ALTER TABLE "Profile" ADD CONSTRAINT "Profile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Profile" ADD CONSTRAINT "Profile_resumeFileId_fkey" FOREIGN KEY ("resumeFileId") REFERENCES "File"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "File" ADD CONSTRAINT "File_uploadedById_fkey" FOREIGN KEY ("uploadedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Event" ADD CONSTRAINT "Event_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "RSVP" ADD CONSTRAINT "RSVP_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "RSVP" ADD CONSTRAINT "RSVP_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Attendance" ADD CONSTRAINT "Attendance_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Attendance" ADD CONSTRAINT "Attendance_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Attendance" ADD CONSTRAINT "Attendance_checkedInById_fkey" FOREIGN KEY ("checkedInById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ProgramApplication" ADD CONSTRAINT "ProgramApplication_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ApplicationDraft" ADD CONSTRAINT "ApplicationDraft_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "ProgramApplication"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ApplicationDraft" ADD CONSTRAINT "ApplicationDraft_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ApplicationSubmission" ADD CONSTRAINT "ApplicationSubmission_pdfFileId_fkey" FOREIGN KEY ("pdfFileId") REFERENCES "File"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ApplicationSubmission" ADD CONSTRAINT "ApplicationSubmission_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "ProgramApplication"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ApplicationSubmission" ADD CONSTRAINT "ApplicationSubmission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ApplicationReview" ADD CONSTRAINT "ApplicationReview_submissionId_fkey" FOREIGN KEY ("submissionId") REFERENCES "ApplicationSubmission"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ApplicationReview" ADD CONSTRAINT "ApplicationReview_reviewerId_fkey" FOREIGN KEY ("reviewerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Membership" ADD CONSTRAINT "Membership_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_actorUserId_fkey" FOREIGN KEY ("actorUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml deleted file mode 100644 index 044d57c..0000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (e.g., Git) -provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e01e5b8..666fb2c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -39,6 +39,13 @@ enum AttendanceMethod { MANUAL } +enum ItemType { + MEAL + DRINK + MERCH + OTHER +} + enum ApplicationStatus { DRAFT SUBMITTED @@ -85,17 +92,16 @@ model User { profile Profile? memberships Membership[] rsvps RSVP[] - attendance Attendance[] submissions ApplicationSubmission[] drafts ApplicationDraft[] reviews ApplicationReview[] // Relations mapped from other models - uploadedFiles File[] - createdEvents Event[] - createdApps ProgramApplication[] - checkedInEvents Attendance[] @relation("AttendanceChecker") - auditLogs AuditLog[] @relation("AuditActor") + uploadedFiles File[] + createdEvents Event[] + createdApps ProgramApplication[] + itemScans ItemScan[] @relation("ItemScanner") + auditLogs AuditLog[] @relation("AuditActor") } model Profile { @@ -155,8 +161,8 @@ model Event { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - rsvps RSVP[] - attendance Attendance[] + rsvps RSVP[] + items EventItem[] } model RSVP { @@ -166,28 +172,51 @@ model RSVP { status RSVPStatus qrToken String qrExpiresAt DateTime? + qrPayload String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id]) - event Event @relation(fields: [eventId], references: [id]) + user User @relation(fields: [userId], references: [id]) + event Event @relation(fields: [eventId], references: [id]) + attendance Attendance? @@unique([userId, eventId]) } model Attendance { - id String @id @default(cuid()) - userId String - eventId String - checkedInById String? - checkedInAt DateTime @default(now()) - method AttendanceMethod + id String @id @default(cuid()) + rsvpId String @unique + rsvp RSVP @relation(fields: [rsvpId], references: [id], onDelete: Cascade) + checkedInAt DateTime @default(now()) + method AttendanceMethod? + qrTokenUsed String? + + scans ItemScan[] +} + +model EventItem { + id String @id @default(cuid()) + eventId String + event Event @relation(fields: [eventId], references: [id], onDelete: Cascade) + name String + type ItemType - user User @relation(fields: [userId], references: [id]) - event Event @relation(fields: [eventId], references: [id]) - checkedInBy User? @relation("AttendanceChecker", fields: [checkedInById], references: [id]) + scans ItemScan[] - @@unique([userId, eventId]) + @@unique([eventId, name]) +} + +model ItemScan { + id String @id @default(cuid()) + attendanceId String + attendance Attendance @relation(fields: [attendanceId], references: [id], onDelete: Cascade) + eventItemId String + eventItem EventItem @relation(fields: [eventItemId], references: [id], onDelete: Cascade) + scannedAt DateTime @default(now()) + scannedById String? + scannedBy User? @relation("ItemScanner", fields: [scannedById], references: [id]) + + @@unique([attendanceId, eventItemId]) } model ProgramApplication { diff --git a/prisma/seed.ts b/prisma/seed.ts index 3c6a87a..da28920 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -69,7 +69,7 @@ async function main() { }, }); - const reviewer = await prisma.user.upsert({ + await prisma.user.upsert({ where: { email: 'reviewer@utdallas.edu' }, update: {}, create: { diff --git a/src/app/api/events/[id]/rsvp/route.ts b/src/app/api/events/[id]/rsvp/route.ts index 97c0506..d4789e2 100644 --- a/src/app/api/events/[id]/rsvp/route.ts +++ b/src/app/api/events/[id]/rsvp/route.ts @@ -1,5 +1,78 @@ import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { getAuthenticatedUser } from "@/lib/auth"; +import crypto from "crypto"; -export async function GET() { - return NextResponse.json({ message: "Draft route placeholder" }); +export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) { + const user = await getAuthenticatedUser(); + + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id: eventId } = await params; + const existingEvent = await prisma.event.findUnique({ where: { id: eventId } }); + + if (!existingEvent) { + return NextResponse.json({ error: "Event not found" }, { status: 404 }); + } + + const existingRsvp = await prisma.rSVP.findUnique({ + where: { + userId_eventId: { + userId: user.id, + eventId, + }, + }, + }); + + if (existingRsvp?.status === "GOING") { + return NextResponse.json({ error: "Already RSVP'd" }, { status: 409 }); + } + + const tokenPayload = `${user.id}:${eventId}:${Date.now()}`; + const qrToken = crypto.createHash("sha256").update(tokenPayload).digest("hex"); + + const rsvp = await prisma.rSVP.create({ + data: { + userId: user.id, + eventId, + status: "GOING", + qrToken, + qrPayload: JSON.stringify({ userId: user.id, eventId, token: qrToken }), + qrExpiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), + }, + }); + + return NextResponse.json({ success: true, rsvp }); +} + +export async function DELETE(_request: Request, { params }: { params: Promise<{ id: string }> }) { + const user = await getAuthenticatedUser(); + + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id: eventId } = await params; + + const existing = await prisma.rSVP.findUnique({ + where: { + userId_eventId: { + userId: user.id, + eventId, + }, + }, + }); + + if (!existing) { + return NextResponse.json({ error: "RSVP not found" }, { status: 404 }); + } + + await prisma.rSVP.update({ + where: { id: existing.id }, + data: { status: "CANCELED" }, + }); + + return NextResponse.json({ success: true }); } diff --git a/src/app/api/events/route.ts b/src/app/api/events/route.ts new file mode 100644 index 0000000..31955ae --- /dev/null +++ b/src/app/api/events/route.ts @@ -0,0 +1,11 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export async function GET() { + const events = await prisma.event.findMany({ + orderBy: { startTime: "asc" }, + take: 20, + }); + + return NextResponse.json(events); +} diff --git a/src/app/api/webhooks/clerk/route.ts b/src/app/api/webhooks/clerk/route.ts index aec8c2f..969f884 100644 --- a/src/app/api/webhooks/clerk/route.ts +++ b/src/app/api/webhooks/clerk/route.ts @@ -5,18 +5,27 @@ import { prisma } from '@/lib/prisma'; import { UserRole } from '@prisma/client'; export async function POST(req: Request) { + console.log('--- WEBHOOK REQUEST RECEIVED ---'); + const headerPayload = await headers(); const svix_id = headerPayload.get("svix-id"); const svix_timestamp = headerPayload.get("svix-timestamp"); const svix_signature = headerPayload.get("svix-signature"); if (!svix_id || !svix_timestamp || !svix_signature) { + console.error("Missing svix headers"); return new Response('Error occurred -- missing svix headers', { status: 400 }); } - const payload = await req.json(); - const body = JSON.stringify(payload); - const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!); + const body = await req.text(); + const webhookSecret = process.env.CLERK_WEBHOOK_SECRET; // CHANGE DURING DEV TO WEBHOOK_SECRET + + if (!webhookSecret) { + console.error("CRITICAL ERROR: CLERK_WEBHOOK_SECRET is not loaded in process.env!"); + return new Response('Missing secret in environment variables', { status: 500 }); + } + + const wh = new Webhook(webhookSecret); let evt: WebhookEvent; try { @@ -26,27 +35,37 @@ export async function POST(req: Request) { "svix-signature": svix_signature, }) as WebhookEvent; } catch (err) { - return new Response('Error occurred', { status: 400 }); + console.error("Svix verification failed with error:", err); + return new Response('Error occurred during verification', { status: 400 }); } - // Handle user creation sync + console.log(`Webhook verified! Event type: ${evt.type}`); + if (evt.type === 'user.created') { const { id, email_addresses } = evt.data; const primaryEmail = email_addresses[0]?.email_address; if (!primaryEmail) { + console.error("User has no email address"); return new Response('Error: User has no email address', { status: 400 }); } - await prisma.user.create({ - data: { - id: id, // Maps Clerk ID to Primary Key - clerkId: id, // Schema's unique clerkId constraint - email: primaryEmail, - role: UserRole.MEMBER, - }, - }); + try { + console.log(`Attempting to create user ${id} in Prisma...`); + await prisma.user.create({ + data: { + id: id, + clerkId: id, + email: primaryEmail, + role: UserRole.MEMBER, + }, + }); + console.log("Successfully created user in Prisma!"); + } catch (dbError) { + console.error("Prisma failed to create user. Error details:", dbError); + return new Response('Database error', { status: 500 }); + } } return new Response('', { status: 200 }); -} +} \ No newline at end of file diff --git a/src/app/events/browse/page.tsx b/src/app/events/browse/page.tsx index 6cb5b09..8fa1b42 100644 --- a/src/app/events/browse/page.tsx +++ b/src/app/events/browse/page.tsx @@ -1,15 +1,27 @@ +export const dynamic = "force-dynamic"; + import type { Metadata } from "next"; import { Navbar } from "@/components/navbar"; import { Tag } from "@/components/ui/tag"; import { EventGridCard } from "@/components/events/event-grid-card"; -import { eventFilterTags, browseEvents } from "@/lib/data"; +import { eventFilterTags } from "@/lib/data"; +import { prisma } from "@/lib/prisma"; export const metadata: Metadata = { title: "Events — Browse", description: "Browse upcoming AIS events by tag.", }; -export default function EventsBrowsePage() { +async function getEvents() { + return prisma.event.findMany({ + orderBy: { startTime: "asc" }, + take: 20, + }); +} + +export default async function EventsBrowsePage() { + const events = await getEvents(); + return (
{message}
: null} ++ we sent a 6-digit code to {email} +
+- we'll send a link to validate your email + {isSignUp ? "we'll email you a 6-digit code to verify your account" : "welcome back to AIS"}