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.
- package/dist/generator.js +2 -6
- package/package.json +1 -1
- package/templates/backend/.env.example +1 -1
- package/templates/backend/package.json +5 -2
- package/templates/backend/src/app.module.ts +68 -40
- package/templates/backend/src/common/interceptors/workspace-tenant.interceptor.ts +27 -45
- package/templates/backend/src/main.ts +50 -51
- package/templates/backend/src/modules/auth/auth.controller.ts +162 -158
- package/templates/backend/src/modules/auth/auth.service.ts +236 -257
- package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +45 -43
- package/templates/backend/src/modules/users/users.controller.ts +28 -0
- package/templates/backend/src/modules/users/users.module.ts +16 -14
- package/templates/backend/src/modules/users/users.repository.ts +57 -51
- package/templates/backend/src/modules/users/users.service.ts +130 -104
- package/templates/backend/src/modules/workspaces/workspaces.repository.ts +38 -34
- package/templates/backend/src/modules/workspaces/workspaces.service.ts +51 -42
- package/templates/frontend/package.json +2 -5
- package/templates/frontend/pnpm-workspace.yaml +2 -2
- package/templates/frontend/src/app/(auth)/layout.tsx +29 -28
- package/templates/frontend/src/app/(dashboard)/billing/page.tsx +111 -111
- package/templates/frontend/src/app/(dashboard)/profile/page.tsx +241 -226
- package/templates/frontend/src/app/(dashboard)/settings/page.tsx +155 -156
- package/templates/frontend/src/app/(dashboard)/team/page.tsx +179 -178
- package/templates/frontend/src/app/layout.tsx +29 -26
- package/templates/frontend/src/app/page.tsx +1 -1
- package/templates/frontend/src/app/setup/page.tsx +1 -1
- package/templates/frontend/src/components/dashboard/header.tsx +5 -3
- 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 `
|
|
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
|
|
214
|
+
healthcheckPath = "/api"
|
|
219
215
|
healthcheckTimeout = 30
|
|
220
216
|
restartPolicyType = "on_failure"
|
|
221
217
|
`;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "ar-saas-backend",
|
|
3
3
|
"version": "0.0.1",
|
|
4
|
-
"description": "Backend SaaS multi-tenant generado con
|
|
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 {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
7
|
-
} from '
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
6
|
-
import {
|
|
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(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
.
|
|
39
|
-
.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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();
|