ar-saas 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/generator.js +2 -6
  2. package/package.json +1 -1
  3. package/templates/backend/.env.example +1 -1
  4. package/templates/backend/README.md +6 -6
  5. package/templates/backend/package.json +5 -2
  6. package/templates/backend/src/app.module.ts +68 -40
  7. package/templates/backend/src/common/interceptors/workspace-tenant.interceptor.ts +27 -45
  8. package/templates/backend/src/main.ts +50 -51
  9. package/templates/backend/src/modules/auth/auth.controller.ts +162 -158
  10. package/templates/backend/src/modules/auth/auth.service.ts +236 -257
  11. package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +45 -43
  12. package/templates/backend/src/modules/users/users.controller.ts +28 -0
  13. package/templates/backend/src/modules/users/users.module.ts +16 -14
  14. package/templates/backend/src/modules/users/users.repository.ts +57 -51
  15. package/templates/backend/src/modules/users/users.service.ts +130 -104
  16. package/templates/backend/src/modules/workspaces/workspaces.repository.ts +38 -34
  17. package/templates/backend/src/modules/workspaces/workspaces.service.ts +51 -42
  18. package/templates/frontend/README.md +2 -2
  19. package/templates/frontend/package.json +2 -6
  20. package/templates/frontend/pnpm-workspace.yaml +2 -2
  21. package/templates/frontend/src/app/(auth)/layout.tsx +29 -28
  22. package/templates/frontend/src/app/(dashboard)/billing/page.tsx +111 -111
  23. package/templates/frontend/src/app/(dashboard)/profile/page.tsx +241 -226
  24. package/templates/frontend/src/app/(dashboard)/settings/page.tsx +155 -156
  25. package/templates/frontend/src/app/(dashboard)/team/page.tsx +179 -178
  26. package/templates/frontend/src/app/layout.tsx +29 -26
  27. package/templates/frontend/src/app/page.tsx +1 -1
  28. package/templates/frontend/src/app/setup/page.tsx +1 -1
  29. package/templates/frontend/src/components/dashboard/header.tsx +5 -3
  30. package/templates/frontend/src/config/site.ts +1 -1
package/dist/generator.js CHANGED
@@ -184,8 +184,6 @@ function buildDockerCompose(config) {
184
184
 
185
185
  mongodb:
186
186
  image: mongo:7
187
- ports:
188
- - "27017:27017"
189
187
  volumes:
190
188
  - mongodb_data:/data/db
191
189
  restart: unless-stopped`);
@@ -202,9 +200,7 @@ function buildDockerCompose(config) {
202
200
  ${hasBackend ? 'depends_on:\n - backend\n ' : ''}restart: unless-stopped`);
203
201
  }
204
202
  const volumes = hasBackend ? '\nvolumes:\n mongodb_data:' : '';
205
- return `version: '3.8'
206
-
207
- services:
203
+ return `services:
208
204
  ${services.join('\n\n')}
209
205
  ${volumes}
