create-apppaaaul 2.0.15 → 2.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/dist/templates/nextjs-ts-clean/project/%%.gitignore +43 -0
- package/dist/templates/nextjs-ts-clean/project/eslint.config.mjs +137 -114
- package/dist/templates/nextjs-ts-clean/project/package.json +15 -15
- package/dist/templates/nextjs-ts-clean/project/src/scripts/dev-rebase.js +12 -2
- package/dist/templates/nextjs-ts-landing-prisma/project/%%.gitignore +46 -0
- package/dist/templates/nextjs-ts-landing-prisma/project/.env.test +26 -0
- package/dist/templates/nextjs-ts-landing-prisma/project/eslint.config.mjs +137 -114
- package/dist/templates/nextjs-ts-landing-prisma/project/package.json +34 -26
- package/dist/templates/nextjs-ts-landing-prisma/project/prisma/schema.prisma +74 -14
- 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/dist/templates/nextjs-ts-landing-prisma/project/src/scripts/dev-rebase.js +12 -2
- package/package.json +1 -1
- package/dist/templates/nextjs-ts-landing-drizzle/project/README.md +0 -15
- package/dist/templates/nextjs-ts-landing-drizzle/project/components.json +0 -21
- package/dist/templates/nextjs-ts-landing-drizzle/project/drizzle.config.ts +0 -11
- package/dist/templates/nextjs-ts-landing-drizzle/project/eslint.config.mjs +0 -183
- package/dist/templates/nextjs-ts-landing-drizzle/project/next.config.mjs +0 -10
- package/dist/templates/nextjs-ts-landing-drizzle/project/package.json +0 -61
- package/dist/templates/nextjs-ts-landing-drizzle/project/postcss.config.js +0 -3
- package/dist/templates/nextjs-ts-landing-drizzle/project/public/next.svg +0 -1
- package/dist/templates/nextjs-ts-landing-drizzle/project/public/vercel.svg +0 -1
- package/dist/templates/nextjs-ts-landing-drizzle/project/src/app/api/auth/[...nextauth]/route.ts +0 -3
- package/dist/templates/nextjs-ts-landing-drizzle/project/src/app/favicon.ico +0 -0
- package/dist/templates/nextjs-ts-landing-drizzle/project/src/app/globals.css +0 -161
- package/dist/templates/nextjs-ts-landing-drizzle/project/src/app/layout.tsx +0 -25
- package/dist/templates/nextjs-ts-landing-drizzle/project/src/app/page.tsx +0 -5
- package/dist/templates/nextjs-ts-landing-drizzle/project/src/auth.ts +0 -79
- package/dist/templates/nextjs-ts-landing-drizzle/project/src/components/ui/button.tsx +0 -49
- package/dist/templates/nextjs-ts-landing-drizzle/project/src/db/index.ts +0 -25
- package/dist/templates/nextjs-ts-landing-drizzle/project/src/db/schema.ts +0 -93
- package/dist/templates/nextjs-ts-landing-drizzle/project/src/lib/utils.ts +0 -6
- package/dist/templates/nextjs-ts-landing-drizzle/project/src/scripts/dev-rebase.js +0 -32
- package/dist/templates/nextjs-ts-landing-drizzle/project/tailwind.config.ts +0 -80
- package/dist/templates/nextjs-ts-landing-drizzle/project/tsconfig.json +0 -27
- 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
|
@@ -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
|
+
});
|
|
@@ -20,12 +20,22 @@ try {
|
|
|
20
20
|
|
|
21
21
|
if (isMerged) {
|
|
22
22
|
run(`git branch -d ${branch}`);
|
|
23
|
-
|
|
23
|
+
// Eliminar la rama del repositorio remoto si existe
|
|
24
|
+
try {
|
|
25
|
+
run(`git push origin --delete ${branch}`);
|
|
26
|
+
console.error(`✅ Rama ${branch} rebaseada, mergeada en dev y eliminada local y remotamente`);
|
|
27
|
+
} catch (remoteDeleteError) {
|
|
28
|
+
console.error(
|
|
29
|
+
`✅ Rama ${branch} rebaseada, mergeada en dev y eliminada localmente (no existía en remoto)`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
24
32
|
} else {
|
|
25
|
-
console.
|
|
33
|
+
console.error(
|
|
26
34
|
`⚠️ La rama ${branch} todavía no está completamente mergeada en dev, no se elimina.`,
|
|
27
35
|
);
|
|
28
36
|
}
|
|
37
|
+
run("git push origin dev");
|
|
38
|
+
run("git remote prune origin");
|
|
29
39
|
} catch (err) {
|
|
30
40
|
console.error("❌ Error en el proceso:", err.message);
|
|
31
41
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
## Getting Started with {{name}}
|
|
2
|
-
|
|
3
|
-
First, run the development server:
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
npm run dev
|
|
7
|
-
# or
|
|
8
|
-
yarn dev
|
|
9
|
-
# or
|
|
10
|
-
pnpm dev
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
14
|
-
|
|
15
|
-
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
-
"style": "new-york",
|
|
4
|
-
"rsc": true,
|
|
5
|
-
"tsx": true,
|
|
6
|
-
"tailwind": {
|
|
7
|
-
"config": "tailwind.config.ts",
|
|
8
|
-
"css": "src/app/globals.css",
|
|
9
|
-
"baseColor": "neutral",
|
|
10
|
-
"cssVariables": true,
|
|
11
|
-
"prefix": ""
|
|
12
|
-
},
|
|
13
|
-
"aliases": {
|
|
14
|
-
"components": "@/components",
|
|
15
|
-
"utils": "@/lib/utils",
|
|
16
|
-
"ui": "@/components/ui",
|
|
17
|
-
"lib": "@/lib",
|
|
18
|
-
"hooks": "@/hooks"
|
|
19
|
-
},
|
|
20
|
-
"iconLibrary": "lucide"
|
|
21
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { config } from "dotenv";
|
|
2
|
-
import { defineConfig } from "drizzle-kit";
|
|
3
|
-
config({ path: ".env" });
|
|
4
|
-
|
|
5
|
-
export default defineConfig({
|
|
6
|
-
schema: "./src/db/schema.ts",
|
|
7
|
-
dialect: "postgresql",
|
|
8
|
-
dbCredentials: {
|
|
9
|
-
url: process.env.DATABASE_URL!,
|
|
10
|
-
},
|
|
11
|
-
});
|