devsquad 1.0.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/LICENSE +21 -0
- package/README.md +117 -0
- package/bin/devsquad.js +42 -0
- package/package.json +42 -0
- package/src/init.js +119 -0
- package/templates/.claude/agents/fullcycle-software-architect.md +291 -0
- package/templates/.claude/skills/clickup/SKILL.md +75 -0
- package/templates/.claude/skills/database/SKILL.md +124 -0
- package/templates/.claude/skills/docs/SKILL.md +74 -0
- package/templates/.claude/skills/docs/templates.md +117 -0
- package/templates/.claude/skills/figma/SKILL.md +72 -0
- package/templates/.claude/skills/git/SKILL.md +92 -0
- package/templates/.claude/skills/nestjs/SKILL.md +35 -0
- package/templates/.claude/skills/nestjs/auth.md +187 -0
- package/templates/.claude/skills/nestjs/modules.md +110 -0
- package/templates/.claude/skills/nestjs/security.md +42 -0
- package/templates/.claude/skills/nestjs/setup.md +162 -0
- package/templates/.claude/skills/postman/SKILL.md +82 -0
- package/templates/.claude/skills/react/SKILL.md +53 -0
- package/templates/.claude/skills/react/setup.md +153 -0
- package/templates/.claude/skills/react-native/SKILL.md +82 -0
- package/templates/.claude/skills/react-native/setup.md +150 -0
- package/templates/.claude/skills/security/SKILL.md +80 -0
- package/templates/.claude/skills/security/owasp-full.md +66 -0
- package/templates/CLAUDE.md +65 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# NestJS — Autenticação JWT
|
|
2
|
+
|
|
3
|
+
## Prisma Schema (auth)
|
|
4
|
+
|
|
5
|
+
```prisma
|
|
6
|
+
model User {
|
|
7
|
+
id String @id @default(uuid())
|
|
8
|
+
email String @unique
|
|
9
|
+
password String
|
|
10
|
+
name String
|
|
11
|
+
role Role @default(USER)
|
|
12
|
+
hashedRefreshToken String? // para refresh token rotation
|
|
13
|
+
createdAt DateTime @default(now())
|
|
14
|
+
updatedAt DateTime @updatedAt
|
|
15
|
+
deletedAt DateTime?
|
|
16
|
+
|
|
17
|
+
@@map("users")
|
|
18
|
+
@@index([email])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
enum Role { ADMIN USER }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> 📖 UUID evita enumeração de IDs — OWASP A01. `hashedRefreshToken` armazenado como hash (bcrypt) para invalidação segura.
|
|
25
|
+
|
|
26
|
+
## JWT Strategy
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// strategies/jwt.strategy.ts
|
|
30
|
+
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
31
|
+
import { PassportStrategy } from '@nestjs/passport';
|
|
32
|
+
import { ExtractJwt, Strategy } from 'passport-jwt';
|
|
33
|
+
import { ConfigService } from '@nestjs/config';
|
|
34
|
+
import { PrismaService } from '../../prisma/prisma.service';
|
|
35
|
+
|
|
36
|
+
@Injectable()
|
|
37
|
+
export class JwtStrategy extends PassportStrategy(Strategy) {
|
|
38
|
+
constructor(config: ConfigService, private prisma: PrismaService) {
|
|
39
|
+
super({
|
|
40
|
+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
41
|
+
secretOrKey: config.get('jwt.secret'),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async validate(payload: { sub: string; email: string; role: string }) {
|
|
46
|
+
const user = await this.prisma.user.findUnique({
|
|
47
|
+
where: { id: payload.sub, deletedAt: null },
|
|
48
|
+
});
|
|
49
|
+
if (!user) throw new UnauthorizedException();
|
|
50
|
+
const { password, hashedRefreshToken, ...result } = user;
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Auth Service — Login + Refresh Token Rotation
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// auth.service.ts
|
|
60
|
+
import { Injectable, UnauthorizedException, ForbiddenException } from '@nestjs/common';
|
|
61
|
+
import { JwtService } from '@nestjs/jwt';
|
|
62
|
+
import { ConfigService } from '@nestjs/config';
|
|
63
|
+
import { PrismaService } from '../../prisma/prisma.service';
|
|
64
|
+
import * as bcrypt from 'bcrypt';
|
|
65
|
+
|
|
66
|
+
@Injectable()
|
|
67
|
+
export class AuthService {
|
|
68
|
+
constructor(
|
|
69
|
+
private prisma: PrismaService,
|
|
70
|
+
private jwt: JwtService,
|
|
71
|
+
private config: ConfigService,
|
|
72
|
+
) {}
|
|
73
|
+
|
|
74
|
+
async login(email: string, password: string) {
|
|
75
|
+
const user = await this.prisma.user.findUnique({ where: { email } });
|
|
76
|
+
|
|
77
|
+
// 🔒 Mesma mensagem para email e senha inválidos — evita user enumeration
|
|
78
|
+
if (!user || !(await bcrypt.compare(password, user.password))) {
|
|
79
|
+
throw new UnauthorizedException('Credenciais inválidas');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const tokens = await this.generateTokens(user.id, user.email, user.role);
|
|
83
|
+
await this.updateRefreshToken(user.id, tokens.refresh_token);
|
|
84
|
+
return tokens;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async refresh(userId: string, refreshToken: string) {
|
|
88
|
+
const user = await this.prisma.user.findUnique({ where: { id: userId } });
|
|
89
|
+
if (!user?.hashedRefreshToken) throw new ForbiddenException();
|
|
90
|
+
|
|
91
|
+
const isValid = await bcrypt.compare(refreshToken, user.hashedRefreshToken);
|
|
92
|
+
if (!isValid) throw new ForbiddenException();
|
|
93
|
+
|
|
94
|
+
// 🔒 Rotation — invalida o token anterior a cada uso
|
|
95
|
+
const tokens = await this.generateTokens(user.id, user.email, user.role);
|
|
96
|
+
await this.updateRefreshToken(user.id, tokens.refresh_token);
|
|
97
|
+
return tokens;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async logout(userId: string) {
|
|
101
|
+
await this.prisma.user.update({
|
|
102
|
+
where: { id: userId },
|
|
103
|
+
data: { hashedRefreshToken: null },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async generateTokens(userId: string, email: string, role: string) {
|
|
108
|
+
const payload = { sub: userId, email, role };
|
|
109
|
+
const [access_token, refresh_token] = await Promise.all([
|
|
110
|
+
this.jwt.signAsync(payload, {
|
|
111
|
+
secret: this.config.get('jwt.secret'),
|
|
112
|
+
expiresIn: '15m',
|
|
113
|
+
}),
|
|
114
|
+
this.jwt.signAsync(payload, {
|
|
115
|
+
secret: this.config.get('jwt.refreshSecret'),
|
|
116
|
+
expiresIn: '7d',
|
|
117
|
+
}),
|
|
118
|
+
]);
|
|
119
|
+
return { access_token, refresh_token };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async updateRefreshToken(userId: string, refreshToken: string) {
|
|
123
|
+
const hashed = await bcrypt.hash(refreshToken, 12);
|
|
124
|
+
await this.prisma.user.update({
|
|
125
|
+
where: { id: userId },
|
|
126
|
+
data: { hashedRefreshToken: hashed },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Guards e RBAC
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// guards/jwt-auth.guard.ts
|
|
136
|
+
import { Injectable } from '@nestjs/common';
|
|
137
|
+
import { AuthGuard } from '@nestjs/passport';
|
|
138
|
+
@Injectable()
|
|
139
|
+
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
|
140
|
+
|
|
141
|
+
// decorators/roles.decorator.ts
|
|
142
|
+
import { SetMetadata } from '@nestjs/common';
|
|
143
|
+
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
|
|
144
|
+
|
|
145
|
+
// guards/roles.guard.ts
|
|
146
|
+
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
|
147
|
+
import { Reflector } from '@nestjs/core';
|
|
148
|
+
@Injectable()
|
|
149
|
+
export class RolesGuard implements CanActivate {
|
|
150
|
+
constructor(private reflector: Reflector) {}
|
|
151
|
+
canActivate(ctx: ExecutionContext): boolean {
|
|
152
|
+
const roles = this.reflector.get<string[]>('roles', ctx.getHandler());
|
|
153
|
+
if (!roles) return true;
|
|
154
|
+
return roles.includes(ctx.switchToHttp().getRequest().user?.role);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// decorators/current-user.decorator.ts
|
|
159
|
+
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
|
160
|
+
export const CurrentUser = createParamDecorator(
|
|
161
|
+
(_: unknown, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().user,
|
|
162
|
+
);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Uso no Controller
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
@Get('profile')
|
|
169
|
+
@UseGuards(JwtAuthGuard)
|
|
170
|
+
getProfile(@CurrentUser() user: User) { return user; }
|
|
171
|
+
|
|
172
|
+
@Get('admin')
|
|
173
|
+
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
174
|
+
@Roles('ADMIN')
|
|
175
|
+
adminOnly() { return 'só admin'; }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Checklist de Auth
|
|
179
|
+
|
|
180
|
+
- [ ] bcrypt com salt 12 no hash de senhas
|
|
181
|
+
- [ ] Mesma mensagem de erro para email/senha inválidos
|
|
182
|
+
- [ ] Access token 15min + Refresh token 7d
|
|
183
|
+
- [ ] Refresh token salvo como hash (bcrypt) no banco
|
|
184
|
+
- [ ] Rotation implementada — token anterior invalidado a cada `/refresh`
|
|
185
|
+
- [ ] Rate limiting no `/auth/login` — `@Throttle({ default: { ttl: 900000, limit: 5 } })`
|
|
186
|
+
- [ ] `JwtAuthGuard` e `RolesGuard` criados e testados
|
|
187
|
+
- [ ] `CurrentUser` decorator funcionando
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# NestJS — Padrão de módulo (DDD leve)
|
|
2
|
+
|
|
3
|
+
Use este padrão para cada feature em `src/modules/<nome>/`.
|
|
4
|
+
|
|
5
|
+
## Estrutura de pastas
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
src/modules/users/
|
|
9
|
+
├── users.module.ts
|
|
10
|
+
├── users.controller.ts
|
|
11
|
+
├── users.service.ts
|
|
12
|
+
├── dto/
|
|
13
|
+
│ ├── create-user.dto.ts
|
|
14
|
+
│ └── update-user.dto.ts
|
|
15
|
+
└── entities/
|
|
16
|
+
└── user.entity.ts ← opcional: classe pura ou type do Prisma
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## users.module.ts
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Module } from '@nestjs/common';
|
|
23
|
+
import { UsersService } from './users.service';
|
|
24
|
+
import { UsersController } from './users.controller';
|
|
25
|
+
import { PrismaModule } from '../../prisma/prisma.module';
|
|
26
|
+
|
|
27
|
+
@Module({
|
|
28
|
+
imports: [PrismaModule],
|
|
29
|
+
controllers: [UsersController],
|
|
30
|
+
providers: [UsersService],
|
|
31
|
+
exports: [UsersService],
|
|
32
|
+
})
|
|
33
|
+
export class UsersModule {}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## DTOs (A03 — validação)
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// create-user.dto.ts
|
|
40
|
+
import { IsEmail, IsString, MinLength, MaxLength } from 'class-validator';
|
|
41
|
+
|
|
42
|
+
export class CreateUserDto {
|
|
43
|
+
@IsEmail()
|
|
44
|
+
email: string;
|
|
45
|
+
|
|
46
|
+
@IsString()
|
|
47
|
+
@MinLength(8)
|
|
48
|
+
@MaxLength(128)
|
|
49
|
+
password: string;
|
|
50
|
+
|
|
51
|
+
@IsString()
|
|
52
|
+
@MaxLength(100)
|
|
53
|
+
name: string;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Service (Prisma)
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
61
|
+
import { PrismaService } from '../../prisma/prisma.service';
|
|
62
|
+
import { CreateUserDto } from './dto/create-user.dto';
|
|
63
|
+
|
|
64
|
+
@Injectable()
|
|
65
|
+
export class UsersService {
|
|
66
|
+
constructor(private prisma: PrismaService) {}
|
|
67
|
+
|
|
68
|
+
async create(dto: CreateUserDto) {
|
|
69
|
+
// hash de senha no service ou em subcamada dedicada — ver auth.md
|
|
70
|
+
return this.prisma.user.create({ data: { /* mapear dto */ } });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async findOne(id: string) {
|
|
74
|
+
const user = await this.prisma.user.findUnique({ where: { id } });
|
|
75
|
+
if (!user) throw new NotFoundException();
|
|
76
|
+
return user;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Controller
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { Controller, Get, Post, Body, Param, UseGuards } from '@nestjs/common';
|
|
85
|
+
import { UsersService } from './users.service';
|
|
86
|
+
import { CreateUserDto } from './dto/create-user.dto';
|
|
87
|
+
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
|
88
|
+
|
|
89
|
+
@Controller('users')
|
|
90
|
+
export class UsersController {
|
|
91
|
+
constructor(private readonly usersService: UsersService) {}
|
|
92
|
+
|
|
93
|
+
@Post()
|
|
94
|
+
create(@Body() dto: CreateUserDto) {
|
|
95
|
+
return this.usersService.create(dto);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Get(':id')
|
|
99
|
+
@UseGuards(JwtAuthGuard)
|
|
100
|
+
findOne(@Param('id') id: string) {
|
|
101
|
+
return this.usersService.findOne(id);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Regras
|
|
107
|
+
|
|
108
|
+
- **Um módulo = uma bounded context** clara (users, orders, billing).
|
|
109
|
+
- **Nunca** exponha entidades Prisma brutas se houver campos sensíveis — use resposta DTO ou `select`.
|
|
110
|
+
- Rotas protegidas: `@UseGuards(JwtAuthGuard)` + ownership quando o recurso for do usuário (ver skill **security**).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# NestJS — Segurança (OWASP no backend)
|
|
2
|
+
|
|
3
|
+
Checklist e pontos **específicos de NestJS**. Para o checklist completo e exemplos genéricos, use a skill **`/devsquad security`** (`security/SKILL.md`).
|
|
4
|
+
|
|
5
|
+
## Já coberto em setup.md
|
|
6
|
+
|
|
7
|
+
- `helmet()`, `ValidationPipe` com `whitelist` + `forbidNonWhitelisted`
|
|
8
|
+
- CORS explícito, `ThrottlerGuard` global
|
|
9
|
+
- Swagger em `/api/docs` com Bearer
|
|
10
|
+
|
|
11
|
+
## Antes de ir para produção
|
|
12
|
+
|
|
13
|
+
| Item | Ação |
|
|
14
|
+
|------|------|
|
|
15
|
+
| **A02** | `JWT_SECRET` / `JWT_REFRESH_SECRET` fortes, nunca no código |
|
|
16
|
+
| **A04** | Login: throttle mais agressivo (ex.: guard dedicado na rota `auth/login`) |
|
|
17
|
+
| **A05** | HTTPS atrás de proxy; confiar em `X-Forwarded-*` só se configurado |
|
|
18
|
+
| **A07** | Access token curto + refresh com rotation (ver `auth.md`) |
|
|
19
|
+
| **A09** | Logger sem email, senha, token ou body completo de erro para cliente |
|
|
20
|
+
|
|
21
|
+
## Snippets úteis
|
|
22
|
+
|
|
23
|
+
### Rate limit na rota de login
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { Throttle } from '@nestjs/throttler';
|
|
27
|
+
|
|
28
|
+
@Throttle({ default: { limit: 5, ttl: 900000 } }) // 5 / 15 min
|
|
29
|
+
@Post('login')
|
|
30
|
+
login(@Body() dto: LoginDto) { /* ... */ }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Não vazar stack trace
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// Em produção: filtre detalhes em ExceptionFilter global
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Referência cruzada
|
|
40
|
+
|
|
41
|
+
- **auth.md** — JWT, guards, refresh
|
|
42
|
+
- **security (skill)** — OWASP Top 10 completo para stack Nest + React
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# NestJS — Setup Completo
|
|
2
|
+
|
|
3
|
+
## src/main.ts
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { NestFactory } from '@nestjs/core';
|
|
7
|
+
import { ValidationPipe, Logger } from '@nestjs/common';
|
|
8
|
+
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
|
9
|
+
import { AppModule } from './app.module';
|
|
10
|
+
import helmet from 'helmet';
|
|
11
|
+
|
|
12
|
+
async function bootstrap() {
|
|
13
|
+
const app = await NestFactory.create(AppModule);
|
|
14
|
+
const logger = new Logger('Bootstrap');
|
|
15
|
+
|
|
16
|
+
// 🔒 OWASP A05 — Headers de segurança HTTP
|
|
17
|
+
app.use(helmet());
|
|
18
|
+
|
|
19
|
+
// 🔒 OWASP A03 — Rejeita campos não declarados nos DTOs
|
|
20
|
+
app.useGlobalPipes(new ValidationPipe({
|
|
21
|
+
whitelist: true,
|
|
22
|
+
forbidNonWhitelisted: true,
|
|
23
|
+
transform: true,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
// CORS explícito — nunca use '*' em produção
|
|
27
|
+
app.enableCors({ origin: process.env.FRONTEND_URL, credentials: true });
|
|
28
|
+
|
|
29
|
+
app.setGlobalPrefix('api/v1');
|
|
30
|
+
|
|
31
|
+
// Swagger
|
|
32
|
+
const config = new DocumentBuilder()
|
|
33
|
+
.setTitle(process.env.APP_NAME ?? 'API')
|
|
34
|
+
.setVersion('1.0')
|
|
35
|
+
.addBearerAuth()
|
|
36
|
+
.build();
|
|
37
|
+
SwaggerModule.setup('api/docs', app, SwaggerModule.createDocument(app, config));
|
|
38
|
+
|
|
39
|
+
const port = process.env.PORT ?? 3000;
|
|
40
|
+
await app.listen(port);
|
|
41
|
+
logger.log(`🚀 http://localhost:${port}`);
|
|
42
|
+
logger.log(`📚 Swagger: http://localhost:${port}/api/docs`);
|
|
43
|
+
}
|
|
44
|
+
bootstrap();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## src/app.module.ts
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { Module } from '@nestjs/common';
|
|
51
|
+
import { ConfigModule } from '@nestjs/config';
|
|
52
|
+
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
|
53
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
54
|
+
import { PrismaModule } from './prisma/prisma.module';
|
|
55
|
+
import { AuthModule } from './modules/auth/auth.module';
|
|
56
|
+
import { UsersModule } from './modules/users/users.module';
|
|
57
|
+
import configuration from './config/configuration';
|
|
58
|
+
|
|
59
|
+
@Module({
|
|
60
|
+
imports: [
|
|
61
|
+
ConfigModule.forRoot({ isGlobal: true, load: [configuration] }),
|
|
62
|
+
ThrottlerModule.forRoot([{ ttl: 60000, limit: 60 }]),
|
|
63
|
+
PrismaModule,
|
|
64
|
+
AuthModule,
|
|
65
|
+
UsersModule,
|
|
66
|
+
],
|
|
67
|
+
providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }],
|
|
68
|
+
})
|
|
69
|
+
export class AppModule {}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## src/config/configuration.ts
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
export default () => ({
|
|
76
|
+
port: parseInt(process.env.PORT ?? '3000', 10),
|
|
77
|
+
jwt: {
|
|
78
|
+
secret: process.env.JWT_SECRET,
|
|
79
|
+
refreshSecret: process.env.JWT_REFRESH_SECRET,
|
|
80
|
+
expiresIn: '15m',
|
|
81
|
+
refreshExpiresIn: '7d',
|
|
82
|
+
},
|
|
83
|
+
frontendUrl: process.env.FRONTEND_URL,
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## src/prisma/prisma.service.ts
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
|
91
|
+
import { PrismaClient } from '@prisma/client';
|
|
92
|
+
|
|
93
|
+
@Injectable()
|
|
94
|
+
export class PrismaService extends PrismaClient
|
|
95
|
+
implements OnModuleInit, OnModuleDestroy {
|
|
96
|
+
async onModuleInit() { await this.$connect(); }
|
|
97
|
+
async onModuleDestroy() { await this.$disconnect(); }
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// prisma/prisma.module.ts
|
|
103
|
+
import { Global, Module } from '@nestjs/common';
|
|
104
|
+
import { PrismaService } from './prisma.service';
|
|
105
|
+
|
|
106
|
+
@Global()
|
|
107
|
+
@Module({ providers: [PrismaService], exports: [PrismaService] })
|
|
108
|
+
export class PrismaModule {}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## .env.example
|
|
112
|
+
|
|
113
|
+
```env
|
|
114
|
+
APP_NAME="Nome do Projeto"
|
|
115
|
+
PORT=3000
|
|
116
|
+
NODE_ENV=development
|
|
117
|
+
FRONTEND_URL="http://localhost:5173"
|
|
118
|
+
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/nome_db?schema=public"
|
|
119
|
+
JWT_SECRET="gere-com-openssl-rand-base64-32"
|
|
120
|
+
JWT_REFRESH_SECRET="outro-segredo-diferente"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Estrutura de pastas (DDD)
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
src/
|
|
127
|
+
├── modules/
|
|
128
|
+
│ ├── auth/
|
|
129
|
+
│ │ ├── dto/ login.dto.ts, register.dto.ts
|
|
130
|
+
│ │ ├── guards/ jwt-auth.guard.ts
|
|
131
|
+
│ │ ├── strategies/ jwt.strategy.ts
|
|
132
|
+
│ │ ├── decorators/ current-user.decorator.ts
|
|
133
|
+
│ │ ├── auth.controller.ts
|
|
134
|
+
│ │ ├── auth.module.ts
|
|
135
|
+
│ │ └── auth.service.ts
|
|
136
|
+
│ └── users/
|
|
137
|
+
│ ├── dto/ create-user.dto.ts, update-user.dto.ts
|
|
138
|
+
│ ├── entities/ user.entity.ts
|
|
139
|
+
│ ├── users.controller.ts
|
|
140
|
+
│ ├── users.module.ts
|
|
141
|
+
│ └── users.service.ts
|
|
142
|
+
├── common/
|
|
143
|
+
│ ├── decorators/ roles.decorator.ts
|
|
144
|
+
│ ├── guards/ roles.guard.ts
|
|
145
|
+
│ └── filters/ http-exception.filter.ts
|
|
146
|
+
├── config/
|
|
147
|
+
│ └── configuration.ts
|
|
148
|
+
├── prisma/
|
|
149
|
+
│ ├── prisma.module.ts
|
|
150
|
+
│ └── prisma.service.ts
|
|
151
|
+
└── main.ts
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Checklist de setup
|
|
155
|
+
|
|
156
|
+
- [ ] NestJS inicializado via CLI
|
|
157
|
+
- [ ] `helmet` + `ValidationPipe` global + CORS configurados no `main.ts`
|
|
158
|
+
- [ ] `PrismaModule` com `@Global()` criado
|
|
159
|
+
- [ ] `ThrottlerModule` configurado no `AppModule`
|
|
160
|
+
- [ ] `.env.example` criado e `.env` no `.gitignore`
|
|
161
|
+
- [ ] Swagger funcionando em `/api/docs`
|
|
162
|
+
- [ ] Prefixo global `api/v1` definido
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: postman-collections
|
|
3
|
+
description: Criação e organização de Postman Collections profissionais para documentar e testar APIs. Use quando o desenvolvedor precisar documentar endpoints, criar environments, escrever scripts de teste ou exportar a collection para o repositório.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Postman Collections
|
|
7
|
+
|
|
8
|
+
## Estrutura da collection
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
📁 [Nome do Projeto] API
|
|
12
|
+
├── 📁 Auth
|
|
13
|
+
│ ├── POST /auth/register
|
|
14
|
+
│ ├── POST /auth/login ← script de captura automática de token
|
|
15
|
+
│ ├── POST /auth/refresh
|
|
16
|
+
│ └── POST /auth/logout
|
|
17
|
+
└── 📁 [Módulo por módulo]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Environments (nunca hardcode URLs ou tokens)
|
|
21
|
+
|
|
22
|
+
**Development**
|
|
23
|
+
```json
|
|
24
|
+
{ "base_url": "http://localhost:3000/api/v1", "access_token": "", "refresh_token": "" }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Production**
|
|
28
|
+
```json
|
|
29
|
+
{ "base_url": "https://api.seudominio.com/api/v1", "access_token": "", "refresh_token": "" }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Uso nas requisições: `{{base_url}}/auth/login` | `Bearer {{access_token}}`
|
|
33
|
+
|
|
34
|
+
## Script de captura automática de token (aba Tests do login)
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
const { access_token, refresh_token } = pm.response.json();
|
|
38
|
+
if (access_token) {
|
|
39
|
+
pm.environment.set('access_token', access_token);
|
|
40
|
+
pm.environment.set('refresh_token', refresh_token);
|
|
41
|
+
}
|
|
42
|
+
pm.test('Status 200', () => pm.response.to.have.status(200));
|
|
43
|
+
pm.test('Retorna tokens', () => {
|
|
44
|
+
pm.expect(pm.response.json()).to.have.property('access_token');
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Testes padrão por endpoint
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
pm.test('Status correto', () => pm.response.to.have.status(201));
|
|
52
|
+
pm.test('Estrutura correta', () => {
|
|
53
|
+
const json = pm.response.json();
|
|
54
|
+
pm.expect(json).to.have.property('id');
|
|
55
|
+
pm.expect(json).not.to.have.property('password'); // 🔒
|
|
56
|
+
});
|
|
57
|
+
pm.test('Resposta < 500ms', () => pm.expect(pm.response.responseTime).to.be.below(500));
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Versionamento no repositório
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
postman/
|
|
64
|
+
├── collection.json ← Exportar: ··· > Export > Collection v2.1
|
|
65
|
+
├── environment.dev.json ← Sem valores reais
|
|
66
|
+
└── environment.prod.json ← Sem valores reais
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git commit -m "docs(postman): adicionar endpoints do módulo de auth"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> 🔒 Nunca commite tokens reais nos arquivos de environment.
|
|
74
|
+
|
|
75
|
+
## Checklist
|
|
76
|
+
|
|
77
|
+
- [ ] Collection com o nome do projeto
|
|
78
|
+
- [ ] Environments dev e prod configurados com variáveis
|
|
79
|
+
- [ ] Todas as URLs usando `{{base_url}}`
|
|
80
|
+
- [ ] Script de auto-captura de token no login
|
|
81
|
+
- [ ] Testes básicos (status + estrutura) em cada endpoint
|
|
82
|
+
- [ ] Collection exportada em `postman/collection.json`
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-frontend
|
|
3
|
+
description: Setup e arquitetura de frontend com React + TypeScript + TailwindCSS + TanStack Query. Use quando o desenvolvedor precisar inicializar o frontend web, configurar autenticação no cliente, criar componentes, hooks, services ou rotas protegidas.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# React Frontend
|
|
7
|
+
|
|
8
|
+
Stack: React + TypeScript + Vite + TailwindCSS + TanStack Query + Axios + React Router + React Hook Form + Zod
|
|
9
|
+
|
|
10
|
+
## Setup rápido
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm create vite@latest nome-do-projeto -- --template react-ts
|
|
14
|
+
cd nome-do-projeto
|
|
15
|
+
npm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p
|
|
16
|
+
npm install axios @tanstack/react-query react-router-dom react-hook-form zod @hookform/resolvers lucide-react
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
// tailwind.config.js
|
|
21
|
+
export default {
|
|
22
|
+
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
|
23
|
+
theme: { extend: {} },
|
|
24
|
+
plugins: [],
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Estrutura de pastas
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
src/
|
|
32
|
+
├── components/ui/ ← Button, Input, Card reutilizáveis
|
|
33
|
+
├── components/layout/ ← Header, Sidebar, PrivateLayout
|
|
34
|
+
├── pages/ ← Uma pasta por rota
|
|
35
|
+
├── hooks/ ← Custom hooks
|
|
36
|
+
├── services/ ← api.ts + *.service.ts (nunca axios direto no componente)
|
|
37
|
+
├── contexts/ ← AuthContext.tsx
|
|
38
|
+
├── routes/ ← AppRoutes.tsx + PrivateRoute.tsx
|
|
39
|
+
├── types/ ← interfaces TypeScript
|
|
40
|
+
└── utils/ ← funções puras
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## .env.example
|
|
44
|
+
|
|
45
|
+
```env
|
|
46
|
+
VITE_API_URL=http://localhost:3000/api/v1
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Arquivos detalhados desta skill
|
|
50
|
+
|
|
51
|
+
- **setup.md** — AuthContext, Axios com interceptors, PrivateRoute, App.tsx prontos
|
|
52
|
+
|
|
53
|
+
Leia `setup.md` para implementar a estrutura base completa.
|