ar-saas 0.3.2 → 0.4.0

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 (28) 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/package.json +5 -2
  5. package/templates/backend/src/app.module.ts +68 -40
  6. package/templates/backend/src/common/interceptors/workspace-tenant.interceptor.ts +27 -45
  7. package/templates/backend/src/main.ts +50 -51
  8. package/templates/backend/src/modules/auth/auth.controller.ts +162 -158
  9. package/templates/backend/src/modules/auth/auth.service.ts +236 -257
  10. package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +45 -43
  11. package/templates/backend/src/modules/users/users.controller.ts +28 -0
  12. package/templates/backend/src/modules/users/users.module.ts +16 -14
  13. package/templates/backend/src/modules/users/users.repository.ts +57 -51
  14. package/templates/backend/src/modules/users/users.service.ts +130 -104
  15. package/templates/backend/src/modules/workspaces/workspaces.repository.ts +38 -34
  16. package/templates/backend/src/modules/workspaces/workspaces.service.ts +51 -42
  17. package/templates/frontend/package.json +2 -5
  18. package/templates/frontend/pnpm-workspace.yaml +2 -2
  19. package/templates/frontend/src/app/(auth)/layout.tsx +29 -28
  20. package/templates/frontend/src/app/(dashboard)/billing/page.tsx +111 -111
  21. package/templates/frontend/src/app/(dashboard)/profile/page.tsx +241 -226
  22. package/templates/frontend/src/app/(dashboard)/settings/page.tsx +155 -156
  23. package/templates/frontend/src/app/(dashboard)/team/page.tsx +179 -178
  24. package/templates/frontend/src/app/layout.tsx +29 -26
  25. package/templates/frontend/src/app/page.tsx +1 -1
  26. package/templates/frontend/src/app/setup/page.tsx +1 -1
  27. package/templates/frontend/src/components/dashboard/header.tsx +5 -3
  28. 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.2",
3
+ "version": "0.4.0",
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,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();