create-apppaaaul 2.0.16 → 2.0.18

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.
Files changed (21) hide show
  1. package/dist/index.js +21 -0
  2. package/dist/index.js.map +1 -1
  3. package/dist/templates/nextjs-ts-clean/project/package.json +15 -15
  4. package/dist/templates/nextjs-ts-landing-prisma/project/.env.test +26 -0
  5. package/dist/templates/nextjs-ts-landing-prisma/project/package.json +34 -26
  6. package/dist/templates/nextjs-ts-landing-prisma/project/prisma/schema.prisma +72 -12
  7. package/dist/templates/nextjs-ts-landing-prisma/project/src/app/api/auth/[...all]/route.ts +5 -0
  8. package/dist/templates/nextjs-ts-landing-prisma/project/src/app/not-found.tsx +118 -0
  9. package/dist/templates/nextjs-ts-landing-prisma/project/src/lib/auth-client.ts +9 -0
  10. package/dist/templates/nextjs-ts-landing-prisma/project/src/lib/auth-utils.ts +197 -0
  11. package/dist/templates/nextjs-ts-landing-prisma/project/src/lib/auth.ts +49 -0
  12. package/dist/templates/nextjs-ts-landing-prisma/project/src/lib/cloudflare-r2.ts +124 -0
  13. package/dist/templates/nextjs-ts-landing-prisma/project/src/lib/db.ts +24 -5
  14. package/dist/templates/nextjs-ts-landing-prisma/project/src/lib/env.ts +44 -0
  15. package/dist/templates/nextjs-ts-landing-prisma/project/src/middleware.ts +105 -0
  16. package/dist/templates/nextjs-ts-landing-prisma/project/src/scripts/db-seed.ts +16 -0
  17. package/package.json +1 -1
  18. package/dist/templates/nextjs-ts-clean/project/%%.gitignore +0 -43
  19. package/dist/templates/nextjs-ts-landing-prisma/project/%%.gitignore +0 -46
  20. package/dist/templates/nextjs-ts-landing-prisma/project/src/app/api/auth/[...nextauth]/route.ts +0 -3
  21. package/dist/templates/nextjs-ts-landing-prisma/project/src/auth.ts +0 -31
package/dist/index.js CHANGED
@@ -131,6 +131,13 @@ async function main() {
131
131
  );
132
132
  const destination = project.name === "." ? process.cwd() : import_node_path.default.join(process.cwd(), project.name);
133
133
  await (0, import_promises.cp)(import_node_path.default.join(template, "project"), destination, { recursive: true });
134
+ const aditionalsPath = import_node_path.default.join(import_node_path.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl)), "aditionals");
135
+ try {
136
+ await (0, import_promises.cp)(aditionalsPath, destination, { recursive: true });
137
+ console.log(`${import_picocolors2.default.green("\u2713")} Copied additional files`);
138
+ } catch (error) {
139
+ console.log(`${import_picocolors2.default.yellow("\u26A0")} Could not copy additional files: ${error}`);
140
+ }
134
141
  const files = await (0, import_glob.glob)(`**/*`, { nodir: true, cwd: destination, absolute: true });
