ar-saas 0.3.1 → 0.3.2
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/LICENSE +21 -21
- package/README.md +338 -314
- package/dist/cli.js +19 -0
- package/dist/generator.js +166 -55
- package/package.json +52 -50
- package/templates/backend/.env.example +67 -67
- package/templates/backend/.prettierrc +4 -4
- package/templates/backend/README.md +249 -168
- package/templates/backend/eslint.config.mjs +35 -35
- package/templates/backend/nest-cli.json +8 -8
- package/templates/backend/package-lock.json +10979 -10979
- package/templates/backend/package.json +88 -88
- package/templates/backend/src/app.controller.spec.ts +24 -24
- package/templates/backend/src/app.controller.ts +15 -15
- package/templates/backend/src/app.module.ts +40 -40
- package/templates/backend/src/app.service.ts +11 -11
- package/templates/backend/src/common/base/base.repository.ts +221 -221
- package/templates/backend/src/common/base/base.schema.ts +24 -24
- package/templates/backend/src/common/decorators/cookie.decorator.ts +9 -9
- package/templates/backend/src/common/decorators/current-user.decorator.ts +20 -20
- package/templates/backend/src/common/decorators/workspace-id.decorator.ts +14 -14
- package/templates/backend/src/common/filters/global-exception.filter.ts +61 -61
- package/templates/backend/src/common/guards/jwt-auth.guard.ts +5 -5
- package/templates/backend/src/common/interceptors/workspace-tenant.interceptor.ts +45 -45
- package/templates/backend/src/main.ts +51 -51
- package/templates/backend/src/modules/auth/auth.controller.ts +158 -158
- package/templates/backend/src/modules/auth/auth.module.ts +20 -20
- package/templates/backend/src/modules/auth/auth.service.ts +257 -257
- package/templates/backend/src/modules/auth/dto/forgot-password.dto.ts +9 -9
- package/templates/backend/src/modules/auth/dto/login.dto.ts +14 -14
- package/templates/backend/src/modules/auth/dto/refresh-token.dto.ts +12 -12
- package/templates/backend/src/modules/auth/dto/register.dto.ts +26 -26
- package/templates/backend/src/modules/auth/dto/reset-password.dto.ts +16 -16
- package/templates/backend/src/modules/auth/dto/verify-email.dto.ts +9 -9
- package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +43 -43
- package/templates/backend/src/modules/mail/mail.module.ts +9 -9
- package/templates/backend/src/modules/mail/mail.service.ts +141 -141
- package/templates/backend/src/modules/users/schemas/user.schema.ts +54 -54
- package/templates/backend/src/modules/users/users.module.ts +14 -14
- package/templates/backend/src/modules/users/users.repository.ts +51 -51
- package/templates/backend/src/modules/users/users.service.ts +104 -104
- package/templates/backend/src/modules/workspaces/schemas/workspace.schema.ts +26 -26
- package/templates/backend/src/modules/workspaces/workspaces.module.ts +16 -16
- package/templates/backend/src/modules/workspaces/workspaces.repository.ts +34 -34
- package/templates/backend/src/modules/workspaces/workspaces.service.ts +42 -42
- package/templates/backend/test/app.e2e-spec.ts +25 -25
- package/templates/backend/test/jest-e2e.json +9 -9
- package/templates/backend/tsconfig.build.json +4 -4
- package/templates/backend/tsconfig.json +26 -26
- package/templates/frontend/.env.local.example +1 -1
- package/templates/frontend/README.md +152 -0
- package/templates/frontend/components.json +20 -20
- package/templates/frontend/eslint.config.mjs +14 -14
- package/templates/frontend/next.config.ts +5 -5
- package/templates/frontend/package-lock.json +6722 -6722
- package/templates/frontend/package.json +48 -48
- package/templates/frontend/pnpm-lock.yaml +5012 -5012
- package/templates/frontend/pnpm-workspace.yaml +3 -3
- package/templates/frontend/postcss.config.mjs +7 -7
- package/templates/frontend/src/app/(auth)/forgot-password/page.tsx +84 -84
- package/templates/frontend/src/app/(auth)/layout.tsx +28 -28
- package/templates/frontend/src/app/(auth)/login/page.tsx +111 -111
- package/templates/frontend/src/app/(auth)/register/page.tsx +161 -161
- package/templates/frontend/src/app/(auth)/reset-password/page.tsx +120 -120
- package/templates/frontend/src/app/(auth)/verify-email/page.tsx +78 -78
- package/templates/frontend/src/app/(dashboard)/billing/page.tsx +111 -111
- package/templates/frontend/src/app/(dashboard)/dashboard/page.tsx +105 -105
- package/templates/frontend/src/app/(dashboard)/layout.tsx +38 -38
- package/templates/frontend/src/app/(dashboard)/profile/page.tsx +226 -226
- package/templates/frontend/src/app/(dashboard)/settings/page.tsx +156 -156
- package/templates/frontend/src/app/(dashboard)/team/page.tsx +178 -178
- package/templates/frontend/src/app/(legal)/privacy/page.tsx +127 -127
- package/templates/frontend/src/app/(legal)/terms/page.tsx +118 -118
- package/templates/frontend/src/app/globals.css +81 -81
- package/templates/frontend/src/app/layout.tsx +26 -26
- package/templates/frontend/src/app/page.tsx +5 -45
- package/templates/frontend/src/app/setup/page.tsx +371 -275
- package/templates/frontend/src/components/dashboard/header.tsx +89 -89
- package/templates/frontend/src/components/dashboard/sidebar.tsx +71 -71
- package/templates/frontend/src/components/dashboard/stat-card.tsx +34 -34
- package/templates/frontend/src/components/landing/faq.tsx +39 -39
- package/templates/frontend/src/components/landing/features.tsx +54 -54
- package/templates/frontend/src/components/landing/footer.tsx +76 -76
- package/templates/frontend/src/components/landing/hero.tsx +72 -72
- package/templates/frontend/src/components/landing/navbar.tsx +78 -78
- package/templates/frontend/src/components/landing/pricing.tsx +90 -90
- package/templates/frontend/src/components/ui/accordion.tsx +52 -52
- package/templates/frontend/src/components/ui/avatar.tsx +46 -46
- package/templates/frontend/src/components/ui/badge.tsx +30 -30
- package/templates/frontend/src/components/ui/button.tsx +52 -52
- package/templates/frontend/src/components/ui/card.tsx +50 -50
- package/templates/frontend/src/components/ui/checkbox.tsx +27 -27
- package/templates/frontend/src/components/ui/dialog.tsx +100 -100
- package/templates/frontend/src/components/ui/dropdown-menu.tsx +173 -173
- package/templates/frontend/src/components/ui/form.tsx +158 -158
- package/templates/frontend/src/components/ui/input.tsx +21 -21
- package/templates/frontend/src/components/ui/label.tsx +22 -22
- package/templates/frontend/src/components/ui/separator.tsx +25 -25
- package/templates/frontend/src/components/ui/skeleton.tsx +7 -7
- package/templates/frontend/src/components/ui/switch.tsx +28 -28
- package/templates/frontend/src/components/ui/tabs.tsx +54 -54
- package/templates/frontend/src/components/ui/textarea.tsx +20 -20
- package/templates/frontend/src/components/ui/toast.tsx +109 -109
- package/templates/frontend/src/components/ui/toaster.tsx +30 -30
- package/templates/frontend/src/config/site.ts +197 -197
- package/templates/frontend/src/hooks/use-toast.ts +116 -116
- package/templates/frontend/src/lib/api/auth.ts +39 -39
- package/templates/frontend/src/lib/api/client.ts +66 -66
- package/templates/frontend/src/lib/hooks/use-auth.ts +1 -1
- package/templates/frontend/src/lib/utils.ts +6 -6
- package/templates/frontend/src/providers/auth-provider.tsx +60 -60
- package/templates/frontend/src/types/api.ts +12 -12
- package/templates/frontend/src/types/auth.ts +27 -27
- package/templates/frontend/tsconfig.json +23 -23
|
@@ -1,275 +1,371 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from 'react'
|
|
4
|
-
import { useRouter } from 'next/navigation'
|
|
5
|
-
|
|
6
|
-
const BACKEND_REQUIRED = [
|
|
7
|
-
{
|
|
8
|
-
key: 'MONGODB_URI',
|
|
9
|
-
example: 'mongodb://localhost:27017/mi-saas',
|
|
10
|
-
desc: 'URI de conexión a MongoDB. Para desarrollo local instalar MongoDB o usar Atlas.',
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
key: 'JWT_ACCESS_SECRET',
|
|
14
|
-
example: '$(openssl rand -hex 64)',
|
|
15
|
-
desc: 'Secreto para firmar access tokens. Generar con OpenSSL.',
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
key: 'JWT_REFRESH_SECRET',
|
|
19
|
-
example: '$(openssl rand -hex 64)',
|
|
20
|
-
desc: 'Secreto para firmar refresh tokens. Usar un valor distinto al anterior.',
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
key: 'RESEND_API_KEY',
|
|
24
|
-
example: 're_xxxxxxxxxxxxxxxx',
|
|
25
|
-
desc: 'API Key de Resend para enviar emails. Crear cuenta en resend.com.',
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
key: 'RESEND_FROM_EMAIL',
|
|
29
|
-
example: 'noreply@tudominio.com',
|
|
30
|
-
desc: 'Email remitente. Debe estar verificado en tu cuenta de Resend.',
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
key: 'APP_URL',
|
|
34
|
-
example: 'http://localhost:3001',
|
|
35
|
-
desc: 'URL del frontend. Se usa en los links de los emails de verificación y reset.',
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
key: 'CORS_ORIGINS',
|
|
39
|
-
example: 'http://localhost:3001',
|
|
40
|
-
desc: 'URL del frontend separada por coma. Debe coincidir con APP_URL.',
|
|
41
|
-
},
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
const BACKEND_OPTIONAL = [
|
|
45
|
-
{ key: 'PORT', example: '3000', desc: 'Puerto del servidor (default: 3000).' },
|
|
46
|
-
{ key: 'JWT_ACCESS_EXPIRES_IN', example: '15m', desc: 'Duración del access token (default: 15m).' },
|
|
47
|
-
{ key: 'JWT_REFRESH_EXPIRES_IN', example: '7d', desc: 'Duración del refresh token (default: 7d).' },
|
|
48
|
-
{ key: 'RESEND_FROM_NAME', example: '"Mi SaaS"', desc: 'Nombre del remitente que ven los destinatarios.' },
|
|
49
|
-
{ key: 'COOKIE_SECRET', example: '$(openssl rand -hex 32)', desc: 'Secreto para firmar cookies.' },
|
|
50
|
-
]
|
|
51
|
-
|
|
52
|
-
const FRONTEND_REQUIRED = [
|
|
53
|
-
{
|
|
54
|
-
key: 'NEXT_PUBLIC_API_URL',
|
|
55
|
-
example: 'http://localhost:3000',
|
|
56
|
-
desc: 'URL base del backend. Debe coincidir con el PORT del backend.',
|
|
57
|
-
},
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
function EnvVar({ varKey, example, desc }: { varKey: string; example: string; desc: string }) {
|
|
61
|
-
const [copied, setCopied] = useState(false)
|
|
62
|
-
|
|
63
|
-
const copy = () => {
|
|
64
|
-
navigator.clipboard.writeText(`${varKey}=${example}`)
|
|
65
|
-
setCopied(true)
|
|
66
|
-
setTimeout(() => setCopied(false), 1500)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<div className="py-3 border-b border-zinc-100 last:border-0">
|
|
71
|
-
<div className="flex items-start justify-between gap-4">
|
|
72
|
-
<div className="flex-1 min-w-0">
|
|
73
|
-
<div className="flex items-center gap-2 mb-0.5">
|
|
74
|
-
<code className="text-sm font-mono font-semibold text-zinc-900">{varKey}</code>
|
|
75
|
-
</div>
|
|
76
|
-
<p className="text-xs text-zinc-500 leading-relaxed">{desc}</p>
|
|
77
|
-
<code className="text-xs text-zinc-400 font-mono">{example}</code>
|
|
78
|
-
</div>
|
|
79
|
-
<button
|
|
80
|
-
onClick={copy}
|
|
81
|
-
className="shrink-0 text-xs text-zinc-400 hover:text-zinc-700 transition-colors px-2 py-1 rounded border border-zinc-200 hover:border-zinc-300"
|
|
82
|
-
>
|
|
83
|
-
{copied ? '✓' : 'copiar'}
|
|
84
|
-
</button>
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function CodeBlock({ children }: { children: string }) {
|
|
91
|
-
const [copied, setCopied] = useState(false)
|
|
92
|
-
|
|
93
|
-
const copy = () => {
|
|
94
|
-
navigator.clipboard.writeText(children)
|
|
95
|
-
setCopied(true)
|
|
96
|
-
setTimeout(() => setCopied(false), 1500)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return (
|
|
100
|
-
<div className="relative group">
|
|
101
|
-
<pre className="bg-zinc-950 text-zinc-100 rounded-lg px-4 py-3 text-sm font-mono overflow-x-auto">
|
|
102
|
-
{children}
|
|
103
|
-
</pre>
|
|
104
|
-
<button
|
|
105
|
-
onClick={copy}
|
|
106
|
-
className="absolute top-2 right-2 text-xs text-zinc-500 hover:text-zinc-300 transition-colors bg-zinc-800 px-2 py-0.5 rounded"
|
|
107
|
-
>
|
|
108
|
-
{copied ? '✓' : 'copiar'}
|
|
109
|
-
</button>
|
|
110
|
-
</div>
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export default function SetupPage() {
|
|
115
|
-
const router = useRouter()
|
|
116
|
-
const [dismissed, setDismissed] = useState(false)
|
|
117
|
-
const [mounted, setMounted] = useState(false)
|
|
118
|
-
|
|
119
|
-
useEffect(() => {
|
|
120
|
-
setMounted(true)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
</
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
<div className="px-4">
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
<div className="
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
<div className="
|
|
216
|
-
<div className="
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
</div>
|
|
229
|
-
</div>
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
|
|
6
|
+
const BACKEND_REQUIRED = [
|
|
7
|
+
{
|
|
8
|
+
key: 'MONGODB_URI',
|
|
9
|
+
example: 'mongodb://localhost:27017/mi-saas',
|
|
10
|
+
desc: 'URI de conexión a MongoDB. Para desarrollo local instalar MongoDB o usar Atlas.',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
key: 'JWT_ACCESS_SECRET',
|
|
14
|
+
example: '$(openssl rand -hex 64)',
|
|
15
|
+
desc: 'Secreto para firmar access tokens. Generar con OpenSSL.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
key: 'JWT_REFRESH_SECRET',
|
|
19
|
+
example: '$(openssl rand -hex 64)',
|
|
20
|
+
desc: 'Secreto para firmar refresh tokens. Usar un valor distinto al anterior.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: 'RESEND_API_KEY',
|
|
24
|
+
example: 're_xxxxxxxxxxxxxxxx',
|
|
25
|
+
desc: 'API Key de Resend para enviar emails. Crear cuenta en resend.com.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: 'RESEND_FROM_EMAIL',
|
|
29
|
+
example: 'noreply@tudominio.com',
|
|
30
|
+
desc: 'Email remitente. Debe estar verificado en tu cuenta de Resend.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
key: 'APP_URL',
|
|
34
|
+
example: 'http://localhost:3001',
|
|
35
|
+
desc: 'URL del frontend. Se usa en los links de los emails de verificación y reset.',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: 'CORS_ORIGINS',
|
|
39
|
+
example: 'http://localhost:3001',
|
|
40
|
+
desc: 'URL del frontend separada por coma. Debe coincidir con APP_URL.',
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
const BACKEND_OPTIONAL = [
|
|
45
|
+
{ key: 'PORT', example: '3000', desc: 'Puerto del servidor (default: 3000).' },
|
|
46
|
+
{ key: 'JWT_ACCESS_EXPIRES_IN', example: '15m', desc: 'Duración del access token (default: 15m).' },
|
|
47
|
+
{ key: 'JWT_REFRESH_EXPIRES_IN', example: '7d', desc: 'Duración del refresh token (default: 7d).' },
|
|
48
|
+
{ key: 'RESEND_FROM_NAME', example: '"Mi SaaS"', desc: 'Nombre del remitente que ven los destinatarios.' },
|
|
49
|
+
{ key: 'COOKIE_SECRET', example: '$(openssl rand -hex 32)', desc: 'Secreto para firmar cookies.' },
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
const FRONTEND_REQUIRED = [
|
|
53
|
+
{
|
|
54
|
+
key: 'NEXT_PUBLIC_API_URL',
|
|
55
|
+
example: 'http://localhost:3000',
|
|
56
|
+
desc: 'URL base del backend. Debe coincidir con el PORT del backend.',
|
|
57
|
+
},
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
function EnvVar({ varKey, example, desc }: { varKey: string; example: string; desc: string }) {
|
|
61
|
+
const [copied, setCopied] = useState(false)
|
|
62
|
+
|
|
63
|
+
const copy = () => {
|
|
64
|
+
navigator.clipboard.writeText(`${varKey}=${example}`)
|
|
65
|
+
setCopied(true)
|
|
66
|
+
setTimeout(() => setCopied(false), 1500)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="py-3 border-b border-zinc-100 last:border-0">
|
|
71
|
+
<div className="flex items-start justify-between gap-4">
|
|
72
|
+
<div className="flex-1 min-w-0">
|
|
73
|
+
<div className="flex items-center gap-2 mb-0.5">
|
|
74
|
+
<code className="text-sm font-mono font-semibold text-zinc-900">{varKey}</code>
|
|
75
|
+
</div>
|
|
76
|
+
<p className="text-xs text-zinc-500 leading-relaxed">{desc}</p>
|
|
77
|
+
<code className="text-xs text-zinc-400 font-mono">{example}</code>
|
|
78
|
+
</div>
|
|
79
|
+
<button
|
|
80
|
+
onClick={copy}
|
|
81
|
+
className="shrink-0 text-xs text-zinc-400 hover:text-zinc-700 transition-colors px-2 py-1 rounded border border-zinc-200 hover:border-zinc-300"
|
|
82
|
+
>
|
|
83
|
+
{copied ? '✓' : 'copiar'}
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function CodeBlock({ children }: { children: string }) {
|
|
91
|
+
const [copied, setCopied] = useState(false)
|
|
92
|
+
|
|
93
|
+
const copy = () => {
|
|
94
|
+
navigator.clipboard.writeText(children)
|
|
95
|
+
setCopied(true)
|
|
96
|
+
setTimeout(() => setCopied(false), 1500)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="relative group">
|
|
101
|
+
<pre className="bg-zinc-950 text-zinc-100 rounded-lg px-4 py-3 text-sm font-mono overflow-x-auto">
|
|
102
|
+
{children}
|
|
103
|
+
</pre>
|
|
104
|
+
<button
|
|
105
|
+
onClick={copy}
|
|
106
|
+
className="absolute top-2 right-2 text-xs text-zinc-500 hover:text-zinc-300 transition-colors bg-zinc-800 px-2 py-0.5 rounded"
|
|
107
|
+
>
|
|
108
|
+
{copied ? '✓' : 'copiar'}
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default function SetupPage() {
|
|
115
|
+
const router = useRouter()
|
|
116
|
+
const [dismissed, setDismissed] = useState(false)
|
|
117
|
+
const [mounted, setMounted] = useState(false)
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
setMounted(true)
|
|
121
|
+
if (typeof window !== 'undefined' && localStorage.getItem('setup_dismissed') === 'true') {
|
|
122
|
+
router.replace('/login')
|
|
123
|
+
}
|
|
124
|
+
}, [router])
|
|
125
|
+
|
|
126
|
+
const handleDone = () => {
|
|
127
|
+
localStorage.setItem('setup_dismissed', 'true')
|
|
128
|
+
setDismissed(true)
|
|
129
|
+
router.push('/login')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!mounted || dismissed) return null
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className="min-h-screen bg-zinc-50">
|
|
136
|
+
<div className="max-w-3xl mx-auto px-4 py-12">
|
|
137
|
+
|
|
138
|
+
{/* Header */}
|
|
139
|
+
<div className="mb-10">
|
|
140
|
+
<div className="inline-flex items-center gap-2 bg-zinc-900 text-white text-xs font-mono px-3 py-1.5 rounded-full mb-4">
|
|
141
|
+
<span className="w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse" />
|
|
142
|
+
create-saas-ar
|
|
143
|
+
</div>
|
|
144
|
+
<h1 className="text-3xl font-bold text-zinc-900 mb-2">
|
|
145
|
+
Tu proyecto está listo
|
|
146
|
+
</h1>
|
|
147
|
+
<p className="text-zinc-500 text-lg">
|
|
148
|
+
Completá la configuración antes de empezar a desarrollar.
|
|
149
|
+
Son 5 minutos y después tenés todo andando.
|
|
150
|
+
</p>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{/* MongoDB */}
|
|
154
|
+
<section className="mb-8">
|
|
155
|
+
<div className="flex items-center gap-3 mb-4">
|
|
156
|
+
<div className="w-7 h-7 rounded-full bg-zinc-900 text-white text-xs font-bold flex items-center justify-center">1</div>
|
|
157
|
+
<h2 className="text-lg font-semibold text-zinc-900">Levantar MongoDB</h2>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div className="bg-white border border-zinc-200 rounded-xl overflow-hidden mb-3">
|
|
161
|
+
<div className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100 flex items-center gap-2">
|
|
162
|
+
<span className="text-xs font-semibold text-zinc-700">Opción A — Docker</span>
|
|
163
|
+
<span className="text-xs bg-zinc-900 text-white px-1.5 py-0.5 rounded font-mono">recomendado</span>
|
|
164
|
+
</div>
|
|
165
|
+
<div className="p-4 space-y-3">
|
|
166
|
+
<p className="text-xs text-zinc-500">El proyecto ya incluye un <code className="bg-zinc-100 px-1 py-0.5 rounded">docker-compose.dev.yml</code> en la raíz. Solo necesitás tener Docker instalado.</p>
|
|
167
|
+
<CodeBlock>{`# Desde la raíz del proyecto (no desde /backend)
|
|
168
|
+
docker compose -f docker-compose.dev.yml up -d
|
|
169
|
+
|
|
170
|
+
# Verificar que está corriendo
|
|
171
|
+
docker ps`}</CodeBlock>
|
|
172
|
+
<p className="text-xs text-zinc-400">MongoDB queda disponible en <code className="bg-zinc-100 px-1 py-0.5 rounded">mongodb://localhost:27017</code>. Los datos persisten en un volumen de Docker.</p>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<details className="bg-white border border-zinc-200 rounded-xl overflow-hidden mb-3">
|
|
177
|
+
<summary className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100 cursor-pointer text-xs font-semibold text-zinc-500 uppercase tracking-wider hover:text-zinc-700">
|
|
178
|
+
Opción B — MongoDB Atlas (cloud gratuito)
|
|
179
|
+
</summary>
|
|
180
|
+
<div className="p-4 space-y-2">
|
|
181
|
+
<ol className="text-xs text-zinc-600 space-y-1.5 list-none">
|
|
182
|
+
{[
|
|
183
|
+
'Crear cuenta gratis en mongodb.com/atlas',
|
|
184
|
+
'Crear un cluster gratuito (M0 Free Tier)',
|
|
185
|
+
'En "Database Access" → crear un usuario con contraseña',
|
|
186
|
+
'En "Network Access" → agregar tu IP (o 0.0.0.0/0 para dev)',
|
|
187
|
+
'En "Connect" → elegir "Connect your application" → copiar la URI',
|
|
188
|
+
'Reemplazar <password> en la URI con la contraseña del paso 3',
|
|
189
|
+
'Pegar la URI completa en MONGODB_URI del .env del backend',
|
|
190
|
+
].map((step, i) => (
|
|
191
|
+
<li key={i} className="flex gap-2">
|
|
192
|
+
<span className="shrink-0 w-4 h-4 rounded-full bg-zinc-200 text-zinc-600 flex items-center justify-center font-bold text-[10px]">{i + 1}</span>
|
|
193
|
+
{step}
|
|
194
|
+
</li>
|
|
195
|
+
))}
|
|
196
|
+
</ol>
|
|
197
|
+
</div>
|
|
198
|
+
</details>
|
|
199
|
+
|
|
200
|
+
<details className="bg-white border border-zinc-200 rounded-xl overflow-hidden">
|
|
201
|
+
<summary className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100 cursor-pointer text-xs font-semibold text-zinc-500 uppercase tracking-wider hover:text-zinc-700">
|
|
202
|
+
Opción C — MongoDB local sin Docker
|
|
203
|
+
</summary>
|
|
204
|
+
<div className="p-4">
|
|
205
|
+
<p className="text-xs text-zinc-500">
|
|
206
|
+
Instalá MongoDB Community Edition siguiendo las instrucciones oficiales para tu sistema operativo:{' '}
|
|
207
|
+
<span className="font-mono text-zinc-700">docs.mongodb.com/manual/installation</span>
|
|
208
|
+
</p>
|
|
209
|
+
</div>
|
|
210
|
+
</details>
|
|
211
|
+
</section>
|
|
212
|
+
|
|
213
|
+
{/* Backend */}
|
|
214
|
+
<section className="mb-8">
|
|
215
|
+
<div className="flex items-center gap-3 mb-4">
|
|
216
|
+
<div className="w-7 h-7 rounded-full bg-zinc-900 text-white text-xs font-bold flex items-center justify-center">2</div>
|
|
217
|
+
<h2 className="text-lg font-semibold text-zinc-900">Backend — configurar <code className="text-sm bg-zinc-100 px-1.5 py-0.5 rounded">.env</code></h2>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<div className="bg-white border border-zinc-200 rounded-xl overflow-hidden mb-4">
|
|
221
|
+
<div className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100">
|
|
222
|
+
<span className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Variables requeridas</span>
|
|
223
|
+
</div>
|
|
224
|
+
<div className="px-4">
|
|
225
|
+
{BACKEND_REQUIRED.map((v) => (
|
|
226
|
+
<EnvVar key={v.key} varKey={v.key} example={v.example} desc={v.desc} />
|
|
227
|
+
))}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 mb-4">
|
|
232
|
+
<p className="text-xs font-semibold text-amber-800 mb-2">Generar JWT secrets</p>
|
|
233
|
+
<p className="text-xs text-amber-700 mb-2">Usá cualquiera de estos comandos para generar valores seguros para <code className="bg-amber-100 px-1 rounded">JWT_ACCESS_SECRET</code> y <code className="bg-amber-100 px-1 rounded">JWT_REFRESH_SECRET</code>. Usá un valor distinto para cada uno.</p>
|
|
234
|
+
<CodeBlock>{`# Con Node.js (disponible si tenés Node instalado)
|
|
235
|
+
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
|
|
236
|
+
|
|
237
|
+
# Con openssl
|
|
238
|
+
openssl rand -hex 64`}</CodeBlock>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<details className="bg-white border border-zinc-200 rounded-xl overflow-hidden mb-4">
|
|
242
|
+
<summary className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100 cursor-pointer text-xs font-semibold text-zinc-500 uppercase tracking-wider hover:text-zinc-700">
|
|
243
|
+
Variables opcionales
|
|
244
|
+
</summary>
|
|
245
|
+
<div className="px-4">
|
|
246
|
+
{BACKEND_OPTIONAL.map((v) => (
|
|
247
|
+
<EnvVar key={v.key} varKey={v.key} example={v.example} desc={v.desc} />
|
|
248
|
+
))}
|
|
249
|
+
</div>
|
|
250
|
+
</details>
|
|
251
|
+
|
|
252
|
+
<div className="bg-white border border-zinc-200 rounded-xl overflow-hidden mb-4">
|
|
253
|
+
<div className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100">
|
|
254
|
+
<span className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Configurar Resend (emails)</span>
|
|
255
|
+
</div>
|
|
256
|
+
<div className="p-4 space-y-2">
|
|
257
|
+
<ol className="text-xs text-zinc-600 space-y-1.5 list-none">
|
|
258
|
+
{[
|
|
259
|
+
'Crear cuenta gratis en resend.com (3 000 emails/mes gratis)',
|
|
260
|
+
'Ir a API Keys → "Create API Key" → copiar la key',
|
|
261
|
+
'Pegar en RESEND_API_KEY del .env',
|
|
262
|
+
'Para dev podés usar el dominio sandbox de Resend (solo envía a tu propio email)',
|
|
263
|
+
'Para prod: agregar y verificar tu dominio en "Domains"',
|
|
264
|
+
].map((step, i) => (
|
|
265
|
+
<li key={i} className="flex gap-2">
|
|
266
|
+
<span className="shrink-0 w-4 h-4 rounded-full bg-zinc-200 text-zinc-600 flex items-center justify-center font-bold text-[10px]">{i + 1}</span>
|
|
267
|
+
{step}
|
|
268
|
+
</li>
|
|
269
|
+
))}
|
|
270
|
+
</ol>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div className="bg-white border border-zinc-200 rounded-xl overflow-hidden">
|
|
275
|
+
<div className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100">
|
|
276
|
+
<span className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Iniciar el backend</span>
|
|
277
|
+
</div>
|
|
278
|
+
<div className="p-4 space-y-3">
|
|
279
|
+
<CodeBlock>{`cd backend
|
|
280
|
+
# .env ya fue copiado automáticamente desde .env.example
|
|
281
|
+
# Completar las variables faltantes (JWT secrets, Resend key)
|
|
282
|
+
npm install
|
|
283
|
+
npm run start:dev`}</CodeBlock>
|
|
284
|
+
<p className="text-xs text-zinc-400">
|
|
285
|
+
El servidor levanta en <code className="bg-zinc-100 px-1 py-0.5 rounded">http://localhost:3000</code>.
|
|
286
|
+
Swagger en <code className="bg-zinc-100 px-1 py-0.5 rounded">http://localhost:3000/api/docs</code>.
|
|
287
|
+
</p>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
</section>
|
|
291
|
+
|
|
292
|
+
{/* Frontend */}
|
|
293
|
+
<section className="mb-8">
|
|
294
|
+
<div className="flex items-center gap-3 mb-4">
|
|
295
|
+
<div className="w-7 h-7 rounded-full bg-zinc-900 text-white text-xs font-bold flex items-center justify-center">3</div>
|
|
296
|
+
<h2 className="text-lg font-semibold text-zinc-900">Frontend — configurar <code className="text-sm bg-zinc-100 px-1.5 py-0.5 rounded">.env.local</code></h2>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
<div className="bg-white border border-zinc-200 rounded-xl overflow-hidden mb-4">
|
|
300
|
+
<div className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100">
|
|
301
|
+
<span className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Variables requeridas</span>
|
|
302
|
+
</div>
|
|
303
|
+
<div className="px-4">
|
|
304
|
+
{FRONTEND_REQUIRED.map((v) => (
|
|
305
|
+
<EnvVar key={v.key} varKey={v.key} example={v.example} desc={v.desc} />
|
|
306
|
+
))}
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<div className="bg-white border border-zinc-200 rounded-xl overflow-hidden">
|
|
311
|
+
<div className="px-4 py-2.5 bg-zinc-50 border-b border-zinc-100">
|
|
312
|
+
<span className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">Iniciar el frontend</span>
|
|
313
|
+
</div>
|
|
314
|
+
<div className="p-4 space-y-3">
|
|
315
|
+
<CodeBlock>{`cd frontend
|
|
316
|
+
# .env.local ya fue copiado automáticamente desde .env.local.example
|
|
317
|
+
# Ajustar NEXT_PUBLIC_API_URL si cambiaste el PORT del backend
|
|
318
|
+
npm install
|
|
319
|
+
npm run dev`}</CodeBlock>
|
|
320
|
+
<p className="text-xs text-zinc-400">
|
|
321
|
+
El frontend levanta en <code className="bg-zinc-100 px-1 py-0.5 rounded">http://localhost:3001</code> (o el puerto que indique Next.js).
|
|
322
|
+
</p>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</section>
|
|
326
|
+
|
|
327
|
+
{/* Verificación */}
|
|
328
|
+
<section className="mb-10">
|
|
329
|
+
<div className="flex items-center gap-3 mb-4">
|
|
330
|
+
<div className="w-7 h-7 rounded-full bg-zinc-900 text-white text-xs font-bold flex items-center justify-center">4</div>
|
|
331
|
+
<h2 className="text-lg font-semibold text-zinc-900">Verificar que todo funciona</h2>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<div className="bg-white border border-zinc-200 rounded-xl p-4 space-y-2">
|
|
335
|
+
{[
|
|
336
|
+
{ label: 'MongoDB corriendo', hint: 'docker ps muestra el contenedor activo, o verificá que el proceso mongod esté levantado. Si el backend da ECONNREFUSED, MongoDB no está corriendo.' },
|
|
337
|
+
{ label: 'Backend corre sin errores', hint: 'npm run start:dev no muestra errores rojos. Errores de JWT_SECRET faltante o MONGODB_URI indican variables sin completar.' },
|
|
338
|
+
{ label: 'Swagger disponible', hint: 'http://localhost:3000/api/docs carga correctamente y lista los endpoints de auth.' },
|
|
339
|
+
{ label: 'MongoDB conectado', hint: 'El log del backend dice "Connected to MongoDB successfully". Si no aparece, verificá MONGODB_URI en backend/.env.' },
|
|
340
|
+
{ label: 'Registro funciona', hint: 'Ir a http://localhost:3001/register y crear una cuenta. Si falla con CORS, verificar que CORS_ORIGINS en backend/.env incluya http://localhost:3001.' },
|
|
341
|
+
{ label: 'Email de verificación llega', hint: 'Revisar spam si no aparece en inbox. Si no llega, verificar RESEND_API_KEY y RESEND_FROM_EMAIL en backend/.env.' },
|
|
342
|
+
].map((item) => (
|
|
343
|
+
<div key={item.label} className="flex items-start gap-3 py-1.5">
|
|
344
|
+
<div className="w-4 h-4 rounded border-2 border-zinc-300 mt-0.5 shrink-0" />
|
|
345
|
+
<div>
|
|
346
|
+
<p className="text-sm text-zinc-800 font-medium">{item.label}</p>
|
|
347
|
+
<p className="text-xs text-zinc-400">{item.hint}</p>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
))}
|
|
351
|
+
</div>
|
|
352
|
+
</section>
|
|
353
|
+
|
|
354
|
+
{/* CTA */}
|
|
355
|
+
<div className="flex items-center justify-between">
|
|
356
|
+
<p className="text-xs text-zinc-400">
|
|
357
|
+
Esta pantalla no vuelve a aparecer. Podés volver desde{' '}
|
|
358
|
+
<code className="bg-zinc-100 px-1 py-0.5 rounded">/setup</code>.
|
|
359
|
+
</p>
|
|
360
|
+
<button
|
|
361
|
+
onClick={handleDone}
|
|
362
|
+
className="bg-zinc-900 hover:bg-zinc-700 text-white font-medium text-sm px-6 py-2.5 rounded-lg transition-colors"
|
|
363
|
+
>
|
|
364
|
+
Todo listo, ir al login →
|
|
365
|
+
</button>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
)
|
|
371
|
+
}
|