create-apppaaaul 2.0.16 → 2.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -143,6 +143,8 @@ async function main() {
143
143
  await execAsync(`cd ${project.name}`);
144
144
  console.log(`
145
145
  ${import_picocolors2.default.green(`cd`)} ${project.name}`);
146
+ await execAsync("git checkout -b dev");
147
+ await execAsync("git push -u origin dev");
146
148
  } catch (error) {
147
149
  console.error(`Error executing commands: ${error}`);
148
150
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../node_modules/.pnpm/tsup@8.4.0_typescript@5.8.2_yaml@2.4.5/node_modules/tsup/assets/cjs_shims.js","../index.ts","../helpers/install.ts"],"sourcesContent":["// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () =>\n typeof document === 'undefined'\n ? new URL(`file:${__filename}`).href\n : (document.currentScript && document.currentScript.src) ||\n new URL('main.js', document.baseURI).href\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","#!/usr/bin/env node\r\n\r\nimport path from \"node:path\";\r\nimport { fileURLToPath } from \"node:url\";\r\nimport { cp, readFile, writeFile } from \"node:fs/promises\";\r\nimport { exec } from \"child_process\";\r\nimport { promisify } from \"util\";\r\n\r\nimport { cyan, green, red } from \"picocolors\";\r\nimport { glob } from \"glob\";\r\nimport color from \"picocolors\";\r\nimport prompts from \"prompts\";\r\nimport yargs from \"yargs\";\r\nimport { hideBin } from \"yargs/helpers\";\r\n\r\nimport { install } from \"./helpers/install\";\r\nconst execAsync = promisify(exec);\r\n\r\n// Define the templates available\r\nconst TEMPLATES = [\r\n {\r\n title: \"Nextjs ts with db setup Landing with prisma\",\r\n value: \"nextjs-ts-landing-prisma\",\r\n },\r\n {\r\n title: 'Nextjs ts clean',\r\n value: 'nextjs-ts-clean'\r\n }\r\n];\r\n\r\n// Specify CLI arguments\r\nconst args = yargs(hideBin(process.argv)).options({\r\n name: {\r\n alias: \"n\",\r\n type: \"string\",\r\n description: \"Name of the project\",\r\n },\r\n template: {\r\n alias: \"t\",\r\n type: \"string\",\r\n description: \"Template to use\",\r\n },\r\n});\r\n\r\n// Override arguments passed on the CLI\r\nprompts.override(args.argv);\r\n\r\nasync function main() {\r\n // Get the initial values for the prompts\r\n const {\r\n _: [initialName, initialProject],\r\n } = await args.argv;\r\n\r\n // Create the project prompt\r\n const project = await prompts(\r\n [\r\n {\r\n type: \"text\",\r\n name: \"name\",\r\n message: \"What is the name of your project?\",\r\n initial: initialName || \"apppaaaul-project\",\r\n validate: (value) => {\r\n if (value !== \".\" && value.match(/[^a-zA-Z0-9-_]+/g)) {\r\n return \"Project name can only contain letters, numbers, dashes, underscores, or be '.' for the current directory\";\r\n }\r\n\r\n return true;\r\n },\r\n },\r\n {\r\n type: \"select\",\r\n name: \"template\",\r\n message: `Which template would you like to use?`,\r\n initial: initialProject || 0,\r\n choices: TEMPLATES,\r\n },\r\n ],\r\n {\r\n onCancel: () => {\r\n console.log(\"\\nBye 👋\\n\");\r\n\r\n process.exit(0);\r\n },\r\n },\r\n );\r\n\r\n // Get the template folder for the selected template\r\n const template = path.join(\r\n path.dirname(fileURLToPath(import.meta.url)),\r\n \"templates\",\r\n project.template,\r\n );\r\n\r\n // Get the destination folder for the project\r\n const destination = project.name === \".\" ? process.cwd() : path.join(process.cwd(), project.name);\r\n\r\n // Copy files from the template folder to the current directory\r\n await cp(path.join(template, \"project\"), destination, { recursive: true });\r\n\r\n // Get all files from the destination folder\r\n const files = await glob(`**/*`, { nodir: true, cwd: destination, absolute: true });\r\n\r\n // Read each file and replace the tokens\r\n for await (const file of files) {\r\n const data = await readFile(file, \"utf8\");\r\n const draft = data.replace(/{{name}}/g, project.name);\r\n\r\n await writeFile(file, draft, \"utf8\");\r\n }\r\n\r\n // Log outro message\r\n console.log(\"\\n✨ Project created ✨\");\r\n\r\n // Run commands if a new directory was created\r\n if (project.name !== \".\") {\r\n try {\r\n await execAsync(`cd ${project.name}`);\r\n console.log(`\\n${color.green(`cd`)} ${project.name}`);\r\n } catch (error) {\r\n console.error(`Error executing commands: ${error}`);\r\n }\r\n }\r\n try {\r\n await execAsync(\"cursor .\");\r\n console.log(\"Installing packages. This might take a couple of minutes.\");\r\n console.log();\r\n await install();\r\n console.log();\r\n console.log(`${green(\"Success!\")} App installed successfully.`);\r\n console.log(cyan(\"Initializing the development server...\"));\r\n //TODO: Add docker-compose up, docker create db, pnpm run db:push\r\n await execAsync(\"pnpm dev\");\r\n } catch (error) {\r\n console.error(`Error executing commands: ${error}`);\r\n }\r\n}\r\n\r\n// Run the main function\r\nmain().catch(console.error);\r\n","import spawn from \"cross-spawn\";\r\nimport { yellow } from \"picocolors\";\r\n\r\nexport async function install(): Promise<void> {\r\n const packageManager = \"pnpm\";\r\n const args: string[] = [\"install\"];\r\n\r\n return new Promise((resolve, reject) => {\r\n /**\r\n * Spawn the installation process.\r\n */\r\n const child = spawn(packageManager, args, {\r\n stdio: \"inherit\",\r\n env: {\r\n ...process.env,\r\n ADBLOCK: \"1\",\r\n // we set NODE_ENV to development as pnpm skips dev\r\n // dependencies when production\r\n NODE_ENV: \"development\",\r\n DISABLE_OPENCOLLECTIVE: \"1\",\r\n },\r\n });\r\n\r\n child.on(\"close\", (code) => {\r\n if (code !== 0) {\r\n reject({ command: `${packageManager} ${args.join(\" \")}` });\r\n\r\n return;\r\n }\r\n resolve();\r\n });\r\n });\r\n}\r\n"],"mappings":"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","names":["import_picocolors","args","spawn","yargs","prompts","path","color"]}
1
+ {"version":3,"sources":["../node_modules/.pnpm/tsup@8.4.0_typescript@5.8.2_yaml@2.4.5/node_modules/tsup/assets/cjs_shims.js","../index.ts","../helpers/install.ts"],"sourcesContent":["// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () =>\n typeof document === 'undefined'\n ? new URL(`file:${__filename}`).href\n : (document.currentScript && document.currentScript.src) ||\n new URL('main.js', document.baseURI).href\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","#!/usr/bin/env node\r\n\r\nimport path from \"node:path\";\r\nimport { fileURLToPath } from \"node:url\";\r\nimport { cp, readFile, writeFile } from \"node:fs/promises\";\r\nimport { exec } from \"child_process\";\r\nimport { promisify } from \"util\";\r\n\r\nimport { cyan, green, red } from \"picocolors\";\r\nimport { glob } from \"glob\";\r\nimport color from \"picocolors\";\r\nimport prompts from \"prompts\";\r\nimport yargs from \"yargs\";\r\nimport { hideBin } from \"yargs/helpers\";\r\n\r\nimport { install } from \"./helpers/install\";\r\nconst execAsync = promisify(exec);\r\n\r\n// Define the templates available\r\nconst TEMPLATES = [\r\n {\r\n title: \"Nextjs ts with db setup Landing with prisma\",\r\n value: \"nextjs-ts-landing-prisma\",\r\n },\r\n {\r\n title: 'Nextjs ts clean',\r\n value: 'nextjs-ts-clean'\r\n }\r\n];\r\n\r\n// Specify CLI arguments\r\nconst args = yargs(hideBin(process.argv)).options({\r\n name: {\r\n alias: \"n\",\r\n type: \"string\",\r\n description: \"Name of the project\",\r\n },\r\n template: {\r\n alias: \"t\",\r\n type: \"string\",\r\n description: \"Template to use\",\r\n },\r\n});\r\n\r\n// Override arguments passed on the CLI\r\nprompts.override(args.argv);\r\n\r\nasync function main() {\r\n // Get the initial values for the prompts\r\n const {\r\n _: [initialName, initialProject],\r\n } = await args.argv;\r\n\r\n // Create the project prompt\r\n const project = await prompts(\r\n [\r\n {\r\n type: \"text\",\r\n name: \"name\",\r\n message: \"What is the name of your project?\",\r\n initial: initialName || \"apppaaaul-project\",\r\n validate: (value) => {\r\n if (value !== \".\" && value.match(/[^a-zA-Z0-9-_]+/g)) {\r\n return \"Project name can only contain letters, numbers, dashes, underscores, or be '.' for the current directory\";\r\n }\r\n\r\n return true;\r\n },\r\n },\r\n {\r\n type: \"select\",\r\n name: \"template\",\r\n message: `Which template would you like to use?`,\r\n initial: initialProject || 0,\r\n choices: TEMPLATES,\r\n },\r\n ],\r\n {\r\n onCancel: () => {\r\n console.log(\"\\nBye 👋\\n\");\r\n\r\n process.exit(0);\r\n },\r\n },\r\n );\r\n\r\n // Get the template folder for the selected template\r\n const template = path.join(\r\n path.dirname(fileURLToPath(import.meta.url)),\r\n \"templates\",\r\n project.template,\r\n );\r\n\r\n // Get the destination folder for the project\r\n const destination = project.name === \".\" ? process.cwd() : path.join(process.cwd(), project.name);\r\n\r\n // Copy files from the template folder to the current directory\r\n await cp(path.join(template, \"project\"), destination, { recursive: true });\r\n\r\n // Get all files from the destination folder\r\n const files = await glob(`**/*`, { nodir: true, cwd: destination, absolute: true });\r\n\r\n // Read each file and replace the tokens\r\n for await (const file of files) {\r\n const data = await readFile(file, \"utf8\");\r\n const draft = data.replace(/{{name}}/g, project.name);\r\n\r\n await writeFile(file, draft, \"utf8\");\r\n }\r\n\r\n // Log outro message\r\n console.log(\"\\n✨ Project created ✨\");\r\n\r\n // Run commands if a new directory was created\r\n if (project.name !== \".\") {\r\n try {\r\n await execAsync(`cd ${project.name}`);\r\n console.log(`\\n${color.green(`cd`)} ${project.name}`);\r\n await execAsync(\"git checkout -b dev\");\r\n await execAsync(\"git push -u origin dev\");\r\n } catch (error) {\r\n console.error(`Error executing commands: ${error}`);\r\n }\r\n }\r\n try {\r\n await execAsync(\"cursor .\");\r\n console.log(\"Installing packages. This might take a couple of minutes.\");\r\n console.log();\r\n await install();\r\n console.log();\r\n console.log(`${green(\"Success!\")} App installed successfully.`);\r\n console.log(cyan(\"Initializing the development server...\"));\r\n await execAsync(\"pnpm dev\");\r\n } catch (error) {\r\n console.error(`Error executing commands: ${error}`);\r\n }\r\n}\r\n\r\n// Run the main function\r\nmain().catch(console.error);\r\n","import spawn from \"cross-spawn\";\r\nimport { yellow } from \"picocolors\";\r\n\r\nexport async function install(): Promise<void> {\r\n const packageManager = \"pnpm\";\r\n const args: string[] = [\"install\"];\r\n\r\n return new Promise((resolve, reject) => {\r\n /**\r\n * Spawn the installation process.\r\n */\r\n const child = spawn(packageManager, args, {\r\n stdio: \"inherit\",\r\n env: {\r\n ...process.env,\r\n ADBLOCK: \"1\",\r\n // we set NODE_ENV to development as pnpm skips dev\r\n // dependencies when production\r\n NODE_ENV: \"development\",\r\n DISABLE_OPENCOLLECTIVE: \"1\",\r\n },\r\n });\r\n\r\n child.on(\"close\", (code) => {\r\n if (code !== 0) {\r\n reject({ command: `${packageManager} ${args.join(\" \")}` });\r\n\r\n return;\r\n }\r\n resolve();\r\n });\r\n });\r\n}\r\n"],"mappings":"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","names":["import_picocolors","args","spawn","yargs","prompts","path","color"]}
@@ -6,47 +6,47 @@
6
6
  "dev": "next dev --turbopack",
7
7
  "build": "next build",
8
8
  "start": "next start",
9
- "lint": "eslint .",
10
- "prod": "pnpm lint && pnpm build && git checkout main && git pull origin main && git merge dev && pnpm version patch && git push origin main && git checkout dev && echo ✅ Deploy completado",
9
+ "lint": "eslint . --fix",
10
+ "prod": "pnpm build && git checkout main && git pull origin main && git merge dev && pnpm version patch && git push origin main && git checkout dev && echo ✅ Deploy completado",
11
11
  "dev-rebase": "node src/scripts/dev-rebase.js"
12
12
  },