135
142
  for await (const file of files) {
136
143
  const data = await (0, import_promises.readFile)(file, "utf8");
@@ -143,6 +150,20 @@ async function main() {
143
150
  await execAsync(`cd ${project.name}`);
144
151
  console.log(`
145
152
  ${import_picocolors2.default.green(`cd`)} ${project.name}`);
153
+ try {
154
+ await execAsync("git status", { cwd: destination });
155
+ console.log(`${import_picocolors2.default.green("\u2713")} Git repository detected`);
156
+ await execAsync("git checkout -b dev", { cwd: destination });
157
+ console.log(`${import_picocolors2.default.green("\u2713")} Created dev branch`);
158
+ try {
159
+ await execAsync("git push -u origin dev", { cwd: destination });
160
+ console.log(`${import_picocolors2.default.green("\u2713")} Pushed dev branch to origin`);
161
+ } catch (pushError) {
162
+ console.log(`${import_picocolors2.default.yellow("\u26A0")} Could not push to origin (remote may not be configured)`);
163
+ }
164
+ } catch (gitError) {
165
+ console.log(`${import_picocolors2.default.yellow("\u26A0")} Git not initialized in this directory`);
166
+ }
146
167
  } catch (error) {
147
168
  console.error(`Error executing commands: ${error}`);
148
169
  }
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, yellow } 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 // Copy additional files from aditionals folder to the project root\r\n const aditionalsPath = path.join(path.dirname(fileURLToPath(import.meta.url)), \"aditionals\");\r\n try {\r\n await cp(aditionalsPath, destination, { recursive: true });\r\n console.log(`${color.green(\"āœ“\")} Copied additional files`);\r\n } catch (error) {\r\n console.log(`${color.yellow(\"⚠\")} Could not copy additional files: ${error}`);\r\n }\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 \r\n // Check if git is initialized\r\n try {\r\n await execAsync(\"git status\", { cwd: destination });\r\n console.log(`${color.green(\"āœ“\")} Git repository detected`);\r\n \r\n // Create dev branch if git is initialized\r\n await execAsync(\"git checkout -b dev\", { cwd: destination });\r\n console.log(`${color.green(\"āœ“\")} Created dev branch`);\r\n \r\n // Try to push to origin if remote exists\r\n try {\r\n await execAsync(\"git push -u origin dev\", { cwd: destination });\r\n console.log(`${color.green(\"āœ“\")} Pushed dev branch to origin`);\r\n } catch (pushError) {\r\n console.log(`${color.yellow(\"⚠\")} Could not push to origin (remote may not be configured)`);\r\n }\r\n } catch (gitError) {\r\n console.log(`${color.yellow(\"⚠\")} Git not initialized in this directory`);\r\n }\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")
4
- }
5
1
 
6
2
  generator client {
7
3
  provider = "prisma-client-js"
8
- output = "./generated"
4
+ }
5
+
6
+ datasource db {
7
+ provider = "postgresql"
8
+ url = env("DATABASE_URL")
9
9
  }
10
10
 
11
11
  model Post {
12
- id Int @id @default(autoincrement())
12
+ id String @id @default(uuid())
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 String?
18
18
  }
19
19
 
20
20
  model User {
21
- id Int @id @default(autoincrement())
22
- email String @unique
23
- name String?
24
- posts Post[]
25
- }
21
+ id String @id @default(uuid())
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")
39
+ }
40
+
41
+ model Session {
42
+ id String @id @default(uuid())
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?
52
+
53
+ @@unique([token])
54
+ @@map("session")
55
+ }
56
+
57
+ model Account {
58
+ id String @id @default(uuid())
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 @default(uuid())
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.18",
4
4
  "description": "Create projects as paaauldev would",
5
5
  "main": "index.mjs",
6
6
  "bin": {
@@ -1,43 +0,0 @@
1
- # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
-
3
- # dependencies
4
- /node_modules
5
- /.pnp
6
- .pnp.js
7
-
8
- # testing
9
- /coverage
10
-
11
- # next.js
12
- /.next/
13
- /out/
14
-
15
- # production
16
- /build
17
-
18
- # misc
19
- .DS_Store
20
- *.pem
21
-
22
- # debug
23
- npm-debug.log*
24
- yarn-debug.log*
25
- yarn-error.log*
26
-
27
- # local env files
28
- .env*.local
29
- .env
30
-
31
- # vercel
32
- .vercel
33
-
34
- # typescript
35
- *.tsbuildinfo
36
- next-env.d.ts
37
-
38
- # docker
39
- docker-compose.yml
40
-
41
- # pnpm
42
- pnpm-lock.yaml
43
- pnpm-*
@@ -1,46 +0,0 @@
1
- # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
-
3
- # dependencies
4
- /node_modules
5
- /.pnp
6
- .pnp.js
7
-
8
- # testing
9
- /coverage
10
-
11
- # next.js
12
- /.next/
13
- /out/
14
-
15
- # production
16
- /build
17
-
18
- # misc
19
- .DS_Store
20
- *.pem
21
-
22
- # debug
23
- npm-debug.log*
24
- yarn-debug.log*
25
- yarn-error.log*
26
-
27
- # local env files
28
- .env*.local
29
- .env
30
-
31
- # vercel
32
- .vercel
33
-
34
- # typescript
35
- *.tsbuildinfo
36
- next-env.d.ts
37
-
38
- # docker
39
- docker-compose.yml
40
-
41
- # prisma
42
- prisma/generated/*
43
-
44
- # pnpm
45
- pnpm-lock.yaml
46
- pnpm-*
@@ -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
- });