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.
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -1
- package/dist/templates/nextjs-ts-clean/project/package.json +15 -15
- package/dist/templates/nextjs-ts-landing-prisma/project/.env.test +26 -0
- package/dist/templates/nextjs-ts-landing-prisma/project/package.json +34 -26
- package/dist/templates/nextjs-ts-landing-prisma/project/prisma/schema.prisma +72 -12
- package/dist/templates/nextjs-ts-landing-prisma/project/src/app/api/auth/[...all]/route.ts +5 -0
- package/dist/templates/nextjs-ts-landing-prisma/project/src/app/not-found.tsx +118 -0
- package/dist/templates/nextjs-ts-landing-prisma/project/src/lib/auth-client.ts +9 -0
- package/dist/templates/nextjs-ts-landing-prisma/project/src/lib/auth-utils.ts +197 -0
- package/dist/templates/nextjs-ts-landing-prisma/project/src/lib/auth.ts +49 -0
- package/dist/templates/nextjs-ts-landing-prisma/project/src/lib/cloudflare-r2.ts +124 -0
- package/dist/templates/nextjs-ts-landing-prisma/project/src/lib/db.ts +24 -5
- package/dist/templates/nextjs-ts-landing-prisma/project/src/lib/env.ts +44 -0
- package/dist/templates/nextjs-ts-landing-prisma/project/src/middleware.ts +105 -0
- package/dist/templates/nextjs-ts-landing-prisma/project/src/scripts/db-seed.ts +16 -0
- package/package.json +1 -1
- package/dist/templates/nextjs-ts-clean/project/%%.gitignore +0 -43
- package/dist/templates/nextjs-ts-landing-prisma/project/%%.gitignore +0 -46
- package/dist/templates/nextjs-ts-landing-prisma/project/src/app/api/auth/[...nextauth]/route.ts +0 -3
- 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
|
|
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.
|
|
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.
|
|
20
|
-
"next": "^
|
|
19
|
+
"lucide-react": "^0.547.0",
|
|
20
|
+
"next": "^16.0.0",
|
|
21
21
|
"postcss": "^8.5.6",
|
|
22
|
-
"react": "19.
|
|
23
|
-
"react-dom": "19.
|
|
22
|
+
"react": "19.2.0",
|
|
23
|
+
"react-dom": "19.2.0",
|
|
24
24
|
"tailwind-merge": "^3.3.1",
|
|
25
|
-
"tailwindcss": "^4.1.
|
|
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": "
|
|
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": "^
|
|
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": "^
|
|
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.
|
|
48
|
-
"tsx": "^4.20.
|
|
49
|
-
"typescript": "^5.9.
|
|
50
|
-
"typescript-eslint": "^8.
|
|
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
|
-
"
|
|
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.
|
|
16
|
-
"@
|
|
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
|
-
"@
|
|
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.
|
|
24
|
-
"lucide-react": "^0.
|
|
25
|
-
"
|
|
26
|
-
"next
|
|
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.
|
|
30
|
-
"react-dom": "^19.
|
|
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.
|
|
41
|
+
"tailwindcss": "^4.1.16",
|
|
34
42
|
"tailwindcss-animate": "^1.0.7",
|
|
35
|
-
"zod": "^4.1.
|
|
43
|
+
"zod": "^4.1.12"
|
|
36
44
|
},
|
|
37
45
|
"devDependencies": {
|
|
38
|
-
"@eslint/compat": "^1.
|
|
39
|
-
"@next/eslint-plugin-next": "
|
|
40
|
-
"@types/node": "^24.
|
|
41
|
-
"@types/react": "^19.
|
|
42
|
-
"@types/react-dom": "^19.
|
|
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.
|
|
45
|
-
"eslint-config-next": "^
|
|
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": "^
|
|
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.
|
|
56
|
-
"prisma": "^6.
|
|
57
|
-
"tsx": "^4.20.
|
|
58
|
-
"typescript": "^5.9.
|
|
59
|
-
"typescript-eslint": "^8.
|
|
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
|
-
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
datasource db {
|
|
7
|
+
provider = "postgresql"
|
|
8
|
+
url = env("DATABASE_URL")
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
model Post {
|
|
12
|
-
id
|
|
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
|
|
17
|
+
authorId String?
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
model User {
|
|
21
|
-
id
|
|
22
|
-
email
|
|
23
|
-
name
|
|
24
|
-
posts
|
|
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,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 {
|
|
3
|
+
const globalForPrisma = global as unknown as {
|
|
4
|
+
db: PrismaClient | undefined;
|
|
5
|
+
};
|
|
4
6
|
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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,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,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
|
-
});
|