13
13
  "dependencies": {
14
14
  "@radix-ui/react-slot": "^1.2.3",
15
- "@tailwindcss/postcss": "^4.1.13",
15
+ "@tailwindcss/postcss": "^4.1.16",
16
16
  "autoprefixer": "^10.4.21",
17
17
  "class-variance-authority": "^0.7.1",
18
18
  "clsx": "^2.1.1",
19
- "lucide-react": "^0.544.0",
20
- "next": "^15.5.3",
19
+ "lucide-react": "^0.547.0",
20
+ "next": "^16.0.0",
21
21
  "postcss": "^8.5.6",
22
- "react": "19.1.1",
23
- "react-dom": "19.1.1",
22
+ "react": "19.2.0",
23
+ "react-dom": "19.2.0",
24
24
  "tailwind-merge": "^3.3.1",
25
- "tailwindcss": "^4.1.13",
25
+ "tailwindcss": "^4.1.16",
26
26
  "tailwindcss-animate": "^1.0.7"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@eslint/compat": "^1.3.2",
30
- "@next/eslint-plugin-next": "15.5.3",
30
+ "@next/eslint-plugin-next": "16.0.0",
31
31
  "@types/node": "^24.3.1",
32
32
  "@types/react": "^19.1.12",
33
33
  "@types/react-dom": "^19.1.9",
34
34
  "@vercel/style-guide": "^6.0.0",
35
35
  "babel-plugin-react-compiler": "19.0.0-beta-e1e972c-20250221",
36
36
  "eslint": "^9.35.0",
37
- "eslint-config-next": "^15.5.3",
37
+ "eslint-config-next": "^16.0.0",
38
38
  "eslint-config-prettier": "^10.1.8",
39
39
  "eslint-plugin-import": "^2.32.0",
40
40
  "eslint-plugin-jsx-a11y": "^6.10.2",
41
41
  "eslint-plugin-prettier": "^5.5.4",
42
42
  "eslint-plugin-react": "^7.37.5",
43
43
  "eslint-plugin-react-compiler": "0.0.0-experimental-c8b3f72-20240517",
44
- "eslint-plugin-react-hooks": "^5.2.0",
44
+ "eslint-plugin-react-hooks": "^7.0.0",
45
45
  "globals": "^16.4.0",
46
46
  "prettier": "^3.6.2",
47
- "prettier-plugin-tailwindcss": "^0.6.14",
48
- "tsx": "^4.20.5",
49
- "typescript": "^5.9.2",
50
- "typescript-eslint": "^8.43.0"
47
+ "prettier-plugin-tailwindcss": "^0.7.1",
48
+ "tsx": "^4.20.6",
49
+ "typescript": "^5.9.3",
50
+ "typescript-eslint": "^8.46.2"
51
51
  }