210
206
  `;
@@ -215,7 +211,7 @@ builder = "nixpacks"
215
211
 
216
212
  [deploy]
217
213
  startCommand = "npm run start:prod"
218
- healthcheckPath = "/api/health"
214
+ healthcheckPath = "/api"
219
215
  healthcheckTimeout = 30
220
216
  restartPolicyType = "on_failure"
221
217
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ar-saas",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
4
4
  "description": "Generador de proyectos SaaS multi-tenant para startups argentinas. Landing page, auth, dashboard y legal listos para producción.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  # =========================================
2
- # create-saas-ar Backend
2
+ # ar-saas Backend
3
3
  # Variables de entorno
4
4
  # =========================================
5
5
  # Copiá este archivo a .env y completá los valores
@@ -1,6 +1,6 @@
1
1
  <p align="center"><samp>
2
2
  ╔══════════════════════════════════╗<br/>
3
- ║&nbsp;&nbsp;create-saas-ar-backend&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>
3
+ ║&nbsp;&nbsp;ar-saas-backend&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>
4
4
  ║&nbsp;&nbsp;AFIP&nbsp;·&nbsp;Mercado&nbsp;Pago&nbsp;·&nbsp;Auth&nbsp;·&nbsp;Mail&nbsp;&nbsp;<br/>
5
5
  ║&nbsp;&nbsp;todo&nbsp;integrado&nbsp;en&nbsp;un&nbsp;comando&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>
6
6
  ╚══════════════════════════════════╝<br/>
@@ -23,7 +23,7 @@ manejo de errores. Y cuando por fin terminás todo ese boilerplate, te das cuent
23
23
  de que todavía falta integrar AFIP para facturación electrónica y Mercado Pago
24
24
  para cobrar. Otras 2 semanas.
25
25
 
26
- `create-saas-ar` genera este repositorio completo **en un comando**. Viene con
26
+ `ar-saas` genera este repositorio completo **en un comando**. Viene con
27
27
  todo lo genérico ya resuelto, más integraciones reales para operar en Argentina.
28
28
  **Solo tenés que agregar tus módulos de negocio** siguiendo el patrón de
29
29
  `.ai-docs/`.
@@ -51,9 +51,9 @@ todo lo genérico ya resuelto, más integraciones reales para operar en Argentin
51
51
 
52
52
  ## Características principales
53
53
 
54
- - **Multi-tenancy real** — Aislamiento por `workspaceId` garantizado en cada query. Un `WorkspaceTenantInterceptor` extrae el tenant del header `x-workspace-id` o del JWT. `BaseRepository` fuerza el filtro en toda operación. Imposible leakear datos entre workspaces por error humano.
54
+ - **Multi-tenancy real** — Aislamiento por `workspaceId` garantizado en cada query. El `workspaceId` se extrae del JWT verificado del usuario autenticado. `BaseRepository` fuerza el filtro en toda operación. Imposible leakear datos entre workspaces por error humano.
55
55
 
56
- - **Auth completo** — Registro, login, refresh token rotativo, email verification, password reset, change password. Access + refresh tokens en cookies `HttpOnly`, `Secure`, `SameSite=Strict`. Nunca en `localStorage` ni en el body.
56
+ - **Auth completo** — Registro, login, refresh token rotativo, email verification, password reset. Access + refresh tokens en cookies `HttpOnly`, `Secure`, `SameSite=Strict`. Con rate limiting en endpoints sensibles y Helmet para headers de seguridad. Nunca en `localStorage` ni en el body.
57
57
 
58
58
  - **BaseRepository genérico** — Soft delete, paginación, filtros dinámicos, agregaciones, conteo, upsert. Manejo automático de errores MongoDB (duplicate key, cast error). 12 métodos heredados por todos los repositorios.
59
59
 
@@ -197,7 +197,7 @@ src/
197
197
  │ ├── guards/
198
198
  │ │ └── jwt-auth.guard.ts # AuthGuard('jwt')
199
199
  │ └── interceptors/
200
- │ └── workspace-tenant.interceptor.ts # x-workspace-id header request.workspaceId
200
+ │ └── workspace-tenant.interceptor.ts # Extrae workspaceId del JWT verificado
201
201
  ├── modules/
202
202
  │ ├── auth/ # Registro, login, refresh, email verification, password reset
203
203
  │ ├── users/ # CRUD de usuarios
@@ -246,4 +246,4 @@ El archivo `.ai-docs/examples/full-module-example.md` tiene el módulo Clientes
246
246
 
247
247
  ## Licencia
248
248
 
249
- Privado (UNLICENSED). Generado con [`create-saas-ar`](https://github.com/anomalyco/create-saas-ar).
249
+ Privado (UNLICENSED). Generado con [`ar-saas`](https://github.com/anomalyco/ar-saas).
@@ -1,7 +1,7 @@
1
1
  {
2
- "name": "create-saas-ar-backend",
2
+ "name": "ar-saas-backend",
3
3
  "version": "0.0.1",
4
- "description": "Backend SaaS multi-tenant generado con create-saas-ar",
4
+ "description": "Backend SaaS multi-tenant generado con ar-saas",
5
5
  "author": "",
6
6
  "private": true,
7
7
  "license": "UNLICENSED",
@@ -29,10 +29,13 @@
29
29
  "@nestjs/platform-express": "^11.0.1",
30
30
  "@nestjs/schedule": "^6.0.1",
31
31
  "@nestjs/swagger": "^11.2.1",
32
+ "@nestjs/throttler": "^6.0.0",
32
33
  "bcryptjs": "^2.4.3",
33
34
  "class-transformer": "^0.5.1",
34
35
  "class-validator": "^0.14.2",
35
36
  "cookie-parser": "^1.4.7",
37
+ "helmet": "^8.0.0",
38
+ "joi": "^17.13.3",
36
39
  "mongoose": "^9.0.1",
37
40
  "passport": "^0.7.0",
38
41
  "passport-jwt": "^4.0.1",
@@ -1,40 +1,68 @@
1
- import { Module } from '@nestjs/common';
2
- import { ConfigModule, ConfigService } from '@nestjs/config';
3
- import { MongooseModule } from '@nestjs/mongoose';
4
- import { APP_FILTER } from '@nestjs/core';
5
- import { AppController } from './app.controller';
6
- import { AppService } from './app.service';
7
- import { GlobalExceptionFilter } from './common/filters/global-exception.filter';
8
- import { AuthModule } from './modules/auth/auth.module';
9
- import { MailModule } from './modules/mail/mail.module';
10
- import { UsersModule } from './modules/users/users.module';
11
- import { WorkspacesModule } from './modules/workspaces/workspaces.module';
12
-
13
- @Module({
14
- imports: [
15
- ConfigModule.forRoot({
16
- isGlobal: true,
17
- envFilePath: '.env',
18
- }),
19
- MongooseModule.forRootAsync({
20
- imports: [ConfigModule],
21
- inject: [ConfigService],
22
- useFactory: (config: ConfigService) => ({
23
- uri: config.getOrThrow<string>('MONGODB_URI'),
24
- }),
25
- }),
26
- MailModule,
27
- UsersModule,
28
- WorkspacesModule,
29
- AuthModule,
30
- ],
31
- controllers: [AppController],
32
- providers: [
33
- AppService,
34
- {
35
- provide: APP_FILTER,
36
- useClass: GlobalExceptionFilter,
37
- },
38
- ],
39
- })
40
- export class AppModule {}
1
+ import { Module } from '@nestjs/common';
2
+ import { ConfigModule, ConfigService } from '@nestjs/config';
3
+ import { MongooseModule } from '@nestjs/mongoose';
4
+ import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
5
+ import { APP_FILTER, APP_GUARD } from '@nestjs/core';
6
+ import * as Joi from 'joi';
7
+ import { AppController } from './app.controller';
8
+ import { AppService } from './app.service';
9
+ import { GlobalExceptionFilter } from './common/filters/global-exception.filter';
10
+ import { AuthModule } from './modules/auth/auth.module';
11
+ import { MailModule } from './modules/mail/mail.module';
12
+ import { UsersModule } from './modules/users/users.module';
13
+ import { WorkspacesModule } from './modules/workspaces/workspaces.module';
14
+
15
+ @Module({
16
+ imports: [
17
+ ConfigModule.forRoot({
18
+ isGlobal: true,
19
+ envFilePath: '.env',
20
+ validationSchema: Joi.object({
21
+ NODE_ENV: Joi.string()
22
+ .valid('development', 'production', 'test')
23
+ .default('development'),
24
+ MONGODB_URI: Joi.string().required(),
25
+ JWT_ACCESS_SECRET: Joi.string().required(),
26
+ JWT_REFRESH_SECRET: Joi.string().required(),
27
+ RESEND_API_KEY: Joi.string().required(),
28
+ RESEND_FROM_EMAIL: Joi.string().email().required(),
29
+ APP_URL: Joi.string().uri().required(),
30
+ COOKIE_SECRET: Joi.string().optional().default(''),
31
+ }),
32
+ }),
33
+ ThrottlerModule.forRootAsync({
34
+ imports: [ConfigModule],
35
+ inject: [ConfigService],
36
+ useFactory: (config: ConfigService) => ({
37
+ throttlers: [{
38
+ ttl: config.get<number>('THROTTLE_TTL', 60) * 1000,
39
+ limit: config.get<number>('THROTTLE_LIMIT', 100),
40
+ }],
41
+ }),
42
+ }),
43
+ MongooseModule.forRootAsync({
44
+ imports: [ConfigModule],
45
+ inject: [ConfigService],
46
+ useFactory: (config: ConfigService) => ({
47
+ uri: config.getOrThrow<string>('MONGODB_URI'),
48
+ }),
49
+ }),
50
+ MailModule,
51
+ UsersModule,
52
+ WorkspacesModule,
53
+ AuthModule,
54
+ ],
55
+ controllers: [AppController],
56
+ providers: [
57
+ AppService,
58
+ {
59
+ provide: APP_FILTER,
60
+ useClass: GlobalExceptionFilter,
61
+ },
62
+ {
63
+ provide: APP_GUARD,
64
+ useClass: ThrottlerGuard,
65
+ },
66
+ ],
67
+ })
68
+ export class AppModule {}
@@ -1,45 +1,27 @@
1
- import {
2
- Injectable,
3
- NestInterceptor,
4
- ExecutionContext,
5
- CallHandler,
6
- BadRequestException,
7
- } from '@nestjs/common';
8
- import { Observable } from 'rxjs';
9
- import { Request } from 'express';
10
-
11
- export interface TenantRequest extends Request {
12
- workspaceId?: string;
13
- user?: Record<string, unknown>;
14
- }
15
-
16
- @Injectable()
17
- export class WorkspaceTenantInterceptor implements NestInterceptor {
18
- intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
19
- const request = context.switchToHttp().getRequest<TenantRequest>();
20
-
21
- const headerWorkspaceId = request.headers['x-workspace-id'];
22
- if (headerWorkspaceId && typeof headerWorkspaceId === 'string') {
23
- request.workspaceId = headerWorkspaceId;
24
- return next.handle();
25
- }
26
-
27
- const workspaceIdFromUser =
28
- typeof request.user?.workspaceId === 'string'
29
- ? request.user.workspaceId
30
- : undefined;
31
- if (workspaceIdFromUser) {
32
- request.workspaceId = workspaceIdFromUser;
33
- return next.handle();
34
- }
35
-
36
- const apiPrefix = process.env.API_PREFIX ?? 'api';
37
- if (request.path.startsWith(`/${apiPrefix}/auth/`)) {
38
- return next.handle();
39
- }
40
-
41
- throw new BadRequestException(
42
- 'Se requiere workspaceId en header x-workspace-id o token JWT',
43
- );
44
- }
45
- }
1
+ import {
2
+ Injectable,
3
+ NestInterceptor,
4
+ ExecutionContext,
5
+ CallHandler,
6
+ } from '@nestjs/common';
7
+ import { Observable } from 'rxjs';
8
+ import { Request } from 'express';
9
+
10
+ export interface TenantRequest extends Request {
11
+ workspaceId?: string;
12
+ user?: Record<string, unknown>;
13
+ }
14
+
15
+ @Injectable()
16
+ export class WorkspaceTenantInterceptor implements NestInterceptor {
17
+ intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
18
+ const request = context.switchToHttp().getRequest<TenantRequest>();
19
+ const apiPrefix = process.env.API_PREFIX ?? 'api';
20
+
21
+ if (request.path.startsWith(`/${apiPrefix}/auth/`)) {
22
+ return next.handle();
23
+ }
24
+
25
+ return next.handle();
26
+ }
27
+ }
@@ -1,51 +1,50 @@
1
- import { NestFactory } from '@nestjs/core';
2
- import { ValidationPipe } from '@nestjs/common';
3
- import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
4
- import cookieParser from 'cookie-parser';
5
- import { AppModule } from './app.module';
6
- import { WorkspaceTenantInterceptor } from './common/interceptors/workspace-tenant.interceptor';
7
-
8
- async function bootstrap() {
9
- const app = await NestFactory.create(AppModule);
10
-
11
- const apiPrefix = process.env.API_PREFIX ?? 'api';
12
- app.setGlobalPrefix(apiPrefix);
13
-
14
- app.use(cookieParser(process.env.COOKIE_SECRET));
15
-
16
- app.enableCors({
17
- origin: process.env.CORS_ORIGINS?.split(',') ?? ['http://localhost:5173'],
18
- credentials: true,
19
- });
20
-
21
- app.useGlobalPipes(
22
- new ValidationPipe({
23
- whitelist: true,
24
- forbidNonWhitelisted: true,
25
- transform: true,
26
- transformOptions: {
27
- enableImplicitConversion: true,
28
- },
29
- }),
30
- );
31
-
32
- app.useGlobalInterceptors(new WorkspaceTenantInterceptor());
33
-
34
- if (process.env.SWAGGER_ENABLED === 'true') {
35
- const config = new DocumentBuilder()
36
- .setTitle('create-saas-ar API')
37
- .setDescription('Backend SaaS multi-tenant')
38
- .setVersion('1.0')
39
- .addCookieAuth('access_token')
40
- .build();
41
-
42
- const document = SwaggerModule.createDocument(app, config);
43
- SwaggerModule.setup(`${apiPrefix}/docs`, app, document);
44
- }
45
-
46
- const port = process.env.PORT ?? 3000;
47
- await app.listen(port);
48
- console.log(`🚀 Servidor corriendo en http://localhost:${port}/${apiPrefix}`);
49
- }
50
-
51
- void bootstrap();
1
+ import { NestFactory } from '@nestjs/core';
2
+ import { ValidationPipe } from '@nestjs/common';
3
+ import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
4
+ import cookieParser from 'cookie-parser';
5
+ import helmet from 'helmet';
6
+ import { AppModule } from './app.module';
7
+
8
+ async function bootstrap() {
9
+ const app = await NestFactory.create(AppModule);
10
+
11
+ const apiPrefix = process.env.API_PREFIX ?? 'api';
12
+ app.setGlobalPrefix(apiPrefix);
13
+
14
+ app.use(helmet());
15
+ app.use(cookieParser(process.env.COOKIE_SECRET));
16
+
17
+ app.enableCors({
18
+ origin: process.env.CORS_ORIGINS?.split(',') ?? ['http://localhost:5173'],
19
+ credentials: true,
20
+ });
21
+
22
+ app.useGlobalPipes(
23
+ new ValidationPipe({
24
+ whitelist: true,
25
+ forbidNonWhitelisted: true,
26
+ transform: true,
27
+ transformOptions: {
28
+ enableImplicitConversion: true,
29
+ },
30
+ }),
31
+ );
32
+
33
+ if (process.env.SWAGGER_ENABLED === 'true') {
34
+ const config = new DocumentBuilder()
35
+ .setTitle('ar-saas API')
36
+ .setDescription('Backend SaaS multi-tenant')
37
+ .setVersion('1.0')
38
+ .addCookieAuth('access_token')
39
+ .build();
40
+
41
+ const document = SwaggerModule.createDocument(app, config);
42
+ SwaggerModule.setup(`${apiPrefix}/docs`, app, document);
43
+ }
44
+
45
+ const port = process.env.PORT ?? 3000;
46
+ await app.listen(port);
47
+ console.log(`🚀 Servidor corriendo en http://localhost:${port}/${apiPrefix}`);
48
+ }
49
+
50
+ void bootstrap();