diff --git a/.gitignore b/.gitignore index 62d7ee4..68bb25e 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ next-env.d.ts detaileplans.md detail.md + +/src/generated/prisma +AUTH_SETUP.md \ No newline at end of file diff --git a/env.example b/env.example index 26b8eba..92e62c4 100644 --- a/env.example +++ b/env.example @@ -1,9 +1,30 @@ # GitHub API Configuration -GITHUB_TOKEN= +REPO_TOKEN= GITHUB_APP_ID= GITHUB_APP_PRIVATE_KEY= GITHUB_CLINET_ID= # Next.js Configuration NEXT_PUBLIC_APP_NAME=GitIntel -NEXT_PUBLIC_APP_DESCRIPTION=Analyze competition in open source GitHub projects \ No newline at end of file +NEXT_PUBLIC_APP_DESCRIPTION=Analyze competition in open source GitHub projects + +# Database Configuration +DATABASE_URL= + +# NextAuth.js Configuration +NEXTAUTH_URL=http://localhost:3000 +NEXTAUTH_SECRET= + +# OAuth Providers +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= + +# Email Service (Resend) +RESEND_API_KEY= +RESEND_FROM_EMAIL=noreply@yourdomain.com + +# App Configuration +NEXT_PUBLIC_APP_URL=http://localhost:3000 \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index cf2b34c..c999a28 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -23,6 +23,26 @@ const eslintConfig = [ "@typescript-eslint/no-explicit-any": "warn", }, }, + { + files: ["src/generated/**/*"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/prefer-as-const": "off", + "@typescript-eslint/no-empty-object-type": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-require-imports": "off", + "@typescript-eslint/no-unused-expressions": "off", + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-unsafe-function-type": "off", + "@typescript-eslint/no-unnecessary-type-constraint": "off", + "@typescript-eslint/no-wrapper-object-types": "off", + "@typescript-eslint/no-unsafe-function-type": "off", + "prefer-const": "off", + "no-var": "off", + }, + }, ]; export default eslintConfig; diff --git a/next.config.ts b/next.config.ts index c6ec757..13cf936 100644 --- a/next.config.ts +++ b/next.config.ts @@ -9,6 +9,24 @@ const nextConfig: NextConfig = { port: '', pathname: '/**', }, + { + protocol: 'https', + hostname: 'lh3.googleusercontent.com', + port: '', + pathname: '/**', + }, + { + protocol: 'https', + hostname: 'googleusercontent.com', + port: '', + pathname: '/**', + }, + { + protocol: 'https', + hostname: '*.googleusercontent.com', + port: '', + pathname: '/**', + }, ], }, }; diff --git a/package.json b/package.json index c16f966..3614287 100644 --- a/package.json +++ b/package.json @@ -9,25 +9,40 @@ "lint": "next lint" }, "dependencies": { + "@auth/prisma-adapter": "^2.10.0", + "@hookform/resolvers": "^5.1.1", + "@neondatabase/serverless": "^1.0.1", "@octokit/auth-app": "^8.0.1", "@octokit/graphql": "^9.0.1", "@octokit/rest": "^21.1.1", + "@prisma/client": "^6.10.1", "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", + "@types/nodemailer": "^6.4.17", + "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", "dotenv": "^16.5.0", "lucide-react": "^0.511.0", "next": "15.3.2", + "next-auth": "^4.24.11", + "nodemailer": "^7.0.3", + "prisma": "^6.10.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.58.1", + "react-icons": "^5.5.0", "recharts": "^2.15.3", + "resend": "^4.6.0", "tailwind-merge": "^3.3.0", - "vaul": "^1.1.2" + "vaul": "^1.1.2", + "zod": "^3.25.67" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f847822..4cd7773 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,15 @@ importers: .: dependencies: + '@auth/prisma-adapter': + specifier: ^2.10.0 + version: 2.10.0(@prisma/client@6.10.1(prisma@6.10.1(typescript@5.8.3))(typescript@5.8.3))(nodemailer@7.0.3) + '@hookform/resolvers': + specifier: ^5.1.1 + version: 5.1.1(react-hook-form@7.58.1(react@19.1.0)) + '@neondatabase/serverless': + specifier: ^1.0.1 + version: 1.0.1 '@octokit/auth-app': specifier: ^8.0.1 version: 8.0.1 @@ -17,21 +26,36 @@ importers: '@octokit/rest': specifier: ^21.1.1 version: 21.1.1 + '@prisma/client': + specifier: ^6.10.1 + version: 6.10.1(prisma@6.10.1(typescript@5.8.3))(typescript@5.8.3) '@radix-ui/react-dialog': specifier: ^1.1.14 version: 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.15 + version: 2.1.15(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-progress': specifier: ^1.1.7 version: 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-select': specifier: ^2.2.5 version: 2.2.5(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-separator': + specifier: ^1.1.7 + version: 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.5)(react@19.1.0) '@radix-ui/react-tabs': specifier: ^1.1.12 version: 1.1.12(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@types/nodemailer': + specifier: ^6.4.17 + version: 6.4.17 + bcryptjs: + specifier: ^3.0.2 + version: 3.0.2 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -50,21 +74,42 @@ importers: next: specifier: 15.3.2 version: 15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next-auth: + specifier: ^4.24.11 + version: 4.24.11(next@15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + nodemailer: + specifier: ^7.0.3 + version: 7.0.3 + prisma: + specifier: ^6.10.1 + version: 6.10.1(typescript@5.8.3) react: specifier: ^19.0.0 version: 19.1.0 react-dom: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) + react-hook-form: + specifier: ^7.58.1 + version: 7.58.1(react@19.1.0) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@19.1.0) recharts: specifier: ^2.15.3 version: 2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + resend: + specifier: ^4.6.0 + version: 4.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) tailwind-merge: specifier: ^3.3.0 version: 3.3.0 vaul: specifier: ^1.1.2 version: 1.1.2(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + zod: + specifier: ^3.25.67 + version: 3.25.67 devDependencies: '@eslint/eslintrc': specifier: ^3 @@ -107,6 +152,25 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@auth/core@0.40.0': + resolution: {integrity: sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==} + 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 + + '@auth/prisma-adapter@2.10.0': + resolution: {integrity: sha512-EliOQoTjGK87jWWqnJvlQjbR4PjQZQqtwRwPAe108WwT9ubuuJJIrL68aNnQr4hFESz6P7SEX2bZy+y2yL37Gw==} + peerDependencies: + '@prisma/client': '>=2.26.0 || >=3 || >=4 || >=5 || >=6' + '@babel/runtime@7.27.1': resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==} engines: {node: '>=6.9.0'} @@ -173,6 +237,11 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@hookform/resolvers@5.1.1': + resolution: {integrity: sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==} + peerDependencies: + react-hook-form: ^7.55.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -334,6 +403,10 @@ packages: '@napi-rs/wasm-runtime@0.2.10': resolution: {integrity: sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ==} + '@neondatabase/serverless@1.0.1': + resolution: {integrity: sha512-O6yC5TT0jbw86VZVkmnzCZJB0hfxBl0JJz6f+3KHoZabjb/X08r9eFA+vuY06z1/qaovykvdkrXYq3SPUuvogA==} + engines: {node: '>=19.0.0'} + '@next/env@15.3.2': resolution: {integrity: sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g==} @@ -502,6 +575,39 @@ packages: '@octokit/types@14.0.0': resolution: {integrity: sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==} + '@panva/hkdf@1.2.1': + resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} + + '@prisma/client@6.10.1': + resolution: {integrity: sha512-Re4pMlcUsQsUTAYMK7EJ4Bw2kg3WfZAAlr8GjORJaK4VOP6LxRQUQ1TuLnxcF42XqGkWQ36q5CQF1yVadANQ6w==} + engines: {node: '>=18.18'} + peerDependencies: + prisma: '*' + typescript: '>=5.1.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@6.10.1': + resolution: {integrity: sha512-kz4/bnqrOrzWo8KzYguN0cden4CzLJJ+2VSpKtF8utHS3l1JS0Lhv6BLwpOX6X9yNreTbZQZwewb+/BMPDCIYQ==} + + '@prisma/debug@6.10.1': + resolution: {integrity: sha512-k2YT53cWxv9OLjW4zSYTZ6Z7j0gPfCzcr2Mj99qsuvlxr8WAKSZ2NcSR0zLf/mP4oxnYG842IMj3utTgcd7CaA==} + + '@prisma/engines-version@6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c': + resolution: {integrity: sha512-ZJFTsEqapiTYVzXya6TUKYDFnSWCNegfUiG5ik9fleQva5Sk3DNyyUi7X1+0ZxWFHwHDr6BZV5Vm+iwP+LlciA==} + + '@prisma/engines@6.10.1': + resolution: {integrity: sha512-Q07P5rS2iPwk2IQr/rUQJ42tHjpPyFcbiH7PXZlV81Ryr9NYIgdxcUrwgVOWVm5T7ap02C0dNd1dpnNcSWig8A==} + + '@prisma/fetch-engine@6.10.1': + resolution: {integrity: sha512-clmbG/Jgmrc/n6Y77QcBmAUlq9LrwI9Dbgy4pq5jeEARBpRCWJDJ7PWW1P8p0LfFU0i5fsyO7FqRzRB8mkdS4g==} + + '@prisma/get-platform@6.10.1': + resolution: {integrity: sha512-4CY5ndKylcsce9Mv+VWp5obbR2/86SHOLVV053pwIkhVtT9C9A83yqiqI/5kJM9T1v1u1qco/bYjDKycmei9HA==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -587,6 +693,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dropdown-menu@2.1.15': + resolution: {integrity: sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.1.2': resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} peerDependencies: @@ -618,6 +737,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-menu@2.1.15': + resolution: {integrity: sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popper@1.2.7': resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==} peerDependencies: @@ -709,6 +841,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: @@ -819,12 +964,25 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@react-email/render@1.1.2': + resolution: {integrity: sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^18.0 || ^19.0 || ^19.0.0-rc + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} '@rushstack/eslint-patch@1.11.0': resolution: {integrity: sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==} + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -961,6 +1119,15 @@ packages: '@types/node@20.17.50': resolution: {integrity: sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A==} + '@types/node@22.15.32': + resolution: {integrity: sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==} + + '@types/nodemailer@6.4.17': + resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==} + + '@types/pg@8.15.4': + resolution: {integrity: sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==} + '@types/react-dom@19.1.5': resolution: {integrity: sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==} peerDependencies: @@ -1183,6 +1350,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bcryptjs@3.0.2: + resolution: {integrity: sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==} + hasBin: true + before-after-hook@3.0.2: resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} @@ -1254,6 +1425,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + 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'} @@ -1346,6 +1521,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -1368,6 +1547,19 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dotenv@16.5.0: resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} engines: {node: '>=12'} @@ -1383,6 +1575,10 @@ packages: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + es-abstract@1.23.10: resolution: {integrity: sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==} engines: {node: '>= 0.4'} @@ -1544,6 +1740,9 @@ packages: fast-content-type-parse@3.0.0: resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} + fast-deep-equal@2.0.1: + resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1681,6 +1880,13 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1821,6 +2027,12 @@ packages: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true + jose@4.15.9: + resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + + jose@6.0.11: + resolution: {integrity: sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1855,6 +2067,9 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1937,6 +2152,10 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + lucide-react@0.511.0: resolution: {integrity: sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==} peerDependencies: @@ -1996,6 +2215,20 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + next-auth@4.24.11: + resolution: {integrity: sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==} + peerDependencies: + '@auth/core': 0.34.2 + next: ^12.2.5 || ^13 || ^14 || ^15 + nodemailer: ^6.6.5 + react: ^17.0.2 || ^18 || ^19 + react-dom: ^17.0.2 || ^18 || ^19 + peerDependenciesMeta: + '@auth/core': + optional: true + nodemailer: + optional: true + next@15.3.2: resolution: {integrity: sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -2017,10 +2250,24 @@ packages: sass: optional: true + nodemailer@7.0.3: + resolution: {integrity: sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==} + engines: {node: '>=6.0.0'} + + oauth4webapi@3.5.3: + resolution: {integrity: sha512-2bnHosmBLAQpXNBLOvaJMyMkr4Yya5ohE5Q9jqyxiN+aa7GFCzvDN1RRRMrp0NkfqRR2MTaQNkcSUCCjILD9oQ==} + + 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'} @@ -2049,6 +2296,13 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + oidc-token-hash@5.1.0: + resolution: {integrity: sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==} + 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'} @@ -2069,6 +2323,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2080,6 +2337,20 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-protocol@1.10.2: + resolution: {integrity: sha512-Ci7jy8PbaWxfsck2dwZdERcDG2A0MG8JoQILs+uZNjABFuBuItAZCWUNz8sXRDMoui24rJw7WlXqgpMdBSN/vQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2103,10 +2374,60 @@ packages: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + preact-render-to-string@5.2.6: + resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} + peerDependencies: + preact: '>=10' + + preact-render-to-string@6.5.11: + resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} + peerDependencies: + preact: '>=10' + + preact@10.24.3: + resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==} + + preact@10.26.9: + resolution: {integrity: sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.5.3: + resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@3.8.0: + resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} + + prisma@6.10.1: + resolution: {integrity: sha512-khhlC/G49E4+uyA3T3H5PRBut486HD2bDqE2+rvkU0pwk9IAqGFacLFUyIx9Uw+W2eCtf6XGwsp+/strUwMNPw==} + engines: {node: '>=18.18'} + hasBin: true + peerDependencies: + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -2122,12 +2443,26 @@ packages: peerDependencies: react: ^19.1.0 + react-hook-form@7.58.1: + resolution: {integrity: sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-icons@5.5.0: + resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} + peerDependencies: + react: '*' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-promise-suspense@0.3.4: + resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -2192,6 +2527,10 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + resend@4.6.0: + resolution: {integrity: sha512-D5T2I82FvEUYFlrHzaDvVtr5ADHdhuoLaXgLFGABKyNtQgPWIuz0Vp2L2Evx779qjK37aF4kcw1yXJDHhA2JnQ==} + engines: {node: '>=18'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2230,6 +2569,9 @@ packages: scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -2421,6 +2763,9 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + universal-github-app-jwt@2.2.2: resolution: {integrity: sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==} @@ -2453,6 +2798,9 @@ packages: '@types/react': optional: true + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true vaul@1.1.2: resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} peerDependencies: @@ -2487,6 +2835,13 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -2495,6 +2850,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod@3.25.67: + resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -2504,6 +2862,25 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 + '@auth/core@0.40.0(nodemailer@7.0.3)': + dependencies: + '@panva/hkdf': 1.2.1 + jose: 6.0.11 + oauth4webapi: 3.5.3 + preact: 10.24.3 + preact-render-to-string: 6.5.11(preact@10.24.3) + optionalDependencies: + nodemailer: 7.0.3 + + '@auth/prisma-adapter@2.10.0(@prisma/client@6.10.1(prisma@6.10.1(typescript@5.8.3))(typescript@5.8.3))(nodemailer@7.0.3)': + dependencies: + '@auth/core': 0.40.0(nodemailer@7.0.3) + '@prisma/client': 6.10.1(prisma@6.10.1(typescript@5.8.3))(typescript@5.8.3) + transitivePeerDependencies: + - '@simplewebauthn/browser' + - '@simplewebauthn/server' + - nodemailer + '@babel/runtime@7.27.1': {} '@emnapi/core@1.4.3': @@ -2583,6 +2960,11 @@ snapshots: '@floating-ui/utils@0.2.9': {} + '@hookform/resolvers@5.1.1(react-hook-form@7.58.1(react@19.1.0))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.58.1(react@19.1.0) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -2705,6 +3087,11 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true + '@neondatabase/serverless@1.0.1': + dependencies: + '@types/node': 22.15.32 + '@types/pg': 8.15.4 + '@next/env@15.3.2': {} '@next/eslint-plugin-next@15.3.2': @@ -2883,6 +3270,38 @@ snapshots: dependencies: '@octokit/openapi-types': 25.0.0 + '@panva/hkdf@1.2.1': {} + + '@prisma/client@6.10.1(prisma@6.10.1(typescript@5.8.3))(typescript@5.8.3)': + optionalDependencies: + prisma: 6.10.1(typescript@5.8.3) + typescript: 5.8.3 + + '@prisma/config@6.10.1': + dependencies: + jiti: 2.4.2 + + '@prisma/debug@6.10.1': {} + + '@prisma/engines-version@6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c': {} + + '@prisma/engines@6.10.1': + dependencies: + '@prisma/debug': 6.10.1 + '@prisma/engines-version': 6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c + '@prisma/fetch-engine': 6.10.1 + '@prisma/get-platform': 6.10.1 + + '@prisma/fetch-engine@6.10.1': + dependencies: + '@prisma/debug': 6.10.1 + '@prisma/engines-version': 6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c + '@prisma/get-platform': 6.10.1 + + '@prisma/get-platform@6.10.1': + dependencies: + '@prisma/debug': 6.10.1 + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.2': {} @@ -2961,6 +3380,21 @@ snapshots: '@types/react': 19.1.5 '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.5)(react@19.1.0)': dependencies: react: 19.1.0 @@ -2985,6 +3419,32 @@ snapshots: optionalDependencies: '@types/react': 19.1.5 + '@radix-ui/react-menu@2.1.15(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.0(@types/react@19.1.5)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@radix-ui/react-popper@1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@floating-ui/react-dom': 2.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -3088,6 +3548,15 @@ snapshots: '@types/react': 19.1.5 '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + '@radix-ui/react-slot@1.2.3(@types/react@19.1.5)(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) @@ -3176,10 +3645,25 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@react-email/render@1.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + html-to-text: 9.0.5 + prettier: 3.5.3 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-promise-suspense: 0.3.4 + '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.11.0': {} + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + + '@standard-schema/utils@0.3.0': {} + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -3297,6 +3781,20 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/node@22.15.32': + dependencies: + undici-types: 6.21.0 + + '@types/nodemailer@6.4.17': + dependencies: + '@types/node': 20.17.50 + + '@types/pg@8.15.4': + dependencies: + '@types/node': 20.17.50 + pg-protocol: 1.10.2 + pg-types: 2.2.0 + '@types/react-dom@19.1.5(@types/react@19.1.5)': dependencies: '@types/react': 19.1.5 @@ -3539,6 +4037,8 @@ snapshots: balanced-match@1.0.2: {} + bcryptjs@3.0.2: {} + before-after-hook@3.0.2: {} brace-expansion@1.1.11: @@ -3614,6 +4114,8 @@ snapshots: concat-map@0.0.1: {} + cookie@0.7.2: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3694,6 +4196,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -3719,6 +4223,24 @@ snapshots: '@babel/runtime': 7.27.1 csstype: 3.1.3 + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dotenv@16.5.0: {} dunder-proto@1.0.1: @@ -3734,6 +4256,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.2 + entities@4.5.0: {} + es-abstract@1.23.10: dependencies: array-buffer-byte-length: 1.0.2 @@ -4037,6 +4561,8 @@ snapshots: fast-content-type-parse@3.0.0: {} + fast-deep-equal@2.0.1: {} + fast-deep-equal@3.1.3: {} fast-equals@5.2.2: {} @@ -4179,6 +4705,21 @@ snapshots: dependencies: function-bind: 1.1.2 + html-to-text@9.0.5: + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + ignore@5.3.2: {} ignore@7.0.4: {} @@ -4325,6 +4866,10 @@ snapshots: jiti@2.4.2: {} + jose@4.15.9: {} + + jose@6.0.11: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -4358,6 +4903,8 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + leac@0.6.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -4420,6 +4967,10 @@ snapshots: dependencies: js-tokens: 4.0.0 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + lucide-react@0.511.0(react@19.1.0): dependencies: react: 19.1.0 @@ -4463,6 +5014,23 @@ snapshots: natural-compare@1.4.0: {} + next-auth@4.24.11(next@15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@babel/runtime': 7.27.1 + '@panva/hkdf': 1.2.1 + cookie: 0.7.2 + jose: 4.15.9 + next: 15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + oauth: 0.9.15 + openid-client: 5.7.1 + preact: 10.26.9 + preact-render-to-string: 5.2.6(preact@10.26.9) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + uuid: 8.3.2 + optionalDependencies: + nodemailer: 7.0.3 + next@15.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@next/env': 15.3.2 @@ -4488,8 +5056,16 @@ snapshots: - '@babel/core' - babel-plugin-macros + nodemailer@7.0.3: {} + + oauth4webapi@3.5.3: {} + + oauth@0.9.15: {} + object-assign@4.1.1: {} + object-hash@2.2.0: {} + object-inspect@1.13.4: {} object-keys@1.1.1: {} @@ -4530,6 +5106,15 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + oidc-token-hash@5.1.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.1.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -4557,12 +5142,31 @@ snapshots: dependencies: callsites: 3.1.0 + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + path-exists@4.0.0: {} path-key@3.1.1: {} path-parse@1.0.7: {} + peberminta@0.9.0: {} + + pg-int8@1.0.1: {} + + pg-protocol@1.10.2: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -4583,8 +5187,42 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.0: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + preact-render-to-string@5.2.6(preact@10.26.9): + dependencies: + preact: 10.26.9 + pretty-format: 3.8.0 + + preact-render-to-string@6.5.11(preact@10.24.3): + dependencies: + preact: 10.24.3 + + preact@10.24.3: {} + + preact@10.26.9: {} + prelude-ls@1.2.1: {} + prettier@3.5.3: {} + + pretty-format@3.8.0: {} + + prisma@6.10.1(typescript@5.8.3): + dependencies: + '@prisma/config': 6.10.1 + '@prisma/engines': 6.10.1 + optionalDependencies: + typescript: 5.8.3 + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -4600,10 +5238,22 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 + react-hook-form@7.58.1(react@19.1.0): + dependencies: + react: 19.1.0 + + react-icons@5.5.0(react@19.1.0): + dependencies: + react: 19.1.0 + react-is@16.13.1: {} react-is@18.3.1: {} + react-promise-suspense@0.3.4: + dependencies: + fast-deep-equal: 2.0.1 + react-remove-scroll-bar@2.3.8(@types/react@19.1.5)(react@19.1.0): dependencies: react: 19.1.0 @@ -4687,6 +5337,13 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + resend@4.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@react-email/render': 1.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + transitivePeerDependencies: + - react + - react-dom + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -4730,6 +5387,10 @@ snapshots: scheduler@0.26.0: {} + selderee@0.11.0: + dependencies: + parseley: 0.12.1 + semver@6.3.1: {} semver@7.7.2: {} @@ -4986,6 +5647,8 @@ snapshots: undici-types@6.19.8: {} + undici-types@6.21.0: {} + universal-github-app-jwt@2.2.2: {} universal-user-agent@7.0.3: {} @@ -5031,6 +5694,9 @@ snapshots: optionalDependencies: '@types/react': 19.1.5 + uuid@8.3.2: {} + + vaul@1.1.2(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -5104,6 +5770,12 @@ snapshots: word-wrap@1.2.5: {} + xtend@4.0.2: {} + + yallist@4.0.0: {} + yallist@5.0.0: {} yocto-queue@0.1.0: {} + + zod@3.25.67: {} diff --git a/prisma/migrations/20250622150343_init/migration.sql b/prisma/migrations/20250622150343_init/migration.sql new file mode 100644 index 0000000..e936b84 --- /dev/null +++ b/prisma/migrations/20250622150343_init/migration.sql @@ -0,0 +1,92 @@ +-- CreateEnum +CREATE TYPE "UserPlan" AS ENUM ('FREE', 'PRO', 'ENTERPRISE'); + +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + + CONSTRAINT "Account_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL, + "sessionToken" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "name" TEXT, + "email" TEXT NOT NULL, + "emailVerified" TIMESTAMP(3), + "image" TEXT, + "password" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "githubUsername" TEXT, + "plan" "UserPlan" NOT NULL DEFAULT 'FREE', + "preferences" JSONB, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VerificationToken" ( + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL +); + +-- CreateTable +CREATE TABLE "EmailVerificationToken" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + "used" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "EmailVerificationToken_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); + +-- CreateIndex +CREATE UNIQUE INDEX "EmailVerificationToken_token_key" ON "EmailVerificationToken"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "EmailVerificationToken_email_token_key" ON "EmailVerificationToken"("email", "token"); + +-- AddForeignKey +ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# 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 new file mode 100644 index 0000000..2f3cf71 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,86 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? +// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init + +generator client { + provider = "prisma-client-js" + output = "../src/generated/prisma" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// NextAuth.js required models +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model User { + id String @id @default(cuid()) + name String? + email String @unique + emailVerified DateTime? + image String? + password String? // For email/password authentication + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + accounts Account[] + sessions Session[] + + // GitIntel specific fields + githubUsername String? + plan UserPlan @default(FREE) + preferences Json? // Store user preferences as JSON +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) +} + +// Email verification tokens for our custom email auth +model EmailVerificationToken { + id String @id @default(cuid()) + email String + token String @unique + expires DateTime + used Boolean @default(false) + + @@unique([email, token]) +} + +enum UserPlan { + FREE + PRO + ENTERPRISE +} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..8a6a402 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,6 @@ +import NextAuth from 'next-auth' +import { authOptions } from '@/lib/auth' + +const handler = NextAuth(authOptions) + +export { handler as GET, handler as POST } \ No newline at end of file diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts new file mode 100644 index 0000000..2a6158a --- /dev/null +++ b/src/app/api/auth/register/route.ts @@ -0,0 +1,95 @@ +import { NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import bcrypt from 'bcryptjs' +import { db } from '@/lib/db' +import { sendVerificationEmail } from '@/lib/email' +import crypto from 'crypto' + +const registerSchema = z.object({ + name: z.string().min(2, 'Name must be at least 2 characters'), + email: z.string().email('Invalid email address'), + password: z.string().min(6, 'Password must be at least 6 characters'), +}) + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { name, email, password } = registerSchema.parse(body) + + // Check if user already exists + const existingUser = await db.user.findUnique({ + where: { email } + }) + + if (existingUser) { + return NextResponse.json( + { error: 'User with this email already exists' }, + { status: 400 } + ) + } + + // Hash password + const hashedPassword = await bcrypt.hash(password, 12) + + // Create user + const user = await db.user.create({ + data: { + name, + email, + password: hashedPassword, + emailVerified: null, // Will be set when email is verified + } + }) + + // Generate verification token + const verificationToken = crypto.randomBytes(32).toString('hex') + const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours + + // Store verification token + await db.emailVerificationToken.create({ + data: { + email, + token: verificationToken, + expires: expiresAt, + } + }) + + // Send verification email + const emailResult = await sendVerificationEmail(email, verificationToken) + + if (!emailResult.success) { + // If email fails, delete the user and token + await db.emailVerificationToken.deleteMany({ + where: { email } + }) + await db.user.delete({ + where: { id: user.id } + }) + + return NextResponse.json( + { error: 'Failed to send verification email. Please try again.' }, + { status: 500 } + ) + } + + return NextResponse.json({ + message: 'Registration successful! Please check your email to verify your account.', + email, + }) + + } catch (error) { + console.error('Registration error:', error) + + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: error.errors[0].message }, + { status: 400 } + ) + } + + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/src/app/api/auth/verify-email/route.ts b/src/app/api/auth/verify-email/route.ts new file mode 100644 index 0000000..99e7b52 --- /dev/null +++ b/src/app/api/auth/verify-email/route.ts @@ -0,0 +1,144 @@ +import { NextRequest, NextResponse } from 'next/server' +import { db } from '@/lib/db' + +export async function POST(request: NextRequest) { + try { + const { token } = await request.json() + + if (!token) { + return NextResponse.json( + { error: 'Verification token is required' }, + { status: 400 } + ) + } + + // Find the verification token + const verificationToken = await db.emailVerificationToken.findUnique({ + where: { token } + }) + + if (!verificationToken) { + return NextResponse.json( + { error: 'Invalid verification token' }, + { status: 400 } + ) + } + + // Check if token has expired + if (verificationToken.expires < new Date()) { + await db.emailVerificationToken.delete({ + where: { id: verificationToken.id } + }) + + return NextResponse.json( + { error: 'Verification token has expired' }, + { status: 400 } + ) + } + + // Check if token has already been used + if (verificationToken.used) { + return NextResponse.json( + { error: 'Verification token has already been used' }, + { status: 400 } + ) + } + + // Find the user + const user = await db.user.findUnique({ + where: { email: verificationToken.email } + }) + + if (!user) { + return NextResponse.json( + { error: 'User not found' }, + { status: 404 } + ) + } + + // Update user email verification status + await db.user.update({ + where: { id: user.id }, + data: { emailVerified: new Date() } + }) + + // Mark token as used + await db.emailVerificationToken.update({ + where: { id: verificationToken.id }, + data: { used: true } + }) + + return NextResponse.json({ + message: 'Email verified successfully! You can now sign in.', + }) + + } catch (error) { + console.error('Email verification error:', error) + + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url) + const token = searchParams.get('token') + + if (!token) { + return NextResponse.redirect('/auth/signin?error=missing-token') + } + + try { + // Find the verification token + const verificationToken = await db.emailVerificationToken.findUnique({ + where: { token } + }) + + if (!verificationToken) { + return NextResponse.redirect('/auth/signin?error=invalid-token') + } + + // Check if token has expired + if (verificationToken.expires < new Date()) { + await db.emailVerificationToken.delete({ + where: { id: verificationToken.id } + }) + + return NextResponse.redirect('/auth/signin?error=expired-token') + } + + // Check if token has already been used + if (verificationToken.used) { + return NextResponse.redirect('/auth/signin?error=token-used') + } + + // Find the user + const user = await db.user.findUnique({ + where: { email: verificationToken.email } + }) + + if (!user) { + return NextResponse.redirect('/auth/signin?error=user-not-found') + } + + // Update user email verification status + await db.user.update({ + where: { id: user.id }, + data: { emailVerified: new Date() } + }) + + // Mark token as used + await db.emailVerificationToken.update({ + where: { id: verificationToken.id }, + data: { used: true } + }) + + return NextResponse.redirect('/auth/signin?verified=true') + + } catch (error) { + console.error('Email verification error:', error) + return NextResponse.redirect('/auth/signin?error=verification-failed') + } +} \ No newline at end of file diff --git a/src/app/api/user/password/route.ts b/src/app/api/user/password/route.ts new file mode 100644 index 0000000..7d27f84 --- /dev/null +++ b/src/app/api/user/password/route.ts @@ -0,0 +1,89 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth/next' +import { authOptions } from '@/lib/auth' +import { db } from '@/lib/db' +import { z } from 'zod' +import bcrypt from 'bcryptjs' + +const updatePasswordSchema = z.object({ + currentPassword: z.string().min(1, 'Current password is required'), + newPassword: z.string().min(6, 'New password must be at least 6 characters'), +}) + +export async function PATCH(request: NextRequest) { + try { + const session = await getServerSession(authOptions) + + if (!session || !session.user) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + + const body = await request.json() + const { currentPassword, newPassword } = updatePasswordSchema.parse(body) + + // Get user with password + const user = await db.user.findUnique({ + where: { id: session.user.id }, + select: { + id: true, + password: true + } + }) + + if (!user) { + return NextResponse.json( + { error: 'User not found' }, + { status: 404 } + ) + } + + // Check if user has a password (they might be OAuth-only user) + if (!user.password) { + return NextResponse.json( + { error: 'Cannot update password for OAuth-only accounts' }, + { status: 400 } + ) + } + + // Verify current password + const isCurrentPasswordValid = await bcrypt.compare(currentPassword, user.password) + + if (!isCurrentPasswordValid) { + return NextResponse.json( + { error: 'Current password is incorrect' }, + { status: 400 } + ) + } + + // Hash new password + const hashedNewPassword = await bcrypt.hash(newPassword, 12) + + // Update password + await db.user.update({ + where: { id: session.user.id }, + data: { password: hashedNewPassword } + }) + + return NextResponse.json({ + message: 'Password updated successfully' + }) + + } catch (error) { + console.error('Password update error:', error) + + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: error.errors[0].message }, + { status: 400 } + ) + } + + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts new file mode 100644 index 0000000..9385505 --- /dev/null +++ b/src/app/api/user/profile/route.ts @@ -0,0 +1,119 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getServerSession } from 'next-auth/next' +import { authOptions } from '@/lib/auth' +import { db } from '@/lib/db' +import { z } from 'zod' + +const updateProfileSchema = z.object({ + name: z.string().min(2, 'Name must be at least 2 characters').optional(), + email: z.string().email('Invalid email address').optional(), +}) + +export async function PATCH(request: NextRequest) { + try { + const session = await getServerSession(authOptions) + + if (!session || !session.user) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + + const body = await request.json() + const { name, email } = updateProfileSchema.parse(body) + + // Check if email is already taken by another user + if (email && email !== session.user.email) { + const existingUser = await db.user.findUnique({ + where: { email } + }) + + if (existingUser) { + return NextResponse.json( + { error: 'Email address is already in use' }, + { status: 400 } + ) + } + } + + // Update user profile + const updatedUser = await db.user.update({ + where: { id: session.user.id }, + data: { + ...(name && { name }), + ...(email && { email }) + } + }) + + return NextResponse.json({ + message: 'Profile updated successfully', + user: { + id: updatedUser.id, + name: updatedUser.name, + email: updatedUser.email, + plan: updatedUser.plan + } + }) + + } catch (error) { + console.error('Profile update error:', error) + + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: error.errors[0].message }, + { status: 400 } + ) + } + + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} + +export async function GET() { + try { + const session = await getServerSession(authOptions) + + if (!session || !session.user) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + + const user = await db.user.findUnique({ + where: { id: session.user.id }, + select: { + id: true, + name: true, + email: true, + emailVerified: true, + image: true, + plan: true, + githubUsername: true, + createdAt: true, + updatedAt: true + } + }) + + if (!user) { + return NextResponse.json( + { error: 'User not found' }, + { status: 404 } + ) + } + + return NextResponse.json({ user }) + + } catch (error) { + console.error('Profile fetch error:', error) + + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx new file mode 100644 index 0000000..b21ac01 --- /dev/null +++ b/src/app/auth/signin/page.tsx @@ -0,0 +1,245 @@ +"use client" + +import { useState, useEffect, Suspense } from 'react' +import { signIn, getSession } from 'next-auth/react' +import { useRouter, useSearchParams } from 'next/navigation' +import Link from 'next/link' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Label } from '@/components/ui/label' +import { Eye, EyeOff, Github, } from 'lucide-react' +import { FcGoogle } from 'react-icons/fc' + +// Loading component for Suspense fallback +function SignInLoadingFallback() { + return ( +
+ Don't have an account? + + Sign up + +
+
- {`GITHUB_TOKEN=your_github_personal_access_token_here
+ {`REPO_TOKEN=your_github_personal_access_token_here
NEXT_PUBLIC_APP_NAME=GitIntel`}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f8d2d12..48c46e0 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -2,6 +2,7 @@ import "./globals.css";
import type { Metadata } from "next";
import { ThemeProvider } from "@/components/ThemeProvider";
import Footer from "@/components/Footer";
+import { AuthProvider } from "@/components/AuthProvider";
export const metadata: Metadata = {
title: "GitIntel",
@@ -12,12 +13,14 @@ export default function RootLayout({ children }: { children: React.ReactNode })
return (
- Loading billing information...
+Manage your subscription and billing information
++ {currentPlan === 'free' + ? 'Free forever' + : `$${plans.find(p => p.id === currentPlan)?.price}/month` + } +
++ Next billing: {formatDate('2024-02-15')} +
+ )} ++ {currentPlan === 'free' ? 'Best effort' : + currentPlan === 'pro' ? '24h response' : '1h response'} +
++ {invoice.plan} Plan +
++ {formatDate(invoice.date)} +
++ ${invoice.amount.toFixed(2)} +
+No billing history yet
++ Upgrade to a paid plan to see your billing history here +
+Loading...
+Manage your account settings and preferences
+Click to change photo
++ JPG, PNG or GIF. Max 5MB +
+Loading profile...
++ Total searches performed +
++ Repository comparisons +
++ Bookmarked repositories +
++ Days since joining +
+Searched for "react" repositories
+2 hours ago
+Compared Next.js vs React
+1 day ago
+Bookmarked facebook/react repository
+3 days ago
+Loading settings...
+Manage your account preferences and security
+Receive email updates about your account activity
+Get a weekly summary of repository insights
+Important security notifications about your account
++ Keep your API key secure. It provides access to your GitIntel account. +
+Help improve GitIntel by sharing anonymous usage data
++ Download a copy of all your data stored in GitIntel +
+ ++ Add an extra layer of security to your account +
+ ++ View and manage devices that are signed into your account +
+ ++ Permanently delete your GitIntel account and all associated data. This action cannot be undone. +
+ +Analyze competition in open source GitHub projects
++ Thanks for signing up! Please click the button below to verify your email address and complete your account setup. +
+ + + Verify Email Address + + +
+ If the button doesn't work, copy and paste this link into your browser:
+
+ ${verificationUrl}
+
+ This verification link will expire in 24 hours. +
+Analyze competition in open source GitHub projects
++ You requested to reset your password. Click the button below to create a new password. +
+ + + Reset Password + + +
+ If the button doesn't work, copy and paste this link into your browser:
+
+ ${resetUrl}
+
+ This reset link will expire in 1 hour. If you didn't request this, please ignore this email. +
+