52
52
  }
@@ -0,0 +1,26 @@
1
+ # DB prod
2
+ #DATABASE_URL=""
3
+
4
+ # DB local
5
+ DATABASE_URL="postgres://postgres:1234@localhost/postgres"
6
+
7
+ # Better auth
8
+ BETTER_AUTH_SECRET=""
9
+ GOOGLE_CLIENT_ID=""
10
+ GOOGLE_CLIENT_SECRET=""
11
+ BETTER_AUTH_URL="http://localhost:3000"
12
+ NEXT_PUBLIC_BETTER_AUTH_URL="http://localhost:3000"
13
+
14
+ # Resend
15
+ AUTH_RESEND_KEY=""
16
+ RESEND_API_KEY=""
17
+ RESEND_FROM_EMAIL="noreply@outreach.nuvace.com"
18
+
19
+ # Cloudflare
20
+ CLOUDFLARE_ACCESS_KEY=""
21
+ CLOUDFLARE_BUCKET=""
22
+ CLOUDFLARE_ENDPOINT=""
23
+ CLOUDFLARE_SECRET_KEY=""
24
+ CLOUDFLARE_TOKEN=""
25
+ CLOUDFLARE_ACCOUNT_ID=""
26
+ CLOUDFLARE_PUBLIC_URL=""
@@ -6,56 +6,64 @@
6
6
  "dev": "next dev --turbopack",
7
7
  "build": "next build",
8
8
  "start": "next start",
9
+ "postinstall": "npx prisma generate",
9
10
  "lint": "next lint",
10
11
  "db:push": "npx prisma generate &&npx prisma db push",
11
- "prod": "pnpm test && pnpm lint && pnpm build && git checkout main && git pull origin main && git merge dev && pnpm version patch && git push origin main && git checkout dev && echo ✅ Deploy completado",
12
+ "db:seed": "tsx src/scripts/db-seed.ts",
13
+ "db:studio": "npx prisma studio",
14
+ "prod": "pnpm build && git checkout main && git pull origin main && git merge dev && pnpm version patch && git push origin main && git checkout dev && echo ✅ Deploy completado",
12
15
  "dev-rebase": "node src/scripts/dev-rebase.js"
