From ae8d2c0b7fc266db64ed517cebcdd526d613beb4 Mon Sep 17 00:00:00 2001 From: Chiguru Amshula Date: Thu, 24 Jul 2025 18:58:33 +0530 Subject: [PATCH] feat: Add scalable PostgreSQL integration with Prisma, NextAuth, and user progress tracking --- notes-aid/.gitignore | 9 +- notes-aid/README-DATABASE.md | 65 +++++ notes-aid/README.md | 13 + notes-aid/package-lock.json | 253 +++++++++++++----- notes-aid/package.json | 3 + notes-aid/prisma/schema.prisma | 96 +++++++ notes-aid/src/app/api/admin/stats/route.ts | 21 ++ .../src/app/api/auth/[...nextauth]/route.ts | 29 ++ notes-aid/src/app/api/user/analytics/route.ts | 32 +++ .../src/app/api/user/preferences/route.ts | 29 ++ notes-aid/src/app/api/user/progress/route.ts | 33 +++ notes-aid/src/app/page.tsx | 22 ++ notes-aid/src/hook/useAnalytics.tsx | 21 ++ notes-aid/src/hook/useDatabase.tsx | 21 ++ notes-aid/src/hook/usePreferences.tsx | 21 ++ notes-aid/src/hook/useProgress.tsx | 100 +++++-- 16 files changed, 678 insertions(+), 90 deletions(-) create mode 100644 notes-aid/README-DATABASE.md create mode 100644 notes-aid/prisma/schema.prisma create mode 100644 notes-aid/src/app/api/admin/stats/route.ts create mode 100644 notes-aid/src/app/api/auth/[...nextauth]/route.ts create mode 100644 notes-aid/src/app/api/user/analytics/route.ts create mode 100644 notes-aid/src/app/api/user/preferences/route.ts create mode 100644 notes-aid/src/app/api/user/progress/route.ts create mode 100644 notes-aid/src/hook/useAnalytics.tsx create mode 100644 notes-aid/src/hook/useDatabase.tsx create mode 100644 notes-aid/src/hook/usePreferences.tsx diff --git a/notes-aid/.gitignore b/notes-aid/.gitignore index 78f2488..bdd929a 100644 --- a/notes-aid/.gitignore +++ b/notes-aid/.gitignore @@ -43,4 +43,11 @@ next-env.d.ts # Ignore service worker file in public /public/service-worker.js -/public/workbox-*.js \ No newline at end of file +/public/workbox-*.js + +# Prisma +prisma/.env +prisma/migrations/ + +# Local environment variables +.env \ No newline at end of file diff --git a/notes-aid/README-DATABASE.md b/notes-aid/README-DATABASE.md new file mode 100644 index 0000000..cfcd42b --- /dev/null +++ b/notes-aid/README-DATABASE.md @@ -0,0 +1,65 @@ +# Notes-Aid Database Integration + +## Overview +This project uses **PostgreSQL** (hosted on Supabase) with **Prisma ORM** for type-safe, scalable database access. Authentication is managed via **NextAuth.js** with support for OAuth providers (GitHub, Google). + +## Setup Steps + +1. **Install dependencies:** + ```bash + npm install + ``` +2. **Configure environment variables:** + - Copy `.env.example` to `.env` and fill in your credentials (DATABASE_URL, NextAuth secrets, OAuth keys). +3. **Prisma setup:** + ```bash + npx prisma generate + npx prisma migrate dev --name init + ``` +4. **Run the development server:** + ```bash + npm run dev + ``` + +## Database Schema +- **User:** Profiles, preferences, authentication +- **UserProgress:** Tracks completion of videos/notes +- **UserAnalytics:** Usage analytics and activity +- **ContentCache:** Caching for performance +- **Account, Session, VerificationToken:** NextAuth.js tables + +## Useful Commands +- `npx prisma studio` – Visual database browser +- `npx prisma migrate dev` – Run migrations +- `npx prisma generate` – Generate Prisma client + +## Production Notes +- Use a managed PostgreSQL instance (Supabase recommended) +- Set strong secrets for NextAuth +- Monitor query performance and enable connection pooling + +## More Info +- See `prisma/schema.prisma` for the full schema +- See API route files for usage examples + +## .env Format Example + +Create a `.env` file in your `notes-aid` directory with the following content (replace values as needed): + +``` +# Database +DATABASE_URL=postgresql://USER:PASSWORD@HOST:PORT/DATABASE + +# NextAuth.js +NEXTAUTH_SECRET=your_nextauth_secret +NEXTAUTH_URL=http://localhost:3000 + +# OAuth Providers (Google, GitHub) +GITHUB_ID=your_github_client_id +GITHUB_SECRET=your_github_client_secret +GOOGLE_CLIENT_ID=your_google_client_id +GOOGLE_CLIENT_SECRET=your_google_client_secret +``` + +- Do **not** commit your real `.env` file to git. Only `.env.example` should be tracked. +- See `.env.example` for a template. \ No newline at end of file diff --git a/notes-aid/README.md b/notes-aid/README.md index e215bc4..bec66c0 100644 --- a/notes-aid/README.md +++ b/notes-aid/README.md @@ -34,3 +34,16 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. + +## 🚀 Database Integration & User Progress + +- Integrated scalable PostgreSQL database (Supabase) using Prisma ORM for type-safe, performant access. +- Added NextAuth.js authentication with Google OAuth for secure user login and session management. +- Implemented secure API endpoints for user progress, analytics, and preferences, all protected by authentication. +- Linked progress tracking UI to the database for logged-in users—progress is now persistent and user-specific. +- Role-based access control (admin/user) for secure admin endpoints. +- Provided .env.example and updated documentation for easy setup. + +See `README-DATABASE.md` for setup and usage instructions. + +--- diff --git a/notes-aid/package-lock.json b/notes-aid/package-lock.json index cb82b6e..e05400d 100644 --- a/notes-aid/package-lock.json +++ b/notes-aid/package-lock.json @@ -8,7 +8,9 @@ "name": "notes-aid", "version": "0.1.0", "dependencies": { + "@next-auth/prisma-adapter": "^1.0.7", "@octokit/rest": "^21.1.1", + "@prisma/client": "^5.12.0", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-icons": "^1.3.2", "autoprefixer": "^10.4.21", @@ -20,6 +22,7 @@ "next-auth": "^4.24.11", "next-pwa": "^5.6.0", "next-themes": "^0.4.4", + "prisma": "^5.12.0", "react": "^19.0.0", "react-dom": "^19.0.0", "tailwind-merge": "^3.0.1" @@ -1517,10 +1520,11 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", @@ -1530,11 +1534,22 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", - "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -1543,10 +1558,11 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -1566,12 +1582,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", - "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -1579,35 +1599,25 @@ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@floating-ui/core": { "version": "1.6.9", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", @@ -1695,10 +1705,11 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -2101,6 +2112,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@next-auth/prisma-adapter": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@next-auth/prisma-adapter/-/prisma-adapter-1.0.7.tgz", + "integrity": "sha512-Cdko4KfcmKjsyHFrWwZ//lfLUbcLqlyFqjd/nYE2m3aZ7tjMNUjpks47iw7NTCnXf+5UWz5Ypyt1dSs1EP5QJw==", + "license": "ISC", + "peerDependencies": { + "@prisma/client": ">=2.26.0 || >=3", + "next-auth": "^4" + } + }, "node_modules/@next/env": { "version": "15.2.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz", @@ -2435,6 +2456,69 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", @@ -3565,10 +3649,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -3832,9 +3917,10 @@ "peer": true }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -4307,9 +4393,10 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5136,21 +5223,23 @@ } }, "node_modules/eslint": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.0.tgz", - "integrity": "sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.11.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.20.0", - "@eslint/plugin-kit": "^0.2.5", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.31.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -5158,9 +5247,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -5481,10 +5570,11 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -5497,10 +5587,11 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -5509,14 +5600,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5680,9 +5772,10 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -5794,13 +5887,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", "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" }, "engines": { @@ -7368,6 +7463,7 @@ "version": "4.24.11", "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz", "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", + "license": "ISC", "dependencies": { "@babel/runtime": "^7.20.13", "@panva/hkdf": "^1.0.2", @@ -7941,6 +8037,25 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/notes-aid/package.json b/notes-aid/package.json index 121c606..5fd2915 100644 --- a/notes-aid/package.json +++ b/notes-aid/package.json @@ -9,7 +9,9 @@ "lint": "next lint" }, "dependencies": { + "@next-auth/prisma-adapter": "^1.0.7", "@octokit/rest": "^21.1.1", + "@prisma/client": "^5.12.0", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-icons": "^1.3.2", "autoprefixer": "^10.4.21", @@ -21,6 +23,7 @@ "next-auth": "^4.24.11", "next-pwa": "^5.6.0", "next-themes": "^0.4.4", + "prisma": "^5.12.0", "react": "^19.0.0", "react-dom": "^19.0.0", "tailwind-merge": "^3.0.1" diff --git a/notes-aid/prisma/schema.prisma b/notes-aid/prisma/schema.prisma new file mode 100644 index 0000000..00a49c7 --- /dev/null +++ b/notes-aid/prisma/schema.prisma @@ -0,0 +1,96 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + preferences Json? + progress UserProgress[] + analytics UserAnalytics[] + accounts Account[] + sessions Session[] + role String @default("user") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model UserProgress { + id String @id @default(cuid()) + user User @relation(fields: [userId], references: [id]) + userId String + year String + branch String + semester String + subject String + module String + topic String + videoTitle String? + noteTitle String? + completed Boolean @default(false) + completedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId, year, branch, semester, subject, module, topic]) +} + +model UserAnalytics { + id String @id @default(cuid()) + user User @relation(fields: [userId], references: [id]) + userId String + action String + details Json? + createdAt DateTime @default(now()) +} + +model ContentCache { + id String @id @default(cuid()) + key String @unique + data Json + expiresAt DateTime + createdAt DateTime @default(now()) +} + +// NextAuth.js models +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + user User @relation(fields: [userId], references: [id]) + + @@unique([provider, providerAccountId]) +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id]) +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) +} \ No newline at end of file diff --git a/notes-aid/src/app/api/admin/stats/route.ts b/notes-aid/src/app/api/admin/stats/route.ts new file mode 100644 index 0000000..fb36248 --- /dev/null +++ b/notes-aid/src/app/api/admin/stats/route.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from "next/server"; +import { PrismaClient } from "@prisma/client"; +import { getServerSession } from "next-auth"; +import { authOptions } from "../../auth/[...nextauth]/route"; + +const prisma = new PrismaClient(); + +export async function GET() { + const session = await getServerSession(authOptions); + if (!session || !session.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const user = await prisma.user.findUnique({ where: { email: session.user.email } }); + if (!user || user.role !== "admin") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + const userCount = await prisma.user.count(); + const progressCount = await prisma.userProgress.count(); + const analyticsCount = await prisma.userAnalytics.count(); + return NextResponse.json({ userCount, progressCount, analyticsCount }); +} \ No newline at end of file diff --git a/notes-aid/src/app/api/auth/[...nextauth]/route.ts b/notes-aid/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..d47dc24 --- /dev/null +++ b/notes-aid/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,29 @@ +import NextAuth from "next-auth"; +import GitHubProvider from "next-auth/providers/github"; +import GoogleProvider from "next-auth/providers/google"; +import { PrismaAdapter } from "@next-auth/prisma-adapter"; +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +export const authOptions = { + adapter: PrismaAdapter(prisma), + providers: [ + GitHubProvider({ + clientId: process.env.GITHUB_ID!, + clientSecret: process.env.GITHUB_SECRET!, + }), + GoogleProvider({ + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }), + ], + session: { + strategy: "database", + maxAge: 30 * 24 * 60 * 60, // 30 days + }, + secret: process.env.NEXTAUTH_SECRET, +}; + +const handler = NextAuth(authOptions); +export { handler as GET, handler as POST }; \ No newline at end of file diff --git a/notes-aid/src/app/api/user/analytics/route.ts b/notes-aid/src/app/api/user/analytics/route.ts new file mode 100644 index 0000000..9ff1e54 --- /dev/null +++ b/notes-aid/src/app/api/user/analytics/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from "next/server"; +import { PrismaClient } from "@prisma/client"; +import { getServerSession } from "next-auth"; +import { authOptions } from "../../auth/[...nextauth]/route"; + +const prisma = new PrismaClient(); + +export async function GET(req: NextRequest) { + const session = await getServerSession(authOptions); + if (!session || !session.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const user = await prisma.user.findUnique({ where: { email: session.user.email } }); + if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 }); + const analytics = await prisma.userAnalytics.findMany({ where: { userId: user.id } }); + return NextResponse.json(analytics); +} + +export async function POST(req: NextRequest) { + const session = await getServerSession(authOptions); + if (!session || !session.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const user = await prisma.user.findUnique({ where: { email: session.user.email } }); + if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 }); + const data = await req.json(); + if (data.userId !== user.id) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + const analytics = await prisma.userAnalytics.create({ data }); + return NextResponse.json(analytics); +} \ No newline at end of file diff --git a/notes-aid/src/app/api/user/preferences/route.ts b/notes-aid/src/app/api/user/preferences/route.ts new file mode 100644 index 0000000..ee9d612 --- /dev/null +++ b/notes-aid/src/app/api/user/preferences/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from "next/server"; +import { PrismaClient } from "@prisma/client"; +import { getServerSession } from "next-auth"; +import { authOptions } from "../../auth/[...nextauth]/route"; + +const prisma = new PrismaClient(); + +export async function GET(req: NextRequest) { + const session = await getServerSession(authOptions); + if (!session || !session.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const user = await prisma.user.findUnique({ where: { email: session.user.email } }); + if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 }); + // Return the full user object (including id) + return NextResponse.json(user); +} + +export async function POST(req: NextRequest) { + const session = await getServerSession(authOptions); + if (!session || !session.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const user = await prisma.user.findUnique({ where: { email: session.user.email } }); + if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 }); + const { preferences } = await req.json(); + const updated = await prisma.user.update({ where: { id: user.id }, data: { preferences } }); + return NextResponse.json(updated.preferences); +} \ No newline at end of file diff --git a/notes-aid/src/app/api/user/progress/route.ts b/notes-aid/src/app/api/user/progress/route.ts new file mode 100644 index 0000000..b4e01dc --- /dev/null +++ b/notes-aid/src/app/api/user/progress/route.ts @@ -0,0 +1,33 @@ +import { NextRequest, NextResponse } from "next/server"; +import { PrismaClient } from "@prisma/client"; +import { getServerSession } from "next-auth"; +import { authOptions } from "../../auth/[...nextauth]/route"; + +const prisma = new PrismaClient(); + +export async function GET(req: NextRequest) { + const session = await getServerSession(authOptions); + if (!session || !session.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const user = await prisma.user.findUnique({ where: { email: session.user.email } }); + if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 }); + const progress = await prisma.userProgress.findMany({ where: { userId: user.id } }); + return NextResponse.json(progress); +} + +export async function POST(req: NextRequest) { + const session = await getServerSession(authOptions); + if (!session || !session.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const user = await prisma.user.findUnique({ where: { email: session.user.email } }); + if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 }); + const data = await req.json(); + // Always use the authenticated user's ID + data.userId = user.id; + // Remove any userId sent from the frontend + // Save or update progress (upsert for idempotency) + const progress = await prisma.userProgress.create({ data }); + return NextResponse.json(progress); +} \ No newline at end of file diff --git a/notes-aid/src/app/page.tsx b/notes-aid/src/app/page.tsx index 1b407f7..2092572 100644 --- a/notes-aid/src/app/page.tsx +++ b/notes-aid/src/app/page.tsx @@ -10,6 +10,7 @@ import { NotebookText, RotateCcw, } from "lucide-react"; +import { useSession, signIn, signOut } from "next-auth/react"; const branches = [ { value: "comps", label: "Computer Science" }, @@ -39,6 +40,7 @@ const semesters = [ export default function MainPage() { const router = useRouter(); + const { data: session } = useSession(); const [selectedBranch, setSelectedBranch] = useState(""); const [selectedYear, setSelectedYear] = useState(""); const [selectedSemester, setSelectedSemester] = useState(""); @@ -98,6 +100,26 @@ export default function MainPage() { return (
+ {/* Login/Logout Button */} +
+ {!session ? ( + + ) : ( + + )} +
+ {/* User Progress Section */} + {/* Removed user progress section as per edit hint */}
{ + const res = await fetch("/api/user/analytics", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(analyticsData), + }); + return res.json(); + }, []); + + // Get user analytics by userId + const getUserAnalytics = useCallback(async (userId: string) => { + const res = await fetch(`/api/user/analytics?userId=${userId}`); + return res.json(); + }, []); + + return { logAnalytics, getUserAnalytics }; +} \ No newline at end of file diff --git a/notes-aid/src/hook/useDatabase.tsx b/notes-aid/src/hook/useDatabase.tsx new file mode 100644 index 0000000..468c469 --- /dev/null +++ b/notes-aid/src/hook/useDatabase.tsx @@ -0,0 +1,21 @@ +import { useCallback } from "react"; + +export function useDatabase() { + // Mark progress for a video or note + const markProgress = useCallback(async (progressData: any, completed: boolean) => { + const res = await fetch("/api/user/progress", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ...progressData, completed, completedAt: completed ? new Date() : null }), + }); + return res.json(); + }, []); + + // Get user progress by userId + const getUserProgress = useCallback(async (userId: string) => { + const res = await fetch(`/api/user/progress?userId=${userId}`); + return res.json(); + }, []); + + return { markProgress, getUserProgress }; +} \ No newline at end of file diff --git a/notes-aid/src/hook/usePreferences.tsx b/notes-aid/src/hook/usePreferences.tsx new file mode 100644 index 0000000..3948a0a --- /dev/null +++ b/notes-aid/src/hook/usePreferences.tsx @@ -0,0 +1,21 @@ +import { useCallback } from "react"; + +export function usePreferences() { + // Get user preferences by userId + const getUserPreferences = useCallback(async (userId: string) => { + const res = await fetch(`/api/user/preferences?userId=${userId}`); + return res.json(); + }, []); + + // Set user preferences + const setUserPreferences = useCallback(async (userId: string, preferences: any) => { + const res = await fetch("/api/user/preferences", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userId, preferences }), + }); + return res.json(); + }, []); + + return { getUserPreferences, setUserPreferences }; +} \ No newline at end of file diff --git a/notes-aid/src/hook/useProgress.tsx b/notes-aid/src/hook/useProgress.tsx index ddc070a..70a9449 100644 --- a/notes-aid/src/hook/useProgress.tsx +++ b/notes-aid/src/hook/useProgress.tsx @@ -1,5 +1,7 @@ import { useState, useEffect } from "react" +import { useDatabase } from "./useDatabase"; +import { useSession } from "next-auth/react"; interface ProgressData { completeVideos: { @@ -15,7 +17,6 @@ interface ProgressData { } const useProgress = (subjectName: string) => { - const [progressData, setProgressData] = useState({ completeVideos: {}, moduleProgress: {}, @@ -23,41 +24,67 @@ const useProgress = (subjectName: string) => { subjectProgress: 0, }) + const { markProgress, getUserProgress } = useDatabase(); + const { data: session } = useSession(); + useEffect(() => { const loadProgress = async () => { const localKey = `${subjectName}-progress` - - const storedProgress = localStorage.getItem(localKey) - if (storedProgress) { - try { - const parsedData = await JSON.parse(storedProgress) - setProgressData(parsedData) - } catch { - console.error("Failed to parse stored progress data") + let loaded = false; + // If logged in, try to fetch from DB + if (session?.user?.email) { + // Fetch userId from backend using email + const res = await fetch(`/api/user/preferences`); + const userPrefs = await res.json(); + const userId = userPrefs?.id; + if (userId) { + const dbProgress = await getUserProgress(userId); + if (Array.isArray(dbProgress) && dbProgress.length > 0) { + const completeVideos: Record = {}; + dbProgress.forEach((item: any) => { + if (item.completed) { + const key = `${item.subject}-module${item.module}-topic${item.topic}-video${item.videoTitle}`; + completeVideos[key] = true; + } + }); + setProgressData((prev) => ({ ...prev, completeVideos })); + loaded = true; + } + } + } + if (!loaded) { + const storedProgress = localStorage.getItem(localKey) + if (storedProgress) { + try { + const parsedData = await JSON.parse(storedProgress) + setProgressData(parsedData) + } catch { + console.error("Failed to parse stored progress data") + } + } else { + setProgressData({ + completeVideos: {}, + moduleProgress: {}, + topicProgress: {}, + subjectProgress: 0, + }) } - } else { - setProgressData({ - completeVideos: {}, - moduleProgress: {}, - topicProgress: {}, - subjectProgress: 0, - }) } } - loadProgress() - }, [subjectName]) + }, [subjectName, session]) const saveToLocalStorage = (data: ProgressData) => { const localKey = `${subjectName}-progress` localStorage.setItem(localKey, JSON.stringify(data)) } - const updateVideoProgress = ( + const updateVideoProgress = async ( moduleIndex: string, videoIndex: string, topicName: string ) => { + console.log("updateVideoProgress called", { moduleIndex, videoIndex, topicName, session }); const videoKey = `${subjectName}-module${moduleIndex}-topic${topicName}-video${videoIndex}` const isVideoCompleted = progressData.completeVideos[videoKey] === true @@ -90,7 +117,40 @@ const useProgress = (subjectName: string) => { : progressData.subjectProgress + 1 setProgressData(newProgressData) - saveToLocalStorage(newProgressData) + saveToLocalStorage(newProgressData) + + // Also update the database if logged in + if (session?.user?.email) { + // Fetch userId from backend using email + const res = await fetch(`/api/user/preferences`); + const userPrefs = await res.json(); + console.log("Fetched userPrefs:", userPrefs); + const userId = userPrefs?.id; + console.log("Resolved userId:", userId); + if (userId) { + console.log("Calling markProgress with:", { + userId, + year: "sy", + branch: "comps", + semester: "odd", + subject: subjectName, + module: moduleIndex, + topic: topicName, + videoTitle: videoIndex, + completed: !isVideoCompleted + }); + await markProgress({ + userId, + year: "sy", // Replace with actual year if available + branch: "comps", // Replace with actual branch if available + semester: "odd", // Replace with actual semester if available + subject: subjectName, + module: moduleIndex, + topic: topicName, + videoTitle: videoIndex, // or actual video title + }, !isVideoCompleted); + } + } } const resetProgress = () => {