13
- },
16
+ },
14
17
  "dependencies": {
15
- "@auth/prisma-adapter": "^2.10.0",
16
- "@prisma/client": "^6.16.0",
18
+ "@auth/prisma-adapter": "^2.11.0",
19
+ "@aws-sdk/client-s3": "^3.916.0",
20
+ "@aws-sdk/s3-request-presigner": "^3.916.0",
21
+ "@prisma/client": "^6.18.0",
17
22
  "@radix-ui/react-slot": "^1.2.3",
18
- "@tailwindcss/postcss": "^4.1.13",
23
+ "@react-email/render": "^1.4.0",
24
+ "@tailwindcss/postcss": "^4.1.16",
19
25
  "autoprefixer": "^10.4.21",
20
26
  "bcryptjs": "^3.0.2",
27
+ "better-auth": "^1.3.29",
21
28
  "class-variance-authority": "^0.7.1",
22
29
  "clsx": "^2.1.1",
23
- "dotenv": "^17.2.2",
24
- "lucide-react": "^0.544.0",
25
- "next": "^15.5.3",
26
- "next-auth": "5.0.0-beta.29",
30
+ "dotenv": "^17.2.3",
31
+ "lucide-react": "^0.547.0",
32
+ "motion": "^12.23.24",
33
+ "next": "^16.0.0",
27
34
  "postcss": "^8.5.6",
28
35
  "postgres": "^3.4.7",
29
- "react": "^19.1.1",
30
- "react-dom": "^19.1.1",
36
+ "react": "^19.2.0",
37
+ "react-dom": "^19.2.0",
38
+ "resend": "^6.2.2",
31
39
  "sonner": "^2.0.7",
32
40
  "tailwind-merge": "^3.3.1",
33
- "tailwindcss": "^4.1.13",
41
+ "tailwindcss": "^4.1.16",
34
42
  "tailwindcss-animate": "^1.0.7",
35
- "zod": "^4.1.7"
43
+ "zod": "^4.1.12"
36
44
  },
37
45
  "devDependencies": {
38
- "@eslint/compat": "^1.3.2",
39
- "@next/eslint-plugin-next": "15.5.3",
40
- "@types/node": "^24.3.1",
41
- "@types/react": "^19.1.12",
42
- "@types/react-dom": "^19.1.9",
46
+ "@eslint/compat": "^1.4.0",
47
+ "@next/eslint-plugin-next": "16.0.0",
48
+ "@types/node": "^24.9.1",
49
+ "@types/react": "^19.2.2",
50
+ "@types/react-dom": "^19.2.2",
43
51
  "babel-plugin-react-compiler": "19.0.0-beta-e1e972c-20250221",
44
- "eslint": "^9.35.0",
45
- "eslint-config-next": "^15.5.3",
52
+ "eslint": "^9.38.0",
53
+ "eslint-config-next": "^16.0.0",
46
54
  "eslint-config-prettier": "^10.1.8",
47
55
  "eslint-plugin-import": "^2.32.0",
48
56
  "eslint-plugin-jsx-a11y": "^6.10.2",
49
57
  "eslint-plugin-prettier": "^5.5.4",
50
58
  "eslint-plugin-react": "^7.37.5",
51
59
  "eslint-plugin-react-compiler": "0.0.0-experimental-c8b3f72-20240517",
52
- "eslint-plugin-react-hooks": "^5.2.0",
60
+ "eslint-plugin-react-hooks": "^7.0.0",
53
61
  "globals": "^16.4.0",
54
62
  "prettier": "^3.6.2",
55
- "prettier-plugin-tailwindcss": "^0.6.14",
56
- "prisma": "^6.16.0",
57
- "tsx": "^4.20.5",
58
- "typescript": "^5.9.2",
59
- "typescript-eslint": "^8.43.0"
63
+ "prettier-plugin-tailwindcss": "^0.7.1",
64
+ "prisma": "^6.18.0",
65
+ "tsx": "^4.20.6",
66
+ "typescript": "^5.9.3",
67
+ "typescript-eslint": "^8.46.2"
60
68
  }
61
69
  }
@@ -1,25 +1,85 @@
1
- datasource db {
2
- provider = "postgresql"
3
- url = env("DATABASE_URL")
1
+
2
+ generator client {
3
+ provider = "prisma-client-js"
4
4
  }
5
5
 
6
- generator client {
7
- provider = "prisma-client-js"
8
- output = "./generated"
6
+ datasource db {
7
+ provider = "postgresql"
8
+ url = env("DATABASE_URL")
9
9
  }
10
10
 
11
- model Post {
11
+ model Post {
12
12
  id Int @id @default(autoincrement())
13
13
  title String
14
14
  content String?
15
15
  published Boolean @default(false)
16
16
  author User? @relation(fields: [authorId], references: [id])
17
- authorId Int?
17
+ authorId Int?
18
+ }
19
+
20
+ model User {
21
+ id Int @id @default(autoincrement())
22
+ email String @unique
23
+ name String?
24
+ posts Post[]
25
+ emailVerified Boolean @default(false)
26
+ image String?
27
+ createdAt DateTime @default(now())
28
+ updatedAt DateTime @default(now()) @updatedAt
29
+ role String
30
+ banned Boolean? @default(false)
31
+ banReason String?
32
+ banExpires DateTime?
33
+ lastName String?
34
+ username String?
35
+ sessions Session[]
36
+ accounts Account[]
37
+
38
+ @@map("user")
18
39
  }
40
+
41
+ model Session {
42
+ id String @id
43
+ expiresAt DateTime
44
+ token String
45
+ createdAt DateTime @default(now())
46
+ updatedAt DateTime @updatedAt
47
+ ipAddress String?
48
+ userAgent String?
49
+ userId String
50
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
51
+ impersonatedBy String?
19
52
 
20
- model User {
21
- id Int @id @default(autoincrement())
22
- email String @unique
23
- name String?
24
- posts Post[]
25
- }
53
+ @@unique([token])
54
+ @@map("session")
55
+ }
56
+
57
+ model Account {
58
+ id String @id
59
+ accountId String
60
+ providerId String
61
+ userId String
62
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
63
+ accessToken String?
64
+ refreshToken String?
65
+ idToken String?
66
+ accessTokenExpiresAt DateTime?
67
+ refreshTokenExpiresAt DateTime?
68
+ scope String?
69
+ password String?
70
+ createdAt DateTime @default(now())
71
+ updatedAt DateTime @updatedAt
72
+
73
+ @@map("account")
74
+ }
75
+
76
+ model Verification {
77
+ id String @id
78
+ identifier String
79
+ value String
80
+ expiresAt DateTime
81
+ createdAt DateTime @default(now())
82
+ updatedAt DateTime @default(now()) @updatedAt
83
+
84
+ @@map("verification")
85
+ }
@@ -0,0 +1,5 @@
1
+ import { toNextJsHandler } from "better-auth/next-js";
2
+
3
+ import { auth } from "@/lib/auth";
4
+
5
+ export const { POST, GET } = toNextJsHandler(auth);
@@ -0,0 +1,118 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { Brain, Home, Search, ArrowLeft } from "lucide-react";
5
+ import { motion } from "motion/react";
6
+
7
+ import { Button } from "@/components/ui/button";
8
+
9
+ export default function NotFound() {
10
+ return (
11
+ <div className="bg-background relative flex min-h-screen flex-col items-center justify-center gap-8 overflow-hidden p-6">
12
+ {/* Contenido principal */}
13
+ <div className="relative z-10 flex w-full max-w-2xl flex-col items-center gap-8 text-center">
14
+ {/* Logo y marca */}
15
+ <motion.div
16
+ animate={{ scale: [1, 1.1, 1] }}
17
+ className="flex items-center gap-3 text-2xl font-bold"
18
+ transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
19
+ >
20
+ <div className="from-primary to-accent text-primary-foreground flex size-12 items-center justify-center rounded-xl bg-gradient-to-br shadow-lg">
21
+ <Brain className="size-7" />
22
+ </div>
23
+ <span className="from-foreground to-primary bg-gradient-to-r bg-clip-text text-transparent">
24
+ Nuvace
25
+ </span>
26
+ </motion.div>
27
+
28
+ {/* Número 404 animado */}
29
+ <motion.div
30
+ animate={{
31
+ rotateY: [0, 180, 360],
32
+ scale: [1, 1.05, 1],
33
+ }}
34
+ className="relative"
35
+ transition={{
36
+ duration: 3,
37
+ repeat: Infinity,
38
+ ease: "easeInOut",
39
+ }}
40
+ >
41
+ <h1 className="text-8xl font-black tracking-tighter sm:text-9xl">
42
+ <span className="from-primary to-accent bg-gradient-to-r bg-clip-text text-transparent">
43
+ 404
44
+ </span>
45
+ </h1>
46
+ </motion.div>
47
+
48
+ {/* Mensaje principal */}
49
+ <div className="space-y-4">
50
+ <h2 className="text-foreground text-2xl font-bold sm:text-3xl">
51
+ ¡Ups! Esta página no existe
52
+ </h2>
53
+ <p className="text-muted-foreground max-w-md text-lg">
54
+ La página que buscas no se encuentra disponible. Es posible que haya sido movida o
55
+ eliminada.
56
+ </p>
57
+ </div>
58
+
59
+ {/* Botones de acción */}
60
+ <div className="flex w-full max-w-sm flex-col gap-4 sm:flex-row">
61
+ <Button asChild className="flex-1 gap-2" size="lg">
62
+ <Link href="/">
63
+ <Home className="size-4" />
64
+ Ir al inicio
65
+ </Link>
66
+ </Button>
67
+ </div>
68
+
69
+ {/* Enlace para volver atrás */}
70
+ <Button
71
+ className="text-muted-foreground hover:text-foreground gap-2"
72
+ variant="ghost"
73
+ onClick={() => window.history.back()}
74
+ >
75
+ <ArrowLeft className="size-4" />
76
+ Volver atrás
77
+ </Button>
78
+
79
+ {/* Información adicional */}
80
+ <div className="border-muted/20 bg-muted/10 mt-8 rounded-lg border p-6 backdrop-blur-sm">
81
+ <p className="text-muted-foreground text-sm">
82
+ Si crees que esto es un error, por favor{" "}
83
+ <Link className="text-primary font-medium hover:underline" href="/panel/resumen">
84
+ contacta con soporte
85
+ </Link>{" "}
86
+ para que podamos ayudarte.
87
+ </p>
88
+ </div>
89
+ </div>
90
+
91
+ {/* Partículas flotantes adicionales para el efecto 404 */}
92
+ <div className="pointer-events-none fixed inset-0">
93
+ {[...Array(5)].map((_, i) => (
94
+ <motion.div
95
+ key={i}
96
+ animate={{
97
+ x: [0, 200, 0],
98
+ y: [0, -200, 0],
99
+ opacity: [0.3, 0.7, 0.3],
100
+ scale: [0.5, 1, 0.5],
101
+ }}
102
+ className="bg-accent absolute h-2 w-2 rounded-full"
103
+ style={{
104
+ left: `${20 + i * 15}%`,
105
+ top: `${30 + i * 10}%`,
106
+ }}
107
+ transition={{
108
+ duration: 5 + i * 0.5,
109
+ ease: "easeInOut",
110
+ repeat: Infinity,
111
+ delay: i * 0.3,
112
+ }}
113
+ />
114
+ ))}
115
+ </div>
116
+ </div>
117
+ );
118
+ }
@@ -0,0 +1,9 @@
1
+ import { createAuthClient } from "better-auth/react";
2
+ import { emailOTPClient, adminClient } from "better-auth/client/plugins";
3
+
4
+ export const authClient = createAuthClient({
5
+ baseURL: process.env.BETTER_AUTH_URL,
6
+ plugins: [emailOTPClient(), adminClient()],
7
+ });
8
+
9
+ export const { signIn, signUp, signOut, useSession } = authClient;
@@ -0,0 +1,197 @@
1
+ import { headers } from "next/headers";
2
+
3
+ import { auth } from "./auth";
4
+ import { rateLimiters } from "./rate-limit";
5
+
6
+ export interface AuthUser {
7
+ id: string;
8
+ email: string;
9
+ role: string;
10
+ banned: boolean;
11
+ }
12
+
13
+ export interface AuthResult {
14
+ success: boolean;
15
+ user?: AuthUser;
16
+ error?: string;
17
+ }
18
+
19
+ export interface AuthorizationOptions {
20
+ requireAuth?: boolean;
21
+ allowedRoles?: string[];
22
+ allowSelf?: boolean; // Para permitir acceso a recursos propios
23
+ resourceOwnerId?: string; // ID del propietario del recurso
24
+ }
25
+
26
+ /**
27
+ * Verifica la autenticación del usuario actual
28
+ */
29
+ export async function verifyAuth(): Promise<AuthResult> {
30
+ try {
31
+ const session = await auth.api.getSession({
32
+ headers: await headers(),
33
+ });
34
+
35
+ if (!session?.user) {
36
+ return {
37
+ success: false,
38
+ error: "Authentication required",
39
+ };
40
+ }
41
+
42
+ // Verificar si el usuario está baneado
43
+ if (session.user.banned) {
44
+ return {
45
+ success: false,
46
+ error: "Account banned",
47
+ };
48
+ }
49
+
50
+ return {
51
+ success: true,
52
+ user: {
53
+ id: session.user.id,
54
+ email: session.user.email,
55
+ role: session.user.role ?? "user",
56
+ banned: session.user.banned ?? false,
57
+ },
58
+ };
59
+ } catch (error) {
60
+ console.error("Authentication error:", error);
61
+
62
+ return {
63
+ success: false,
64
+ error: "Authentication failed",
65
+ };
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Verifica autorización basada en opciones
71
+ */
72
+ export async function verifyAuthorization(options: AuthorizationOptions = {}): Promise<AuthResult> {
73
+ const { requireAuth = true, allowedRoles = [], allowSelf = false, resourceOwnerId } = options;
74
+
75
+ if (!requireAuth) {
76
+ return { success: true };
77
+ }
78
+
79
+ const authResult = await verifyAuth();
80
+
81
+ if (!authResult.success || !authResult.user) {
82
+ return authResult;
83
+ }
84
+
85
+ const user = authResult.user;
86
+
87
+ // Verificar roles permitidos
88
+ if (allowedRoles.length > 0 && !allowedRoles.includes(user.role)) {
89
+ // Si se permite acceso propio, verificar si es el propietario del recurso
90
+ if (allowSelf && resourceOwnerId && user.id === resourceOwnerId) {
91
+ return { success: true, user };
92
+ }
93
+
94
+ return {
95
+ success: false,
96
+ error: "Insufficient permissions",
97
+ };
98
+ }
99
+
100
+ return { success: true, user };
101
+ }
102
+
103
+ /**
104
+ * Aplica rate limiting basado en el usuario
105
+ */
106
+ export function checkRateLimit(
107
+ limitType: keyof typeof rateLimiters,
108
+ userId?: string,
109
+ ): { success: boolean; error?: string } {
110
+ const identifier = userId ?? "anonymous";
111
+ const result = rateLimiters[limitType].check(identifier);
112
+
113
+ if (!result.success) {
114
+ return {
115
+ success: false,
116
+ error: "Rate limit exceeded. Please try again later.",
117
+ };
118
+ }
119
+
120
+ return { success: true };
121
+ }
122
+
123
+ /**
124
+ * Middleware de seguridad combinado para server actions
125
+ */
126
+ export function withSecurityMiddleware<T extends unknown[], R>(
127
+ action: (...args: T) => Promise<R>,
128
+ options: AuthorizationOptions & {
129
+ rateLimitType?: keyof typeof rateLimiters;
130
+ } = {},
131
+ ): (...args: T) => Promise<R> {
132
+ return async (...args: T): Promise<R> => {
133
+ // Verificar autorización
134
+ const authResult = await verifyAuthorization(options);
135
+
136
+ if (!authResult.success) {
137
+ throw new Error(authResult.error ?? "Authorization failed");
138
+ }
139
+
140
+ // Aplicar rate limiting si está especificado
141
+ if (options.rateLimitType && authResult.user) {
142
+ const rateLimitResult = checkRateLimit(options.rateLimitType, authResult.user.id);
143
+
144
+ if (!rateLimitResult.success) {
145
+ throw new Error(rateLimitResult.error ?? "Rate limit exceeded");
146
+ }
147
+ }
148
+
149
+ // Ejecutar la acción
150
+ return await action(...args);
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Verifica si el usuario es admin
156
+ */
157
+ export async function requireAdmin(): Promise<AuthUser> {
158
+ const result = await verifyAuthorization({
159
+ allowedRoles: ["admin"],
160
+ });
161
+
162
+ if (!result.success || !result.user) {
163
+ throw new Error(result.error ?? "Admin access required");
164
+ }
165
+
166
+ return result.user;
167
+ }
168
+
169
+ /**
170
+ * Verifica si el usuario está autenticado
171
+ */
172
+ export async function requireAuth(): Promise<AuthUser> {
173
+ const result = await verifyAuth();
174
+
175
+ if (!result.success || !result.user) {
176
+ throw new Error(result.error ?? "Authentication required");
177
+ }
178
+
179
+ return result.user;
180
+ }
181
+
182
+ /**
183
+ * Verifica si el usuario puede acceder a un recurso (admin o propietario)
184
+ */
185
+ export async function requireOwnershipOrAdmin(resourceOwnerId: string): Promise<AuthUser> {
186
+ const result = await verifyAuthorization({
187
+ allowedRoles: ["admin"],
188
+ allowSelf: true,
189
+ resourceOwnerId,
190
+ });
191
+
192
+ if (!result.success || !result.user) {
193
+ throw new Error(result.error ?? "Access denied");
194
+ }
195
+
196
+ return result.user;
197
+ }
@@ -0,0 +1,49 @@
1
+ import { betterAuth } from "better-auth";
2
+ import { prismaAdapter } from "better-auth/adapters/prisma";
3
+ import { emailOTP, admin } from "better-auth/plugins";
4
+ import { PrismaClient } from "@prisma/client";
5
+ import { Resend } from "resend";
6
+ import { env } from "./env";
7
+
8
+ const prisma = new PrismaClient();
9
+ const resend = new Resend(env.RESEND_API_KEY);
10
+
11
+ export const auth = betterAuth({
12
+ baseURL: env.BETTER_AUTH_URL,
13
+ database: prismaAdapter(prisma, {
14
+ provider: "postgresql",
15
+ }),
16
+ emailAndPassword: {
17
+ enabled: true,
18
+ autoSignIn: false,
19
+ },
20
+ socialProviders: {
21
+ google: {
22
+ clientId: env.GOOGLE_CLIENT_ID,
23
+ clientSecret: env.GOOGLE_CLIENT_SECRET,
24
+ prompt: "select_account",
25
+ },
26
+ },
27
+ user: {
28
+ additionalFields: {
29
+ lastName: {
30
+ type: "string",
31
+ required: false,
32
+ },
33
+ username: {
34
+ type: "string",
35
+ required: false,
36
+ },
37
+ role: {
38
+ type: "string",
39
+ required: true,
40
+ defaultValue: "user",
41
+ },
42
+ },
43
+ },
44
+ plugins: [
45
+ admin({
46
+ defaultBanReason: "Has sido baneado por violación de nuestros términos y condiciones",
47
+ }),
48
+ ],
49
+ });
@@ -0,0 +1,124 @@
1
+ import { S3Client, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
2
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
3
+
4
+ // Configuración del cliente S3 para Cloudflare R2
5
+ const createS3Client = () => {
6
+ const endpoint = process.env.CLOUDFLARE_ENDPOINT;
7
+ const accessKeyId = process.env.CLOUDFLARE_ACCESS_KEY;
8
+ const secretAccessKey = process.env.CLOUDFLARE_SECRET_KEY;
9
+
10
+ if (!endpoint || !accessKeyId || !secretAccessKey) {
11
+ throw new Error("Cloudflare R2 environment variables are not properly configured");
12
+ }
13
+
14
+ return new S3Client({
15
+ region: "auto",
16
+ endpoint,
17
+ credentials: {
18
+ accessKeyId,
19
+ secretAccessKey,
20
+ },
21
+ });
22
+ };
23
+
24
+ export interface UploadUrlResponse {
25
+ presignedUrl: string;
26
+ publicUrl: string;
27
+ key: string;
28
+ }
29
+
30
+ export async function generateUploadUrl(
31
+ fileName: string,
32
+ fileType: string,
33
+ userId: string,
34
+ ): Promise<UploadUrlResponse> {
35
+ const s3Client = createS3Client();
36
+ const bucketName = process.env.CLOUDFLARE_BUCKET;
37
+ const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
38
+
39
+ if (!bucketName) {
40
+ throw new Error("CLOUDFLARE_BUCKET environment variable is not set");
41
+ }
42
+
43
+ if (!accountId) {
44
+ throw new Error("CLOUDFLARE_ACCOUNT_ID environment variable is not set");
45
+ }
46
+
47
+ // Generar nombre único para el archivo
48
+ const timestamp = Date.now();
49
+ const sanitizedFileName = fileName.replace(/[^a-zA-Z0-9.-]/g, "_");
50
+ const key = `tickets/${userId}/${timestamp.toString()}-${sanitizedFileName}`;
51
+
52
+ // Crear comando para subir archivo
53
+ const putCommand = new PutObjectCommand({
54
+ Bucket: bucketName,
55
+ Key: key,
56
+ ContentType: fileType,
57
+ });
58
+
59
+ // Generar URL presignada (válida por 5 minutos)
60
+ const presignedUrl = await getSignedUrl(s3Client, putCommand, {
61
+ expiresIn: 300, // 5 minutos
62
+ });
63
+
64
+ // URL pública del archivo (después de ser subido)
65
+ const publicDomain = process.env.CLOUDFLARE_PUBLIC_URL;
66
+
67
+ const publicUrl = publicDomain
68
+ ? `${publicDomain}/${key}`
69
+ : `https://${bucketName}.${accountId}.r2.cloudflarestorage.com/${key}`;
70
+
71
+ return {
72
+ presignedUrl,
73
+ publicUrl,
74
+ key,
75
+ };
76
+ }
77
+
78
+ // Función para eliminar un archivo de R2
79
+ export async function deleteFile(key: string): Promise<void> {
80
+ const s3Client = createS3Client();
81
+ const bucketName = process.env.CLOUDFLARE_BUCKET;
82
+
83
+ if (!bucketName) {
84
+ throw new Error("CLOUDFLARE_BUCKET environment variable is not set");
85
+ }
86
+
87
+ const deleteCommand = new DeleteObjectCommand({
88
+ Bucket: bucketName,
89
+ Key: key,
90
+ });
91
+
92
+ await s3Client.send(deleteCommand);
93
+ }
94
+
95
+ // Función para extraer la key de una URL pública
96
+ export function extractKeyFromUrl(publicUrl: string): string | null {
97
+ try {
98
+ // Obtener el dominio público configurado
99
+ const publicDomain = process.env.CLOUDFLARE_PUBLIC_URL;
100
+
101
+ if (publicDomain && publicUrl.startsWith(publicDomain)) {
102
+ // URL con dominio personalizado: https://images.skinsbrain.com/tickets/user123/1234567890-image.jpg
103
+ return publicUrl.replace(`${publicDomain}/`, "");
104
+ }
105
+
106
+ // URL estándar de R2: https://bucket.accountid.r2.cloudflarestorage.com/tickets/user123/1234567890-image.jpg
107
+ const bucketName = process.env.CLOUDFLARE_BUCKET;
108
+ const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
109
+
110
+ if (bucketName && accountId) {
111
+ const standardDomain = `https://${bucketName}.${accountId}.r2.cloudflarestorage.com/`;
112
+
113
+ if (publicUrl.startsWith(standardDomain)) {
114
+ return publicUrl.replace(standardDomain, "");
115
+ }
116
+ }
117
+
118
+ return null;
119
+ } catch (error) {
120
+ console.error("Error extracting key from URL:", error);
121
+
122
+ return null;
123
+ }
124
+ }
@@ -1,9 +1,28 @@
1
1
  import { PrismaClient } from "@prisma/client";
2
2
 
3
- const globalForPrisma = global as unknown as { prisma: PrismaClient };
3
+ const globalForPrisma = global as unknown as {
4
+ db: PrismaClient | undefined;
5
+ };
4
6
 
5
- export const db = globalForPrisma.prisma || new PrismaClient();
7
+ // Optimized Prisma configuration
8
+ const createPrismaClient = () => {
9
+ return new PrismaClient({
10
+ log: [],
11
+ datasources: {
12
+ db: {
13
+ url: process.env.DATABASE_URL,
14
+ },
15
+ },
16
+ });
17
+ };
6
18
 
7
- if (process.env.NODE_ENV !== "production") {
8
- globalForPrisma.prisma = db;
9
- }
19
+ const db = globalForPrisma.db ?? createPrismaClient();
20
+
21
+ if (process.env.NODE_ENV !== "production") globalForPrisma.db = db;
22
+
23
+ // Graceful shutdown handling
24
+ process.on("beforeExit", () => {
25
+ void db.$disconnect();
26
+ });
27
+
28
+ export default db;
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+
3
+ const envSchema = z.object({
4
+ DATABASE_URL: z.url(),
5
+ BETTER_AUTH_URL: z.url(),
6
+ RESEND_API_KEY: z.string().min(1),
7
+ RESEND_FROM_EMAIL: z.email(),
8
+ GOOGLE_CLIENT_ID: z.string().min(1),
9
+ GOOGLE_CLIENT_SECRET: z.string().min(1),
10
+ NODE_ENV: z.enum(["development", "production"]).default("development"),
11
+ // Cloudflare R2 variables
12
+ CLOUDFLARE_ACCESS_KEY: z.string().min(1).optional(),
13
+ CLOUDFLARE_SECRET_KEY: z.string().min(1).optional(),
14
+ CLOUDFLARE_ENDPOINT: z.url().optional(),
15
+ CLOUDFLARE_BUCKET: z.string().min(1).optional(),
16
+ CLOUDFLARE_ACCOUNT_ID: z.string().min(1).optional(),
17
+ CLOUDFLARE_PUBLIC_URL: z.url().optional(),
18
+ });
19
+
20
+ export type Env = z.infer<typeof envSchema>;
21
+
22
+ function validateEnv(): Env {
23
+ try {
24
+ return envSchema.parse(process.env);
25
+ } catch (error) {
26
+ if (error instanceof z.ZodError) {
27
+ const missingVars = error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
28
+
29
+ console.error("❌ Invalid environment variables:");
30
+ missingVars.forEach((err) => console.error(` - ${err}`));
31
+
32
+ process.exit(1);
33
+ }
34
+
35
+ throw error;
36
+ }
37
+ }
38
+
39
+ export const env = validateEnv();
40
+
41
+ // Helper to check if we're in development
42
+ export const isDevelopment = env.NODE_ENV === "development";
43
+
44
+ export const isProduction = env.NODE_ENV === "production";
@@ -0,0 +1,105 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { headers } from "next/headers";
3
+
4
+ import { auth } from "@/lib/auth";
5
+
6
+ // Rutas públicas que no requieren autenticación
7
+ const publicRoutes = [
8
+ "/",
9
+ "/login",
10
+ "/marketplace",
11
+ "/politica-cookies",
12
+ "/politica-privacidad",
13
+ "/terminos-condiciones",
14
+ "/api/auth",
15
+ ];
16
+
17
+ // Rutas que requieren autenticación de usuario (role: user)
18
+ const userRoutes = ["/dashboard"];
19
+
20
+ // Rutas que requieren autenticación de admin (role: admin)
21
+ const adminRoutes = ["/admin"];
22
+
23
+ export async function middleware(request: NextRequest) {
24
+ const { pathname } = request.nextUrl;
25
+
26
+ // Verificar si la ruta es pública
27
+ const isPublicRoute = publicRoutes.some(
28
+ (route) => pathname === route || pathname.startsWith(route + "/"),
29
+ );
30
+
31
+ // Si es una ruta pública, permitir acceso
32
+ if (isPublicRoute) {
33
+ return NextResponse.next();
34
+ }
35
+
36
+ // Obtener sesión
37
+ const session = await auth.api.getSession({
38
+ headers: await headers(),
39
+ });
40
+
41
+ // Si no hay sesión, redirigir al login
42
+ if (!session) {
43
+ return NextResponse.redirect(new URL("/login", request.url));
44
+ }
45
+
46
+ const userRole = session.user.role;
47
+
48
+ // Verificar si el usuario está baneado y perfil incompleto en una sola consulta
49
+ // Solo verificar si no está ya en la página de perfil, resumen o login para evitar bucles
50
+ if (pathname !== "/dashboard" && pathname !== "/login") {
51
+ try {
52
+ const userData = await auth.api.getUser({ query: { id: session.user.id } });
53
+
54
+ if (!userData) {
55
+ // Si no se puede obtener los datos del usuario, redirigir al login
56
+ console.error("User data not found for session:", session.user.id);
57
+
58
+ return NextResponse.redirect(new URL("/login?error=user_not_found", request.url));
59
+ }
60
+ } catch (error) {
61
+ // Si hay error obteniendo los datos del usuario, log detallado y permitir continuar
62
+ console.error("Error checking user status in middleware:", {
63
+ error: error instanceof Error ? error.message : error,
64
+ userId: session.user.id,
65
+ pathname,
66
+ });
67
+ }
68
+ }
69
+
70
+ // Verificar rutas de admin
71
+ const isAdminRoute = adminRoutes.some((route) => pathname.startsWith(route));
72
+
73
+ if (isAdminRoute && userRole !== "admin") {
74
+ // Si no es admin, redirigir al panel de usuario
75
+ return NextResponse.redirect(new URL("/dashboard", request.url));
76
+ }
77
+
78
+ // Verificar rutas de usuario
79
+ const isUserRoute = userRoutes.some((route) => pathname.startsWith(route));
80
+
81
+ if (isUserRoute && userRole !== "user" && userRole !== "admin") {
82
+ // Si no es usuario ni admin, redirigir al login
83
+ return NextResponse.redirect(new URL("/login", request.url));
84
+ }
85
+
86
+ // Si es admin y está intentando acceder a rutas de usuario, permitir
87
+ if (userRole === "admin") {
88
+ return NextResponse.next();
89
+ }
90
+
91
+ // Si es usuario normal, verificar que no esté intentando acceder a rutas de admin
92
+ if (userRole === "user" && isAdminRoute) {
93
+ return NextResponse.redirect(new URL("/dashboard", request.url));
94
+ }
95
+
96
+ return NextResponse.next();
97
+ }
98
+
99
+ export const config = {
100
+ runtime: "nodejs",
101
+ matcher: [
102
+ // Aplicar middleware a todas las rutas excepto archivos estáticos
103
+ "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
104
+ ],
105
+ };
@@ -0,0 +1,16 @@
1
+ import { PrismaClient, Prisma } from "@prisma/client";
2
+
3
+
4
+ const prisma = new PrismaClient();
5
+
6
+ async function main() {
7
+ //Create database data
8
+ console.log("Database seeded");
9
+ }
10
+
11
+ main().catch((error) => {
12
+ console.error(error);
13
+ process.exit(1);
14
+ }).finally(async () => {
15
+ await prisma.$disconnect();
16
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-apppaaaul",
3
- "version": "2.0.16",
3
+ "version": "2.0.17",
4
4
  "description": "Create projects as paaauldev would",
5
5
  "main": "index.mjs",
6
6
  "bin": {
@@ -1,3 +0,0 @@
1
- import { handlers } from "@/auth";
2
-
3
- export const { GET, POST } = handlers;
@@ -1,31 +0,0 @@
1
- import NextAuth from "next-auth";
2
- import { PrismaAdapter } from "@auth/prisma-adapter";
3
- import Google from "next-auth/providers/google";
4
-
5
- import { db } from "./lib/db";
6
-
7
- export const { handlers, signIn, signOut, auth } = NextAuth({
8
- adapter: PrismaAdapter(db),
9
- providers: [
10
- Google({
11
- clientId: process.env.AUTH_GOOGLE_ID!,
12
- clientSecret: process.env.AUTH_GOOGLE_SECRET!,
13
- }),
14
- ],
15
- session: {
16
- strategy: "jwt",
17
- },
18
- pages: {
19
- signIn: "/login",
20
- error: "/error",
21
- },
22
- callbacks: {
23
- async session({ session, token }) {
24
- if (session.user) {
25
- session.user.id = token.sub!;
26
- }
27
-
28
- return session;
29
- },
30
- },
31
- });