Corelab Challenge - Backend NodeJS + Nest + Prisma + Redis + Swagger#82
Corelab Challenge - Backend NodeJS + Nest + Prisma + Redis + Swagger#82odavibatista wants to merge 23 commits into
Conversation
…project's purpose on README.md
…er for API endpoint's docs, added redis configuration and prisma's first schemas
…ts on its controller and repository
WalkthroughThis PR replaces an AdonisJS setup with a NestJS-based API. It removes Adonis files and configurations, adds Nest app bootstrap, modules for users and notes (controllers, use cases, repositories, DTOs), shared configs (env, redis, swagger), middleware for JWT auth, caching via Redis, Prisma-based persistence, tests, Docker/Compose, and tooling configs. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant C as Client
participant A as API (Nest)
participant M as Auth Middleware
participant UC as Usecase
participant R as Repository (Prisma)
participant Cache as Cache (Keyv/Redis)
rect rgba(200,220,255,0.2)
note over C,A: Authenticated request
C->>A: HTTP request with Authorization: Bearer <token>
A->>M: Run AuthenticationMiddleware
M->>M: Verify JWT (APP_SECRET)
M-->>A: Attach req.user, next()
end
alt Read endpoint (cached)
A->>Cache: Get notes-{userId} / note-{id}
Cache-->>A: Hit
A-->>C: 200 OK (cached payload)
else Read endpoint (miss)
A->>UC: execute(...)
UC->>R: Query Prisma
R-->>UC: Data
UC-->>A: DTO
A->>Cache: Set cache key
A-->>C: 200 OK
end
rect rgba(220,255,220,0.2)
note over A,UC: Write endpoint
C->>A: Create/Star/Edit/Delete
A->>UC: execute(...)
UC->>R: Mutate via Prisma
R-->>UC: Result
UC-->>A: DTO/boolean
A->>Cache: Invalidate/Refresh per-note and per-user keys
A-->>C: 200/201/204
end
sequenceDiagram
autonumber
participant C as Client
participant A as API (Nest)
participant UUC as User Usecases
participant UR as UserRepository
participant H as HashProvider
participant JWT as JWTProvider
rect rgba(255,240,200,0.25)
note over C,A: Registration
C->>A: POST /user/register
A->>UUC: CreateUserUseCase.execute(data)
UUC->>UR: findByEmail
UR-->>UUC: null or user
alt Email free
UUC->>H: hash(password)
H-->>UUC: hash
UUC->>UR: create(user)
UR-->>UUC: created user
UUC->>JWT: generate({ user })
JWT-->>UUC: token
UUC-->>A: { token, id }
A-->>C: 201 Created
else Email exists
UUC-->>A: EmailAlreadyRegisteredException
A-->>C: 403 with message
end
end
rect rgba(255,220,220,0.25)
note over C,A: Login
C->>A: POST /user/login
A->>UUC: UserLoginUsecase.execute(data, ip)
UUC->>UR: findByEmail
UR-->>UUC: user
UUC->>UR: comparePassword
UR-->>UUC: true/false
alt Valid
UUC->>JWT: generate({ user })
JWT-->>UUC: token
UUC-->>A: { token, user }
A-->>C: 200 OK
else Invalid
UUC-->>A: InvalidCredentialsException
A-->>C: 400 with message
end
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 80
🧹 Nitpick comments (115)
src/shared/domain/dtos/repositories/Configuration.repository.dto.ts (3)
1-3: Type the config key (avoid magic strings) and make it readonlyPassing arbitrary strings invites runtime errors and weakens refactors. Consider a dedicated key type and readonly field.
-export interface GetConfigurationValueParamsDTO { - name: string; -} +export type ConfigurationKey = string; // tighten to a union of known keys when available +export interface GetConfigurationValueParamsDTO { + readonly name: ConfigurationKey; +}If you already have a Zod env schema, we can derive this type (or define a union in the domain to avoid cross-layer coupling). Want me to wire that up?
5-7: Generalize the value type; many config entries aren’t stringsSome values (PORT, flags) are numeric/boolean. A generic with a safe default keeps callers typed without breaking anything.
-export interface GetConfigurationValueResponseDTO { - value: string; -} +export type ConfigurationPrimitive = string | number | boolean; +export interface GetConfigurationValueResponseDTO<T extends ConfigurationPrimitive = string> { + value: T; +}
1-7: Guard against leaking secretsIf this DTO ever bubbles up to controllers, ensure secret-like keys (e.g., APP_SECRET, CRYPTO_SECRET/IV) are never serialized to API responses or logs. Confirm usage is constrained to repository/service layers.
I can add a small masking utility and an allowlist to enforce this at boundaries if you’d like.
.gitignore (2)
31-37: Consider dropping VSCode exceptions or audit them for secrets.Keeping these files tracked is fine for shared debug configs, but ensure they never contain tokens/paths specific to individuals. Otherwise, ignore the entire
.vscode/.
59-59: Ensuresrc/shared/infra/db/generatedartifacts are regenerated on install/build: ignoring this directory will break fresh clones if those files aren’t produced. Add a generation step (e.g.prisma generateinpostinstall) or switch users to a published client like@prisma/client..prettierrc (1)
1-4: Add trailing newline and consider pinning end-of-line to avoid cross-OS diffs.Prettier prefers files ending with a newline; also consider
endOfLine: "lf"if teammates use Windows.{ "singleQuote": true, - "trailingComma": "all" -} + "trailingComma": "all" +} +nest-cli.json (1)
5-8: entryFile path valid; consider adding asset configuration
src/app/main.ts exists; nocompilerOptions.assetsconfigured—add asset patterns if you serve Swagger static assets or other non-TS files.src/modules/user/domain/dtos/errors/UserNotFound.exception.ts (2)
1-6: Prefer Nest built-ins over raw HttpException.Use
NotFoundExceptionfor semantics and consistency; drop the numeric status.-import { HttpException } from '@nestjs/common'; +import { NotFoundException } from '@nestjs/common'; -export class UserNotFoundException extends HttpException { +export class UserNotFoundException extends NotFoundException { constructor() { - super('Usuário não encontrado.', 404); + super('Usuário não encontrado.'); } }
3-7: Unify error filename conventions.File is
UserNotFound.exception.tswhile class isUserNotFoundException. Consideruser-not-found.exception.tsfor consistency with other Nest projects.src/modules/note/domain/dtos/errors/ColorAlreadySetException.exception.ts (2)
1-6: UseBadRequestExceptioninstead of baseHttpException.Encodes the 400 semantics without magic numbers.
-import { HttpException } from '@nestjs/common'; +import { BadRequestException } from '@nestjs/common'; -export class ColorAlreadySetException extends HttpException { +export class ColorAlreadySetException extends BadRequestException { constructor() { - super('A nota já está com esta cor inserida.', 400); + super('A nota já está com esta cor inserida.'); } }
3-7: Fix duplicated “Exception” in filename.
ColorAlreadySetException.exception.tsrepeats the suffix. Suggestcolor-already-set.exception.ts.src/modules/note/domain/dtos/errors/NoteNotFoundException.exception.ts (2)
1-6: Prefer Nest’s NotFoundException (or HttpStatus) for clarity and consistencyUse the framework’s specific exception or the HttpStatus enum instead of a numeric literal.
-import { HttpException } from '@nestjs/common'; +import { NotFoundException } from '@nestjs/common'; -export class NoteNotFoundException extends HttpException { +export class NoteNotFoundException extends NotFoundException { constructor() { - super('Anotação não encontrada.', 404); + super('Anotação não encontrada.'); } }
3-7: Consider relocating exceptions out of “dtos”Placing exceptions under a “dtos” path is misleading. Suggest moving to domain/errors or shared/errors to align with intent.
src/shared/config/swagger.config.ts (1)
3-16: Set default security requirement so all operations require Bearer by defaultThis avoids repeating @ApiBearerAuth on every controller and ensures the Swagger UI sends the token automatically.
export const sharedSwaggerConfig = new DocumentBuilder() .setTitle('CoreNotes API') .setDescription('API for a note-taking application.') .setVersion('1.0.0') .addBearerAuth( { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', description: 'Insert a JWT token in order to authenticate.', }, 'access-token', ) + .addSecurityRequirements('access-token') .build();src/@types/express.d.ts (1)
1-9: Harden Express.Request module augmentation
Add an empty export and wrap the namespace in a global‐augmentation block so this file is always treated as a module and your augmentation merges reliably.File: src/@types/express.d.ts
export {}; declare global { namespace Express { interface Request { userIp: string | string[]; user?: { id_user: string; name: string; }; } } }src/modules/note/domain/dtos/requests/StarNote.request.dto.ts (1)
4-9: Tighten validation for note_id and reject unknown fieldsIf IDs are UUIDs, validate as such. At minimum ensure non-empty and strip/forbid extraneous properties to harden the DTO.
-export const StarNoteBodySchema = z.object({ - note_id: z.string(), -}); +export const StarNoteBodySchema = z + .object({ + // If your IDs are UUIDs, swap .min(1) for .uuid() + note_id: z.string().min(1, 'note_id is required'), + }) + .strict();src/shared/domain/dtos/requests/HealthCheck.request.dto.ts (1)
4-8: Tighten validation (non-empty strings, ISO datetime).Add basic guards and ensure timestamp is ISO-8601. Keeps Swagger accurate and prevents junk values.
export const HealthCheckSchema = z.object({ - message: z.string().describe('Mensagem de verificação do HealthCheck'), - status: z.string().describe('Status do HealthCheck'), - timestamp: z.string().describe('Timestamp da verificação do HealthCheck'), + message: z.string().min(1).describe('Mensagem de verificação do HealthCheck'), + status: z.string().min(1).describe('Status do HealthCheck'), + timestamp: z.string().datetime().describe('Timestamp da verificação do HealthCheck'), });src/modules/user/domain/providers/Hash.provider.ts (1)
1-4: Make bcrypt cost configurable
Replace the hardcoded10in HashProvider (src/modules/user/infra/providers/hash.provider.ts:8) with a value injected via environment (e.g.process.env.BCRYPT_ROUNDS) or passed through the provider’s config, with a sensible default fallback.PULL_REQUEST.md (2)
46-50: Add languages to fenced code blocks (markdownlint MD040).Use bash for commands and env for .env snippets to improve rendering and tooling.
Examples:
Also applies to: 61-87, 93-95, 100-106
102-103: Use forward slashes in paths for cross‑platform docs.Prefer ./src/shared/infra/db/prisma/seeders over backslashes.
-('./src\shared\infra\db\prisma\seeders') +('./src/shared/infra/db/prisma/seeders')src/shared/domain/dtos/providers/Date.provider.dto.ts (2)
21-25: Clarify interval semantics (inclusive/exclusive).Add flags or document boundary behavior to avoid off‑by‑one errors.
export interface IsDateInIntervalDTO { date: Date; startDate: Date; endDate: Date; + inclusiveStart?: boolean; // default: true + inclusiveEnd?: boolean; // default: false }
16-19: Naming nit: simplifyDateIsDateGreaterThanDTO.
IsDateGreaterThanDTOis clearer.src/modules/user/domain/dtos/requests/UserLogin.request.dto.ts (1)
11-19: Make response schemas strict and non-empty token.
- Add
.strict()to both the top-level and nesteduserobject.- Ensure
tokenis non-empty.-export const UserLoginResponseSchema = z.object({ - token: z.string().describe('Token de autenticação do usuário'), - user: z - .object({ - id_user: z.string().describe('ID do usuário autenticado'), - name: z.string().describe('Nome do usuário autenticado'), - }) - .describe('Dados do usuário autenticado'), -}); +export const UserLoginResponseSchema = z + .object({ + token: z.string().min(1).describe('Token de autenticação do usuário'), + user: z + .object({ + id_user: z.string().describe('ID do usuário autenticado'), + name: z.string().describe('Nome do usuário autenticado'), + }) + .strict() + .describe('Dados do usuário autenticado'), + }) + .strict();If IDs are UUIDs/CUIDs, consider
z.string().uuid()orz.string().cuid2()forid_user.src/shared/domain/dtos/controllers/health.controller.dto.ts (2)
1-9: Decouple controllers from ExpressResponseacross the codebase.Script (
rg -nP --type=ts -C2 '\bResponse\b' src) revealed ExpressResponseinjections in:
src/shared/http/controllers/shared.controller.tssrc/modules/user/domain/dtos/controllers/user.controller.dto.tssrc/modules/note/domain/dtos/controllers/note.controller.dto.ts
(plus other controller DTOs and HTTP controllers)• Update
HealthControllerInterface.getStatusto drop@Res()/Responseand return a typed DTO (e.g.HealthStatusResponseDto).
• Apply the same refactor to all controller interfaces and implementations—return plain objects or DTO classes and let Nest handle HTTP responses.
• Optionally introduce dedicated response DTOs (via Zod or class-transformer) to enforce schema and keep Swagger docs consistent.
2-2: Configure and use TS path aliasestsconfig.json currently lacks a
pathsmapping undercompilerOptions. Add something like:"compilerOptions": { "baseUrl": ".", "paths": { "@shared/*": ["src/shared/*"] } }Then replace the relative import with an alias import:
import { AllExceptionsFilterDTO } from '@shared/domain/dtos/errors/AllException.filter.dto';src/modules/note/domain/dtos/requests/ChangeNoteColor.request.dto.ts (1)
4-9: Tighten schema (strict) and fix apostrophe.
- Make the request body strict.
- Fix description punctuation.
-export const ChangeNoteColorBodySchema = z.object({ - note_id: z.string(), - note_color: z - .enum(['red', 'green', 'blue', 'yellow']) - .describe('Note`s color'), -}); +export const ChangeNoteColorBodySchema = z + .object({ + note_id: z.string(), + note_color: z + .enum(['red', 'green', 'blue', 'yellow']) + .describe("Note's color"), + }) + .strict();If IDs are UUIDs/CUIDs, consider
z.string().uuid()orz.string().cuid2(). Also consider centralizing the color enum in a shared type used by both FE and BE to avoid drift.eslint.config.mjs (2)
9-10: Ignore common build/artifact folders.Add
dist/**andcoverage/**to reduce lint noise.- ignores: ['eslint.config.mjs'], + ignores: ['eslint.config.mjs', 'dist/**', 'coverage/**'],
20-20: Remove explicitsourceType: defer to tsconfig
tsconfig.jsonalready specifiescompilerOptions.module = "commonjs", so ESLint will infer the same—dropping this override preserves current behavior and avoids ESM conflicts.src/modules/note/domain/dtos/requests/EditNote.request.dto.ts (1)
15-16: Unify color typing via a shared enum.Keep a single source of truth and reuse across DTOs (this file and FindNote.*).
Example:
// src/modules/note/domain/schemas/note-color.schema.ts export const NoteColorSchema = z.enum(['red', 'green', 'blue', 'yellow']); export type NoteColor = z.infer<typeof NoteColorSchema>;Then in this file:
- note_color: z - .enum(['red', 'green', 'blue', 'yellow']) + note_color: NoteColorSchemasrc/modules/note/domain/dtos/requests/FindNote.request.dto.ts (2)
15-23: Class-name casing: standardize DTO suffix.Other files use “DTO”; these are “Dto”. Consider renaming for consistency (will require updating imports).
-export class FindNoteByIdResponseDto extends createZodDto( +export class FindNoteByIdResponseDTO extends createZodDto( FindNoteByIdResponseSchema, ) {} export const BrowseNotesResponseSchema = z.array(FindNoteByIdResponseSchema); -export class BrowseNotesResponseDto extends createZodDto( +export class BrowseNotesResponseDTO extends createZodDto( BrowseNotesResponseSchema, ) {}This is a breaking rename; confirm and update all imports/usages.
4-13: Doc language consistency.Descriptions here are PT‑BR; others are EN. Pick one language for Swagger to avoid mixed docs.
Also applies to: 19-19
Dockerfile (1)
7-9: Pin and verify the wait script download.Remote ADD without checksum is brittle. Prefer curl + sha256 verification.
# example # RUN apk add --no-cache curl ca-certificates && \ # curl -fsSL -o /wait ".../wait" && \ # echo "<sha256> /wait" | sha256sum -c - && chmod +x /waitsrc/modules/user/domain/dtos/errors/InvalidCredentials.exception.ts (1)
1-7: Use UnauthorizedException for invalid credentials
ExtendingUnauthorizedExceptionand removing the explicit 400 status aligns with HTTP semantics and NestJS conventions; existing tests rely onnew InvalidCredentialsException().getStatus()and will adapt automatically.src/app/main.ts (1)
22-25: Hide Swagger in production (or guard it)Prevent exposing API docs publicly in prod.
- const sharedDocument = SwaggerModule.createDocument(app, sharedSwaggerConfig); - SwaggerModule.setup('swagger', app, sharedDocument); + if (process.env.NODE_ENV !== 'production') { + const sharedDocument = SwaggerModule.createDocument(app, sharedSwaggerConfig); + SwaggerModule.setup('swagger', app, sharedDocument); + }src/modules/user/infra/providers/jwt.provider.spec.ts (1)
15-28: Drop unnecessary async from this testNo awaits used; keep it synchronous.
-it('should generate a JWT given the proper data', async () => { +it('should generate a JWT given the proper data', () => {src/modules/user/domain/dtos/errors/EmailAlreadyRegistered.exception.ts (1)
1-7: Use 409 Conflict (ConflictException) instead of 403 for duplicate emailMatches resource-conflict semantics and common REST patterns.
-import { HttpException } from '@nestjs/common'; +import { ConflictException } from '@nestjs/common'; -export class EmailAlreadyRegisteredException extends HttpException { +export class EmailAlreadyRegisteredException extends ConflictException { constructor() { - super('Este e-mail já está associado a uma conta.', 403); + super('Este e-mail já está associado a uma conta.'); } }src/shared/domain/errors/Environment.exception.ts (1)
5-9: Improve message formatting and name the errorPrefix the message with the path only when present and set a specific
nameto ease filtering/logging.- const errorMessage = error.issues - .map((issue) => `${issue.path.join('.')} ${issue.message}`) - .join('\n'); - super(errorMessage); + const errorMessage = error.issues + .map(({ path, message }) => `${path.length ? `${path.join('.')}: ` : ''}${message}`) + .join('\n'); + super(errorMessage); + this.name = 'EnvironmentException';src/app/app.module.ts (1)
19-24: Provide ValidationPipe optionsEnable common safeguards (strip unknown fields, transform primitives).
providers: [ { provide: APP_PIPE, - useClass: ValidationPipe, + useFactory: () => + new ValidationPipe({ + whitelist: true, + transform: true, + forbidNonWhitelisted: true, + }), }, ],src/modules/note/infra/db/repositories/note.repository.spec.ts (3)
5-5: Rename suite to match subject-describe('Gym Repository Test Suites', () => { +describe('Note Repository Test Suites', () => {
9-16: Merge duplicate beforeEach hooks (Biome warning)-beforeEach(() => { - jest.useFakeTimers({ doNotFake: ['nextTick'] }); -}); - -beforeEach(() => { - encrypterProvider = new EncrypterProvider(); - repository = new NoteRepository(encrypterProvider); -}); +beforeEach(() => { + jest.useFakeTimers({ doNotFake: ['nextTick'] }); + encrypterProvider = new EncrypterProvider(); + repository = new NoteRepository(encrypterProvider); +});
1-25: Add meaningful tests for encryption behaviorNo assertions yet. Add at least one test asserting the repository encrypts sensitive fields and can round-trip decrypt.
I can draft tests once you confirm the repository API (e.g.,
create,findById) signatures.src/modules/user/infra/providers/hash.provider.spec.ts (3)
7-13: Compile the TestingModule once with beforeAll to speed up tests.
Provider is stateless; setting up the module per test is unnecessary overhead.Apply this diff:
- beforeEach(async () => { + beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [HashProvider], }).compile(); hashProvider = module.get<HashProvider>(HashProvider); - }); + });
15-20: Avoid brittle assertion on '$' in hash string.
The presence of '$' is bcrypt‑specific; the test should remain algorithm‑agnostic.Apply this diff:
const hashedValue = await hashProvider.hash('password'); expect(hashedValue).toEqual(expect.any(String)); - expect(hashedValue).toContain('$'); + expect(hashedValue).not.toBe('password'); + expect(hashedValue.length).toBeGreaterThan(20);
28-33: Add a test to ensure salting (same input ≠ same hash).
Prevents regressions if salting is accidentally disabled.Add this test:
it('should produce different hashes for the same password (salted)', async () => { const a = await hashProvider.hash('password'); const b = await hashProvider.hash('password'); expect(a).not.toBe(b); });src/modules/user/domain/providers/JWT.provider.ts (1)
1-7: Decouple domain from jsonwebtoken types; return a domain payload instead.
Importing JwtPayload in the domain layer couples it to an infra lib and leaks implementation details.Apply this diff to remove the dependency and return a generic decoded shape:
-import { JwtPayload } from 'jsonwebtoken'; import { JWTProviderDTO, JWTValidateDTO } from '../../dto/JWTProvider.dto'; export interface JWTProviderInterface { generate(data: JWTProviderDTO): string; - validate(data: JWTValidateDTO): JwtPayload | string; + validate(data: JWTValidateDTO): Record<string, unknown>; }Optionally, define and return a project‑level JWTPayload type (e.g., { user: { id_user: string; name?: string } } & standard claims).
src/shared/domain/errors/AllException.filter.ts (3)
12-12: Remove unnecessary async from catch; nothing is awaited.Apply this diff:
- async catch(exception: unknown, host: ArgumentsHost) { + catch(exception: unknown, host: ArgumentsHost) {
22-24: Preserve HttpException response message(s) when available.
Surface validation/user errors consistently; fall back safely for unknown errors.Apply this diff:
- const message = - exception instanceof Error ? exception.message : 'Internal Server Error'; + const message = + exception instanceof HttpException + ? (() => { + const res = exception.getResponse(); + if (typeof res === 'string') return res; + const msg = (res as any)?.message; + return Array.isArray(msg) ? msg.join(', ') : msg ?? exception.message; + })() + : 'Internal Server Error';
25-33: Avoid leaking internal messages in production; add minimal guard.
For 500s in production, prefer a generic message.Suggested snippet (outside diff scope):
const isProd = process.env.NODE_ENV === 'production'; const safeMessage = isProd && status === HttpStatus.INTERNAL_SERVER_ERROR ? 'Internal Server Error' : message; response.status(status).json({ ...responseJSON, message: safeMessage });Also consider logging the original error via Nest Logger.
src/modules/user/dto/JWTProvider.dto.ts (1)
1-10: Make secret a config concern (not per-call DTO) or require it explicitly.
Per-call optional secret risks signing/verifying with an empty/undefined secret.If the provider already falls back to auth.config, remove secret from the DTO:
export type JWTProviderDTO = { payload: { user?: { id_user: string; name?: string; }; }; - secret?: string; expiresIn?: number | string; };Otherwise, make it required and validate upstream.
To keep naming consistent, consider userId over id_user in tokens.src/modules/note/infra/usecases/browse-notes.usecase.spec.ts (3)
20-24: Avoid constructing real crypto/hash providers in unit testsInstantiate minimal mocks/stubs to avoid env-coupling and speed up tests. Also, EncrypterProvider uses a fixed IV for AES‑CBC (security risk); mocking here avoids accidental crypto usage in unit tests.
Example:
- encrypterProvider = new EncrypterProvider(); - mockRepository = new NoteRepository(encrypterProvider); - hashProvider = new HashProvider(); - userRepository = new UserRepository(hashProvider, encrypterProvider); + mockRepository = { findByUser: jest.fn() } as unknown as NoteRepository; + userRepository = { findById: jest.fn() } as unknown as UserRepository;(Architecture note: use a random per‑message IV and store it alongside ciphertext in EncrypterProvider.)
35-41: Use a color-like value for note_color to match domain intentfaker.lorem.paragraph() isn’t a color. Use faker.color.rgb() (or hex) to better reflect the schema.
- note_color: faker.lorem.paragraph(), + note_color: faker.color.rgb(), // or faker.color.rgb({ prefix: '#' })
44-52: Assert repository calls, not just resultsValidate that the correct dependencies are invoked with the right arguments.
const result = await usecase.execute(mockNote.user_id); expect(result).toEqual([]); + expect(userRepository.findById).toHaveBeenCalledWith(mockNote.user_id); + expect(mockRepository.findByUser).toHaveBeenCalledWith(mockNote.user_id);src/modules/user/infra/usecases/user-login.usecase.spec.ts (1)
64-69: Verify JWT payload shapeAssert generator is called with the expected payload to lock contract.
jest.spyOn(jwtProvider, 'generate').mockReturnValue('valid-token'); const result = await usecase.execute(data, ip); expect(result).toEqual({ @@ }); + expect(jwtProvider.generate).toHaveBeenCalledWith({ + payload: { user: { id_user: mockUser.id_user, name: mockUser.name } }, + });Also applies to: 70-79
src/modules/note/infra/usecases/change-note-color.usecase.spec.ts (2)
39-45: Rename misleading variable and prefer a domain‑valid colorinvalidData is used in successful paths; rename to payload and use a color-like string.
- const invalidData: ChangeNoteColorRequestDto = { + const payload: ChangeNoteColorRequestDto = { note_id: faker.string.uuid(), - note_color: faker.color.rgb() as any, + note_color: faker.color.rgb(), };Also update subsequent references from invalidData → payload.
81-92: Fix test description and assert repo call argumentsThe test is for color change, not starring. Also ensure changeColor is called correctly.
- it('should successfully star a note', async () => { + it('should successfully change note color', async () => { @@ - await expect(useCase.execute(userId, invalidData)).resolves.not.toThrow(); + await expect(useCase.execute(userId, payload)).resolves.not.toThrow(); + expect(mockNoteRepository.changeColor).toHaveBeenCalledWith( + payload.note_id, + payload.note_color, + );src/modules/note/infra/usecases/change-note-color.usecase.ts (3)
21-27: Tighten return type to reflect thrown exceptions.This use case throws on errors; it does not return exception instances. Prefer
Promise<boolean>.- ): Promise< - | boolean - | ColorAlreadySetException - | UserNotFoundException - | UnauthorizedException - | NoteNotFoundException - > { + ): Promise<boolean> {
1-1: Use Forbidden (403) for ownership violations, not Unauthorized (401).Ownership check should return 403. Keep 401 for unauthenticated requests.
-import { Inject, UnauthorizedException } from '@nestjs/common'; +import { Inject, ForbiddenException, UnauthorizedException } from '@nestjs/common'; ... - if (note.user_id !== user.id_user) throw new UnauthorizedException(); + if (note.user_id !== user.id_user) throw new ForbiddenException();Also applies to: 36-36
46-46: Minor: Prefer explicit Boolean conversion.
Boolean(updated)is clearer than!!updated.- return !!updated; + return Boolean(updated);src/modules/user/infra/providers/jwt.provider.ts (1)
15-18: Constrain verification algorithm and narrow payload type.
- Restrict algorithms to prevent alg confusion.
- Consider a custom payload type including
user.- public validate({ token, secret }: JWTValidateDTO): JwtPayload | string { - const decoded = verify(token, secret); + public validate({ token, secret }: JWTValidateDTO): JwtPayload | string { + const decoded = verify(token, secret, { algorithms: ['HS256'] }); return decoded; }Optionally define:
type AppJwtPayload = JwtPayload & { user?: { id_user: string; name?: string } };and propagate through the interface.
src/modules/note/infra/usecases/edit-note.usecase.spec.ts (2)
37-40: Avoid shared mutable test data; make each test self-contained.Mutating
invalidData/dataintroduces inter-test coupling. Create local payloads per test.Example pattern:
const payload: EditNoteBodyDTO = { note_title: faker.string.alpha(10), note_text: faker.string.alpha(10) }; await expect(useCase.execute(noteId, userId, payload)).rejects.toThrow(...);Also applies to: 49-54, 58-66, 68-76, 78-88, 92-105
94-105: Strengthen success case: return value assertions and realistic mocks.Mock
editwith a full entity and assert the mapped DTO.- jest.spyOn(mockNoteRepository, 'edit').mockResolvedValueOnce({} as any); - await expect( - useCase.execute(noteId, userId, invalidData), - ).resolves.not.toThrow(); + const edited = { + id_note: faker.string.uuid(), + note_title: faker.string.alpha(10), + note_text: faker.string.alpha(20), + note_color: 'yellow', + user_id: userId, + starred: false, + createdAt: new Date(), + updatedAt: new Date(), + }; + jest.spyOn(mockNoteRepository, 'edit').mockResolvedValueOnce(edited as any); + await expect(useCase.execute(noteId, userId, { + note_title: edited.note_title, + note_text: edited.note_text, + } as any)).resolves.toEqual({ + id_note: edited.id_note, + note_title: edited.note_title, + note_text: edited.note_text, + note_color: edited.note_color, + user_id: edited.user_id, + starred: edited.starred, + created_at: edited.createdAt, + updated_at: edited.updatedAt, + });Also applies to: 100-101
src/modules/user/infra/usecases/create-user.usecase.spec.ts (1)
43-52: Use per-test input objects; assert hashing is used.
- Avoid mutating shared
data/mockdataacross tests.- In the success case, stub
hashto return a different value (e.g., 'hashed') and assertcreatereceives it.Example additions:
jest.spyOn(hashProvider, 'hash').mockResolvedValueOnce('hashed'); ... expect(mockUserRepository.create).toHaveBeenCalledWith( expect.objectContaining({ password: 'hashed' }) );Also applies to: 64-72, 74-82, 84-92, 93-101, 105-129
src/shared/config/app.config.ts (1)
20-36: Optional: Use Zod coercion for numeric envs and add stronger validation.
- Replace manual
Number(...)withz.coerce.number()forPORT/Redis ports.- Consider
CRYPTO_IVlength constraints (e.g., 16 bytes for AES-256-CBC).- Optionally validate
FRONTEND_URLwith.url().Example schema tweaks:
- PORT: z.number(), + PORT: z.coerce.number(), ... - REDIS_PRODUCTION_PORT: z.number().optional(), + REDIS_PRODUCTION_PORT: z.coerce.number().optional(), ... - REDIS_PORT: z.number().optional(), + REDIS_PORT: z.coerce.number().optional(), + CRYPTO_IV: z.string().length(16), + FRONTEND_URL: z.string().url(),And pass raw strings into
parse(no pre-Number casting).Also applies to: 48-49, 61-62, 71-73
src/modules/note/infra/modules/note.module.ts (3)
31-31: Remove duplicate provider entry for UserRepository.It's listed twice; keep a single entry.
BrowseNotesUsecase, - UserRepository, ChangeNoteColorUsecase,Also applies to: 38-38
57-78: Unify route param naming (:noteId vs :cuid).Use one param name across find/edit/delete for consistency and clearer Swagger.
Would you like me to align controller @Param names and middleware paths to :noteId?
24-26: Consider importing shared/user modules instead of re-declaring shared providers.Import/export Encrypter/Date/JWT/User providers from SharedModule/UserModule to avoid multiple instances and ease maintenance.
src/modules/user/http/controllers/user.controller.spec.ts (2)
21-26: Remove unused variables (controller, jwtToken) or use them.Avoid dead code in tests.
- let controller: UserController; - let app: INestApplication; let prisma: PrismaProvider; - let jwtToken: string;
51-53: No-op global filters call.useGlobalFilters() without args is a no-op; remove or pass filters.
- app.useGlobalFilters();src/modules/user/infra/providers/hash.provider.ts (1)
1-16: Make bcrypt cost configurable and avoid name shadowing.
- Externalize salt rounds to config (e.g., ENV or app config) for security/perf tuning.
- Alias imports to avoid confusion with param name “hash”.
-import { compare, hash } from 'bcryptjs'; +import { compare as bcryptCompare, hash as bcryptHash } from 'bcryptjs'; @@ public async hash(payload: string): Promise<string> { - const hashedValue = await hash(payload, 10); + const rounds = Number(process.env.BCRYPT_ROUNDS ?? 10); + const hashedValue = await bcryptHash(payload, rounds); @@ public async compare(payload: string, hash: string): Promise<boolean> { - const isHashEqual = await compare(payload, hash); + const isHashEqual = await bcryptCompare(payload, hash);src/modules/note/infra/usecases/find-note-by-id.usecase.spec.ts (1)
25-27: Merge duplicate beforeEach hooks.Keep a single beforeEach to set timers and initialize test state.
- beforeEach(() => { - jest.useFakeTimers({ doNotFake: ['nextTick'] }); - }); - - beforeEach(async () => { + beforeEach(async () => { + jest.useFakeTimers({ doNotFake: ['nextTick'] });Also applies to: 29-34
src/modules/note/infra/usecases/create-note.usecase.spec.ts (2)
22-29: Merge duplicate beforeEach hooks.One hook is enough; initialize timers and test doubles there.
- beforeEach(() => { - jest.useFakeTimers({ doNotFake: ['nextTick'] }); - }); - - beforeEach(async () => { + beforeEach(async () => { + jest.useFakeTimers({ doNotFake: ['nextTick'] });
1-10: Remove unused imports.UnauthorizedException and UnprocessableDataException aren’t used here.
-import { UnauthorizedException } from '@nestjs/common'; @@ -import { UnprocessableDataException } from '../../../../shared/domain/errors/UnprocessableData.exception';src/modules/user/domain/dtos/requests/CreateUser.request.dto.ts (1)
18-21: Constrain id format if applicable.If user IDs are CUIDs/UUIDs, assert it in the schema to prevent leaking internal formats.
Example (CUID):
- id: z.string().describe('ID do usuário criado'), + id: z.string().cuid().describe('ID do usuário criado'),If UUID, use
.uuid()instead. Confirm the intended format before changing.src/modules/user/http/middlewares/Auth.middleware.ts (2)
5-5: Fix typo in exception name (Eception → Exception) across codebase.Consistent naming improves readability and discoverability.
If you rename the class to
BadTokenException, update imports here and all throw sites.
24-24: Avoid reading env directly in request path; inject config.Use Nest ConfigService (or a dedicated settings provider) to access APP_SECRET and fail fast on misconfig at boot.
src/modules/note/infra/usecases/browse-notes.usecase.ts (2)
16-24: Align typing with control flow: remove exception types from return union.This use case throws on error; it never returns exceptions.
- async execute( - cuid: string, - ): Promise<BrowseNotesResponseDto | UserNotFoundException> { + async execute(cuid: string): Promise<BrowseNotesResponseDto> {
17-18: Clarify parameter name.
cuidhere is a user id; rename touser_idfor readability unless it is strictly a CUID type everywhere.src/modules/note/infra/usecases/find-note-by-id.usecase.ts (1)
16-18: Narrow the return type to the success DTO only.Exceptions are thrown, not returned.
- ): Promise< - FindNoteByIdResponseDto | NoteNotFoundException | UnauthorizedException - > { + ): Promise<FindNoteByIdResponseDto> {(Update the imported exception type above per the prior comment.)
src/modules/note/infra/usecases/star-note.usecase.ts (2)
10-15: Mark injected deps as readonlyMinor immutability/readability improvement.
- private noteRepository: NoteRepository, + private readonly noteRepository: NoteRepository, @@ - private userRepository: UserRepository, + private readonly userRepository: UserRepository,
17-25: Narrow return type; exceptions are thrown, not returnedThe method never resolves to an exception instance. Declare Promise.
- ): Promise< - | boolean - | NoteNotFoundException - | UserNotFoundException - | UnauthorizedException - > { + ): Promise<boolean> {src/modules/note/infra/usecases/search-notes.usecase.spec.ts (1)
15-27: Consolidate duplicate beforeEach and move clearAllMocks to afterEachRemoves lint warning (noDuplicateTestHooks) and keeps hooks tidy.
-beforeEach(() => { - jest.useFakeTimers({ doNotFake: ['nextTick'] }); -}); - -beforeEach(async () => { +beforeEach(async () => { + jest.useFakeTimers({ doNotFake: ['nextTick'] }); encrypterProvider = new EncrypterProvider(); mockRepository = new NoteRepository(encrypterProvider); hashProvider = new HashProvider(); userRepository = new UserRepository(hashProvider, encrypterProvider); - usecase = new SearchNotesUsecase(mockRepository, userRepository); - jest.clearAllMocks(); });Add afterEach to clean mocks (outside this hunk):
afterEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); });src/modules/user/domain/dtos/repositories/User.repository.ts (1)
4-14: Interface shape: consider returning concrete User where possible and make encryptedFields readonlyReturning Partial from reads makes downstream code defensive and led to inconsistent ownership checks elsewhere. Prefer Promise<User | null> and readonly encryptedFields to stabilize contracts. If changing now is too invasive, add a typed pick that matches actual projections (e.g., Pick<User, 'id_user' | 'email' | ...>).
src/modules/note/infra/usecases/delete-note.usecase.spec.ts (2)
19-29: Consolidate duplicate beforeEach; move timers into the remaining oneFixes lint warning and reduces hook churn.
-beforeEach(() => { - jest.useFakeTimers({ doNotFake: ['nextTick'] }); -}); - -beforeEach(async () => { +beforeEach(async () => { + jest.useFakeTimers({ doNotFake: ['nextTick'] }); encrypterProvider = new EncrypterProvider(); hashProvider = new HashProvider(); mockUserRepository = new UserRepository(hashProvider, encrypterProvider); mockNoteRepository = new NoteRepository(encrypterProvider); useCase = new DeleteNoteUsecase(mockUserRepository, mockNoteRepository); - jest.clearAllMocks(); });Add afterEach to clean mocks (outside this hunk):
afterEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); });
87-101: Set mockNote.user_id before wiring spiesAvoid relying on object aliasing timing; makes the test intention clearer.
- it('should return true when a note is successfully deleted', async () => { - jest - .spyOn(mockNoteRepository, 'findById') - .mockResolvedValueOnce(mockNote); - jest - .spyOn(mockUserRepository, 'findById') - .mockResolvedValueOnce({} as any); - jest.spyOn(mockNoteRepository, 'delete').mockResolvedValueOnce(true); - - mockNote.user_id = user_id; + it('should return true when a note is successfully deleted', async () => { + mockNote.user_id = user_id; + jest + .spyOn(mockNoteRepository, 'findById') + .mockResolvedValueOnce(mockNote); + jest + .spyOn(mockUserRepository, 'findById') + .mockResolvedValueOnce({} as any); + jest.spyOn(mockNoteRepository, 'delete').mockResolvedValueOnce(true);src/modules/user/infra/db/repositories/User.repository.spec.ts (1)
10-19: Consolidate duplicate beforeEachRemoves lint warning and keeps setup simple.
-beforeEach(() => { - jest.useFakeTimers({ doNotFake: ['nextTick'] }); -}); - -beforeEach(() => { +beforeEach(() => { + jest.useFakeTimers({ doNotFake: ['nextTick'] }); repository = new UserRepository( new HashProvider(), new EncrypterProvider(), ); });src/modules/note/infra/usecases/star-note.usecase.spec.ts (2)
32-34: Remove unnecessary fake timers.Timers aren’t used; drop this to reduce global test side effects.
-afterAll(() => { - jest.useRealTimers(); -}); +// No fake timers used; no teardown needed.
24-29: Avoid env‑dependent providers in unit tests.Instantiating real EncrypterProvider/HashProvider and concrete repositories couples tests to env (CRYPTO_SECRET/IV). Prefer pure mocks for NoteRepository/UserRepository.
I can replace these with typed jest mocks (minimal interface stubs) if you want.
src/modules/user/domain/dtos/controllers/user.controller.dto.ts (1)
6-21: Controller contract mixes Express Response and DTOs; align with Nest patterns.Returning Response couples to Express and complicates testing. Prefer returning DTOs and letting Nest handle the response; also keep param order consistent.
Would you like me to generate a DTO-first interface and update the controller signatures accordingly?
src/modules/note/infra/usecases/create-note.usecase.ts (1)
30-46: Defensive default for starred.If DTO marks “starred” optional, default to false when creating notes.
- starred: data.starred, + starred: data.starred ?? false,README.md (4)
56-61: Clarify database engine and provide correct DSN examples.PR uses MySQL; either lock to MySQL or show both DSNs explicitly.
-Since we're using Prisma, you only need to set the DB's connection string in the `DATABASE_URL` variable. The application supports both PostgreSQL and MySQL, so you can choose any of them. The connection string should be set in the following way: +Since we're using Prisma, set the DB connection string in `DATABASE_URL`. +Examples: @@ -DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public" +# MySQL +DATABASE_URL="mysql://USER:PASSWORD@HOST:3306/DATABASE" +# PostgreSQL +# DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/DATABASE?schema=public"
30-38: Tighten Redis wording.“Redis CLI” isn’t a database; clarify local server via Docker/redis-server and credentials behavior.
-If you're going for Redis CLI or a Redis Image being run on Docker, the application won't require user and password to authenticate you in the database. It's enough to only set the following variables: +If you're running a local Redis server (e.g., Docker image or redis-server), authentication is typically disabled by default. In that case, set only:
87-87: Link to Node’s built‑in crypto docs (not the npm “crypto” stub).-- [Crypto](https://www.npmjs.com/package/crypto) +- [Node.js Crypto](https://nodejs.org/api/crypto.html)
94-94: Use the correct Faker package.The repo uses @faker-js/faker; update the link/name.
-- [Faker](https://www.npmjs.com/package/faker) +- [@faker-js/faker](https://www.npmjs.com/package/@faker-js/faker)src/modules/note/http/controllers/note.controller.spec.ts (3)
12-12: Remove unused import.NoteController is not referenced.
-import { NoteController } from './note.controller';
23-23: Remove unused variable.
controlleris declared but never used.- let controller: NoteController;
47-47: Drop no-op global filters registration.
app.useGlobalFilters()with no filters is a no-op; remove for clarity or pass actual filters.- app.useGlobalFilters(); + // app.useGlobalFilters(new AllExceptionsFilter()); // if/when neededsrc/modules/note/domain/dtos/controllers/note.controller.dto.ts (1)
8-49: Prefer Nest-style controller contracts (return DTOs, avoid Response).Returning Express Response and threading req/res through the interface tightly couples to Express and complicates testing. Consider returning DTOs and letting Nest handle response mapping; keep errors as thrown exceptions, not part of the return union.
src/modules/note/infra/usecases/edit-note.usecase.ts (4)
42-42: Use ForbiddenException (403) for ownership violations.The user is authenticated; lack of ownership is authorization, not authentication.
- if (noteExists.user_id !== user_id) throw new UnauthorizedException(); + if (noteExists.user_id !== user_id) throw new ForbiddenException();
1-1: Align imports with the change above.-import { Inject, UnauthorizedException } from '@nestjs/common'; +import { ForbiddenException } from '@nestjs/common';
15-19: Remove redundant @Inject with type tokens.Constructor injection by type doesn’t need @Inject here.
- constructor( - @Inject() - private readonly userRepository: UserRepository, - @Inject() - private readonly noteRepository: NoteRepository, - ) {} + constructor( + private readonly userRepository: UserRepository, + private readonly noteRepository: NoteRepository, + ) {}
25-29: Document thrown NoteNotFoundException in the signature (for consistency).You already throw it below; add it to the union for clarity (even though TS doesn’t enforce thrown types).
): Promise< | EditNoteResponseDTO | UnauthorizedException | UserNotFoundException | UnprocessableDataException + | NoteNotFoundException > {src/modules/note/domain/dtos/repositories/Note.repository.ts (2)
14-14: Parameter name typo: this should be id_note.Prevents confusion with user id.
- findById(id_user: string): Promise<FindNoteByIdResponseDto | null>; + findById(id_note: string): Promise<FindNoteByIdResponseDto | null>;
11-27: Repository return-type consistency.Some methods return DTOs while others return entities (Note). Consider standardizing on entities at repository layer and mapping to DTOs in use cases/controllers.
src/modules/user/infra/usecases/create-user.usecase.ts (3)
42-47: Avoid mixing await with .then; simplify for readability.- await this.userRepository - .findByEmail(data.email) - .then((emailAlreadyRegisteredByUser) => { - if (emailAlreadyRegisteredByUser) - throw new EmailAlreadyRegisteredException(); - }); + const emailAlreadyRegisteredByUser = + await this.userRepository.findByEmail(data.email); + if (emailAlreadyRegisteredByUser) + throw new EmailAlreadyRegisteredException();
22-24: Consistent DI style: drop @Inject() when injecting by type.- @Inject() - private userRepository: UserRepository, + private userRepository: UserRepository,
1-1: Remove unused Inject import (after the change above).-import { Inject, UnprocessableEntityException } from '@nestjs/common'; +import { UnprocessableEntityException } from '@nestjs/common';src/modules/user/infra/usecases/user-login.usecase.ts (1)
30-31: Redundant checks forid_userandnameThe checks for
!user.id_userand!user.nameare redundant if the database schema enforces these as required fields. The!usercheck should be sufficient.- if (!user || !user.id_user || !user.name) + if (!user) throw new InvalidCredentialsException();src/modules/user/infra/db/repositories/user.repository.ts (2)
50-60: Potential performance issue with encrypted email lookupsFinding users by encrypted email requires exact string matching of the encrypted value. This prevents database indexing from working efficiently and could cause performance issues at scale.
Consider using a deterministic encryption scheme for the email field or maintaining a separate hashed email column for lookups. This would allow for efficient indexing while maintaining security.
112-112: Unnecessary object spread operationThe spread operation
{ ...userData }creates an unnecessary copy since you're not modifying the original object structure.- const { ...userData } = data; + const userData = data;docker-compose.yml (2)
28-30: DeprecatedlinksdirectiveThe
linksdirective is deprecated in Docker Compose. Services can communicate using service names directly through the default network.- links: - - database - - redis depends_on:
40-40: Missing newline at end of fileAdd a newline character at the end of the file to follow YAML best practices.
- PORT=3333 +src/modules/user/http/controllers/user.controller.ts (1)
75-77: Consider using middleware for authentication checks.The authentication check
if (req.user)is repeated across methods. Consider extracting this into a middleware or guard for better separation of concerns and consistency.src/modules/note/infra/db/repositories/note.repository.ts (1)
28-35: Search functionality has potential performance issues.The search method loads all user notes into memory and filters them client-side. For users with many notes, this could be inefficient.
Consider implementing database-level search or adding pagination:
// Alternative approach using database search async search(id_user: string, content: string): Promise<BrowseNotesResponseDto> { const notes = await prisma.note.findMany({ where: { user_id: id_user, deletedAt: null, OR: [ { note_title: { contains: content } }, { note_text: { contains: content } } ] }, }); // Note: This would require storing search keywords unencrypted // or implementing searchable encryption }src/modules/note/http/controllers/note.controller.ts (2)
164-164: Return type should be more specific.The return type
any | Response | AllExceptionsFilterDTOis too broad. Consider using a union of specific response types.Apply this diff for better type safety:
- ): Promise<any | Response | AllExceptionsFilterDTO> { + ): Promise<Response | AllExceptionsFilterDTO> {Also applies to: 209-209
90-90: Authentication checks are consistent but repetitive.All endpoints properly check for authentication, but this pattern is repeated throughout. Consider using a guard or middleware for cleaner code.
Also applies to: 121-121, 152-152, 165-165, 210-210, 249-249, 296-296, 344-344, 382-382
| # Application Environment Variables | ||
| NODE_ENV= | ||
| PORT= | ||
| API_URL="" | ||
| FRONTEND_URL="" | ||
| DATABASE_URL="" | ||
| APP_SECRET= | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Sync with docs, remove quotes, add missing DATABASE_PROVIDER, and end with newline.
Aligns with PULL_REQUEST.md and quiets dotenv-linter. Quotes aren’t needed here.
# Application Environment Variables
-NODE_ENV=
-PORT=
-API_URL=""
-FRONTEND_URL=""
-DATABASE_URL=""
-APP_SECRET=
+NODE_ENV=
+PORT=
+API_URL=
+FRONTEND_URL=
+DATABASE_URL=
+DATABASE_PROVIDER=
+APP_SECRET=📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Application Environment Variables | |
| NODE_ENV= | |
| PORT= | |
| API_URL="" | |
| FRONTEND_URL="" | |
| DATABASE_URL="" | |
| APP_SECRET= | |
| # Application Environment Variables | |
| NODE_ENV= | |
| PORT= | |
| API_URL= | |
| FRONTEND_URL= | |
| DATABASE_URL= | |
| DATABASE_PROVIDER= | |
| APP_SECRET= | |
🧰 Tools
🪛 dotenv-linter (3.3.0)
[warning] 4-4: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
[warning] 4-4: [UnorderedKey] The API_URL key should go before the NODE_ENV key
(UnorderedKey)
[warning] 5-5: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
[warning] 5-5: [UnorderedKey] The FRONTEND_URL key should go before the NODE_ENV key
(UnorderedKey)
[warning] 6-6: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
[warning] 6-6: [UnorderedKey] The DATABASE_URL key should go before the FRONTEND_URL key
(UnorderedKey)
[warning] 7-7: [UnorderedKey] The APP_SECRET key should go before the DATABASE_URL key
(UnorderedKey)
🤖 Prompt for AI Agents
In .env.example lines 1-8, the file needs to be synced with PULL_REQUEST.md and
dotenv-linter expectations: remove the unnecessary double quotes around API_URL
and FRONTEND_URL, add a new DATABASE_PROVIDER= line (matching docs), ensure all
env entries are present (NODE_ENV, PORT, API_URL, FRONTEND_URL,
DATABASE_PROVIDER, DATABASE_URL, APP_SECRET) and finish the file with a trailing
newline; update the lines accordingly so values are blank without quotes and
append the newline at EOF.
| API_URL="" | ||
| FRONTEND_URL="" | ||
| DATABASE_URL="" | ||
| APP_SECRET= |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Require high-entropy JWT secret.
Document that APP_SECRET must be at least 256-bit random. Example generation:
- macOS/Linux:
- openssl rand -hex 32
- node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
🧰 Tools
🪛 dotenv-linter (3.3.0)
[warning] 7-7: [UnorderedKey] The APP_SECRET key should go before the DATABASE_URL key
(UnorderedKey)
🤖 Prompt for AI Agents
In .env.example around line 7, APP_SECRET currently empty; update the file to
require a high-entropy JWT secret (at least 256 bits / 32 bytes) and document
generation examples. Replace the blank value with a comment explaining the
requirement and add example CLI commands to generate a 32-byte hex secret
(openssl rand -hex 32 and node -e
"console.log(require('crypto').randomBytes(32).toString('hex'))") so developers
know to use a 64-character hex string for APP_SECRET.
| # Cryptography Settings | ||
| CRYPTO_SECRET= | ||
| CRYPTO_IV= | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Do not use a static IV in AES. Generate a random IV per encryption and store it alongside the ciphertext.
A fixed CRYPTO_IV enables IV reuse, breaking confidentiality (CBC) or authenticity (GCM). Keep CRYPTO_SECRET in env, but derive a fresh IV with crypto.randomBytes() for each operation and persist it with the payload.
🏁 Script executed:
#!/bin/bash
# Verify whether a static IV from env is used.
rg -nP -C3 'CRYPTO_IV' src
rg -nP -C3 'createCipheriv\(|createDecipheriv\(' src
rg -nP -C3 'aes-256-' src
rg -nP -C3 'getAuthTag\(|setAuthTag\(' srcLength of output: 3181
Use a random IV per encryption instead of the static CRYPTO_IV
In src/shared/infra/providers/Encrypter.provider.ts you’re currently doing
this.iv = Buffer.from(appConfigurations.CRYPTO_IV);
…
createCipheriv(this.algorithm, key, this.iv)
createDecipheriv(this.algorithm, key, this.iv)Replace that with, in encrypt(),
const iv = crypto.randomBytes(16);
const cipher = createCipheriv(this.algorithm, key, iv);
// prepend or include iv in the returned ciphertextand in decrypt(), extract the IV from the payload before calling createDecipheriv.
🧰 Tools
🪛 dotenv-linter (3.3.0)
[warning] 14-14: [UnorderedKey] The CRYPTO_IV key should go before the CRYPTO_SECRET key
(UnorderedKey)
🤖 Prompt for AI Agents
.env.example lines 12-15: the current pattern of using a static CRYPTO_IV is
insecure; update Encrypter.provider.ts to generate a random IV per encryption
and include it with the ciphertext. In encrypt(), generate a random 16-byte IV,
use it in createCipheriv, and prepend or otherwise include the IV (e.g., prefix)
in the returned encrypted payload; in decrypt(), extract the IV from the
incoming payload before calling createDecipheriv and use the extracted IV to
decrypt. Ensure key derivation/formatting stays the same and update any
consumers/tests to handle the combined IV+ciphertext format.
| WORKDIR /usr/src/corenotes-api | ||
| COPY package.json ./ | ||
| COPY package-lock.json ./ | ||
| COPY . . | ||
| EXPOSE 3333 | ||
| ENV WAIT_VERSION 2.9.0 | ||
| ADD https://github.com/ufoscout/docker-compose-wait/releases/download/$WAIT_VERSION/wait /wait | ||
| RUN chmod +x /wait | ||
|
|
||
| USER node No newline at end of file |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Non-root runtime will lack write perms; chown workdir.
You switch to USER node but never chown the app dir; npm install or writing cache/logs will fail.
COPY . .
EXPOSE 3333
ENV WAIT_VERSION 2.9.0
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/$WAIT_VERSION/wait /wait
RUN chmod +x /wait
+RUN chown -R node:node /usr/src/corenotes-api
USER node📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| WORKDIR /usr/src/corenotes-api | |
| COPY package.json ./ | |
| COPY package-lock.json ./ | |
| COPY . . | |
| EXPOSE 3333 | |
| ENV WAIT_VERSION 2.9.0 | |
| ADD https://github.com/ufoscout/docker-compose-wait/releases/download/$WAIT_VERSION/wait /wait | |
| RUN chmod +x /wait | |
| USER node | |
| WORKDIR /usr/src/corenotes-api | |
| COPY package.json ./ | |
| COPY package-lock.json ./ | |
| COPY . . | |
| EXPOSE 3333 | |
| ENV WAIT_VERSION 2.9.0 | |
| ADD https://github.com/ufoscout/docker-compose-wait/releases/download/$WAIT_VERSION/wait /wait | |
| RUN chmod +x /wait | |
| RUN chown -R node:node /usr/src/corenotes-api | |
| USER node |
🤖 Prompt for AI Agents
In Dockerfile around lines 2-11, you switch to USER node but never chown the
application directory, so the non-root user will lack write permissions; fix by
changing ownership of /usr/src/corenotes-api to the node user (chown -R
node:node /usr/src/corenotes-api) after copying files and before switching to
USER node (or alternatively run npm install as root then chown the directory),
ensuring any subsequent writes (npm install, cache, logs) succeed under the node
user.
| "crypto": "^1.0.1", | ||
| "date-fns": "^4.1.0", | ||
| "dot-env": "^0.0.1", | ||
| "dotenv": "^17.2.1", |
There was a problem hiding this comment.
Remove unsafe/typosquat packages: do not depend on npm ‘crypto’ or ‘dot-env’.
Node’s crypto is built-in; the npm ‘crypto’ and ‘dot-env’ packages are unnecessary and risky.
- "crypto": "^1.0.1",
- "dot-env": "^0.0.1",Also ensure imports use import crypto from 'node:crypto' or import * as crypto from 'crypto', and only dotenv for env loading.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "crypto": "^1.0.1", | |
| "date-fns": "^4.1.0", | |
| "dot-env": "^0.0.1", | |
| "dotenv": "^17.2.1", | |
| "date-fns": "^4.1.0", | |
| "dotenv": "^17.2.1", |
🤖 Prompt for AI Agents
In package.json around lines 46-49, remove the unsafe/typosquat dependencies
"crypto" and "dot-env" from dependencies and update the lockfile (run npm/yarn
uninstall and regen lockfile). Search the repo for imports of 'crypto' and
replace them with the built-in form (either import crypto from 'node:crypto' or
import * as crypto from 'crypto') and remove any imports of 'dot-env', ensuring
only 'dotenv' is used for env loading (and that dotenv is loaded correctly at
app entry). After changes, run tests/lint and rebuild to confirm no remaining
references to the removed packages.
| export const AllExceptionFilterSchema = z | ||
| .object({ | ||
| message: z.string().describe('Descrição do erro'), | ||
| statusCode: z.number().describe('Código do erro'), | ||
| timestamp: z.string().describe('Timestamp do erro'), | ||
| }) | ||
| .required() | ||
| .describe('Resposta de erro genérico'); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Align error DTO with actual filter output; tighten types.
- Add
path(commonly returned by error filters). - Use
.int()forstatusCode. - Use
.datetime()for ISO timestamps. - Make schema strict;
.required()is redundant here.
export const AllExceptionFilterSchema = z
.object({
- message: z.string().describe('Descrição do erro'),
- statusCode: z.number().describe('Código do erro'),
- timestamp: z.string().describe('Timestamp do erro'),
+ message: z.string().describe('Descrição do erro'),
+ statusCode: z.number().int().describe('Código do erro'),
+ timestamp: z.string().datetime().describe('Timestamp do erro (ISO-8601)'),
+ path: z.string().describe('Path da requisição'),
})
- .required()
+ .strict()
.describe('Resposta de erro genérico');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const AllExceptionFilterSchema = z | |
| .object({ | |
| message: z.string().describe('Descrição do erro'), | |
| statusCode: z.number().describe('Código do erro'), | |
| timestamp: z.string().describe('Timestamp do erro'), | |
| }) | |
| .required() | |
| .describe('Resposta de erro genérico'); | |
| export const AllExceptionFilterSchema = z | |
| .object({ | |
| message: z.string().describe('Descrição do erro'), | |
| statusCode: z.number().int().describe('Código do erro'), | |
| timestamp: z.string().datetime().describe('Timestamp do erro (ISO-8601)'), | |
| path: z.string().describe('Path da requisição'), | |
| }) | |
| .strict() | |
| .describe('Resposta de erro genérico'); |
🤖 Prompt for AI Agents
In src/shared/domain/dtos/errors/AllException.filter.dto.ts around lines 4 to
11, the DTO does not match the actual filter output and uses loose types: add a
path field (z.string()) to reflect returned error paths, change statusCode to
z.number().int(), change timestamp to z.string().datetime() (ISO), remove the
redundant .required(), and make the object strict (use .strict()) so no extra
properties are allowed; keep descriptions for each field.
| export interface EncrypterEncryptDTO { | ||
| content: string; | ||
| secret?: string; | ||
| } | ||
|
|
||
| export interface EncrypterDecryptDTO { | ||
| content: string; | ||
| secret?: string; | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Static IV in env is insecure; include IV (and auth tag) in DTOs and use per-message random IV.
PR setup references CRYPTO_IV in .env, which implies a fixed IV. For AES modes (CBC/GCM/CTR), a per-encryption random IV/nonce is mandatory; reusing IVs breaks confidentiality/integrity. Propagate IV through DTOs and store it alongside ciphertext. Prefer AES-256-GCM and persist iv + authTag.
-export interface EncrypterEncryptDTO {
- content: string;
- secret?: string;
-}
+export interface EncrypterEncryptDTO {
+ readonly content: string;
+ readonly secret?: string; // KMS/env-managed key OK, but IV must be per-message
+ readonly iv?: string; // optional override; if omitted, provider MUST generate cryptographically random IV
+ readonly aad?: string; // optional: for AES-GCM Additional Authenticated Data
+}
-export interface EncrypterDecryptDTO {
- content: string;
- secret?: string;
-}
+export interface EncrypterDecryptDTO {
+ readonly content: string; // ciphertext (e.g., base64)
+ readonly secret?: string;
+ readonly iv: string; // required to decrypt
+ readonly authTag?: string; // required if using AES-GCM
+ readonly aad?: string;
+}Additionally, define result types returned by the provider:
export interface EncrypterEncryptResultDTO {
ciphertext: string;
iv: string;
authTag?: string; // for GCM
}Verification checklist:
- Provider generates a random IV per encryption (e.g., crypto.randomBytes(12) for GCM).
- IV and authTag are stored with the ciphertext (DB columns or serialized blob).
- Decrypt path uses the stored IV/authTag; env only supplies the key, not the IV.
🏁 Script executed:
#!/bin/bash
set -e
echo "Searching for CRYPTO_IV references..."
rg -n 'CRYPTO_IV' -C3 .
echo "Searching for randomBytes usage..."
rg -n 'randomBytes' -C3 .
echo "Searching for create(Cipher|Decipher)iv usage..."
rg -n -P 'create(Cipher|Decipher)iv' -C3 .
echo "Locating Encrypter provider implementation..."
if file=$(fd --ignore-case 'Encrypter.provider.ts' 2>/dev/null); then
echo "Found: $file"
sed -n '1,200p' "$file"
else
echo "Encrypter.provider.ts not found via fd; trying find..."
file=$(find . -type f -path '*Encrypter*provider.ts' | head -n1)
if [ -n "$file" ]; then
echo "Found via find: $file"
sed -n '1,200p' "$file"
else
echo "Encrypter provider file not found"
fi
fiLength of output: 3176
Replace the static IV with a per‐encryption random IV and include the IV/authTag in your DTOs
The current provider (src/shared/infra/providers/Encrypter.provider.ts:16) reads a fixed IV from CRYPTO_IV, which breaks confidentiality when reused.
- In the provider’s encrypt method, generate a cryptographically secure random IV (e.g., crypto.randomBytes(16) or 12 bytes for GCM) for each call and, if using AES-GCM, collect the authTag.
- Update EncrypterEncryptDTO/Result to carry the IV (and authTag for GCM) alongside the ciphertext.
- Update EncrypterDecryptDTO to require the IV (and authTag) so decryption uses the stored values, with the env only supplying the secret key.
| timestamp: z.string().describe('Timestamp da verificação do HealthCheck'), | ||
| }); | ||
|
|
||
| export class HealthCheckDto extends createZodDto(HealthCheckSchema) {} |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Confirm intent: request vs response DTO.
Health check typically returns this shape; it’s unusual as a request body. Validate actual usage and consider moving to responses/* if it’s a response DTO.
🏁 Script executed:
#!/bin/bash
# Locate usages to confirm role (request/response).
rg -nP -C2 '\bHealthCheckDto\b' --type=tsLength of output: 1205
🏁 Script executed:
sed -n '1,200p' src/shared/http/controllers/shared.controller.tsLength of output: 1555
🏁 Script executed:
#!/bin/bash
sed -n '1,200p' src/shared/domain/dtos/requests/HealthCheck.request.dto.tsLength of output: 479
Move HealthCheckDto to the response DTOs folder. HealthCheckDto is exclusively used for the GET /health-check response schema, not as a request body—relocate src/shared/domain/dtos/requests/HealthCheck.request.dto.ts to src/shared/domain/dtos/responses/HealthCheck.response.dto.ts.
🤖 Prompt for AI Agents
In src/shared/domain/dtos/requests/HealthCheck.request.dto.ts around line 10,
the DTO is placed in the requests folder but is only used as a response; move it
to src/shared/domain/dtos/responses/HealthCheck.response.dto.ts. Create the new
file with the same class definition (rename file but keep class name or adjust
export if you have naming conventions), update all imports across the codebase
from the old requests path to the new responses path (including barrel/index
files), and delete the original requests file to avoid duplicate exports. Ensure
CI/import checks pass after the rename.
| @Catch() | ||
| export class AllExceptionsFilter implements ExceptionFilter { |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Verify global registration of the filter.
Ensure it’s applied (via app.useGlobalFilters or APP_FILTER).
Run:
🏁 Script executed:
#!/bin/bash
rg -nC2 'AllExceptionsFilter' src
rg -nC2 'useGlobalFilters' src/**/main.ts
rg -nC2 'APP_FILTER' srcLength of output: 24672
🏁 Script executed:
#!/bin/bash
# Broad search for global filter registration and bootstrap
echo "Searching for useGlobalFilters across entire repo"
rg -nC2 "useGlobalFilters" .
echo "Searching for APP_FILTER across entire repo"
rg -nC2 "APP_FILTER" .
echo "Searching for provider registration of APP_FILTER"
rg -nC3 "provide\s*:\s*APP_FILTER" .
echo "Locating NestFactory.create calls (bootstrapping entrypoints)"
rg -nC2 "NestFactory.create" -g "*.ts" .Length of output: 2161
Register the global exception filter
In src/app/main.ts, after const app = await NestFactory.create…, import AllExceptionsFilter and add:
app.useGlobalFilters(new AllExceptionsFilter());—or— in AppModule’s providers:
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
}🤖 Prompt for AI Agents
In src/shared/domain/errors/AllException.filter.ts around lines 9-10, the
AllExceptionsFilter is implemented but not registered globally; register it
either in src/app/main.ts by importing AllExceptionsFilter and calling
app.useGlobalFilters(new AllExceptionsFilter()) after creating the Nest app, or
add it to your AppModule providers by importing APP_FILTER and providing {
provide: APP_FILTER, useClass: AllExceptionsFilter } so the filter is applied
application-wide.
| export class BadTokenEception extends HttpException { | ||
| constructor() { | ||
| super('Token inválido ou expirado.', 401); | ||
| } | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Typo: BadTokenEception → BadTokenException; also prefer HttpStatus enum
The current name breaks imports/usages; fix the class name and use HttpStatus for clarity.
-import { HttpException } from '@nestjs/common';
+import { HttpException, HttpStatus } from '@nestjs/common';
-export class BadTokenEception extends HttpException {
+export class BadTokenException extends HttpException {
constructor() {
- super('Token inválido ou expirado.', 401);
+ super('Token inválido ou expirado.', HttpStatus.UNAUTHORIZED);
}
}🏁 Script executed:
#!/bin/bash
# Find and update all references to the misspelled class
rg -n 'BadTokenEception|BadTokenException'Length of output: 382
Rename BadTokenEception to BadTokenException and use HttpStatus.UNAUTHORIZED
- In src/shared/domain/errors/BadToken.exception.ts:
• Change class name fromBadTokenEception→BadTokenException
• Add import ofHttpStatusand replace literal401withHttpStatus.UNAUTHORIZED - In src/modules/user/http/middlewares/Auth.middleware.ts (lines 5 & 38):
• Update import toBadTokenExceptionand replace all its usages
--- a/src/shared/domain/errors/BadToken.exception.ts
+++ b/src/shared/domain/errors/BadToken.exception.ts
@@
-import { HttpException } from '@nestjs/common';
+import { HttpException, HttpStatus } from '@nestjs/common';
-export class BadTokenEception extends HttpException {
+export class BadTokenException extends HttpException {
constructor() {
- super('Token inválido ou expirado.', 401);
+ super('Token inválido ou expirado.', HttpStatus.UNAUTHORIZED);
}
}
--- a/src/modules/user/http/middlewares/Auth.middleware.ts
@@
-import { BadTokenEception } from '../../../../shared/domain/errors/BadToken.exception';
+import { BadTokenException } from '../../../../shared/domain/errors/BadToken.exception';
…
- throw new BadTokenEception();
+ throw new BadTokenException();🤖 Prompt for AI Agents
In src/shared/domain/errors/BadToken.exception.ts (lines 3–7) the class is
misspelled and uses a magic status code; rename the class from BadTokenEception
to BadTokenException, import HttpStatus from @nestjs/common, and replace the
literal 401 with HttpStatus.UNAUTHORIZED in the super call. Also update
src/modules/user/http/middlewares/Auth.middleware.ts (lines ~5 and ~38) to
import the renamed BadTokenException and replace any references to the old name.
| volumes: | ||
| - .:/usr/src/corenotes-db |
There was a problem hiding this comment.
Incorrect volume mount for MySQL container
The MySQL container volume mounts the entire project directory to /usr/src/corenotes-db, which is unnecessary and could cause issues. MySQL data should be persisted using a named volume.
volumes:
- - .:/usr/src/corenotes-db
+ - mysql_data:/var/lib/mysql
+
+volumes:
+ mysql_data:📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| volumes: | |
| - .:/usr/src/corenotes-db | |
| volumes: | |
| - mysql_data:/var/lib/mysql | |
| volumes: | |
| mysql_data: |
🤖 Prompt for AI Agents
In docker-compose.yml around lines 8 to 9, the MySQL service is mounting the
entire project directory (.-:/usr/src/corenotes-db) which is incorrect; replace
that bind mount with a named volume for MySQL data (e.g., use a named volume
mapped to MySQL’s data directory such as /var/lib/mysql) and add the named
volume under the top-level volumes section so the database files are persisted
correctly and the project directory is not exposed to the container.
| - MYSQL_USER=docker | ||
| - MYSQL_PASSWORD=docker | ||
| - MYSQL_DATABASE=corenotes_db | ||
| - MYSQL_ROOT_PASSWORD=docker |
There was a problem hiding this comment.
Using hardcoded credentials ('docker'/'docker') in the docker-compose file is a security risk. These should be externalized using environment variables or Docker secrets.
environment:
- - MYSQL_USER=docker
- - MYSQL_PASSWORD=docker
+ - MYSQL_USER=${MYSQL_USER:-docker}
+ - MYSQL_PASSWORD=${MYSQL_PASSWORD:-docker}
- MYSQL_DATABASE=corenotes_db
- - MYSQL_ROOT_PASSWORD=docker
+ - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-docker}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - MYSQL_USER=docker | |
| - MYSQL_PASSWORD=docker | |
| - MYSQL_DATABASE=corenotes_db | |
| - MYSQL_ROOT_PASSWORD=docker | |
| environment: | |
| - MYSQL_USER=${MYSQL_USER:-docker} | |
| - MYSQL_PASSWORD=${MYSQL_PASSWORD:-docker} | |
| - MYSQL_DATABASE=corenotes_db | |
| - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-docker} |
🤖 Prompt for AI Agents
In docker-compose.yml around lines 11 to 14, hardcoded DB credentials
(MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_ROOT_PASSWORD) must be
removed and replaced with environment variables or Docker secrets; update the
file to reference variables (e.g., MYSQL_USER: ${MYSQL_USER}) and add a .env
file (gitignored) to store defaults or configure Docker secrets and mount them
into the service, ensuring sensitive values are not committed to the repo and
documented for local/dev deployment.
| depends_on: | ||
| - database | ||
| - redis | ||
| command: sh -c "/wait; npm i; npx prisma generate; npx prisma migrate dev; npx prisma db seed; npm run start:dev" |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Production-unsafe command in docker-compose
Running migrations, seeds, and installing dependencies in the container startup command is inappropriate for production. This should only be used for development.
Consider creating separate docker-compose files for development and production:
docker-compose.dev.yml- with the current setupdocker-compose.yml- for production with a simpler command likenpm start
- command: sh -c "/wait; npm i; npx prisma generate; npx prisma migrate dev; npx prisma db seed; npm run start:dev"
+ command: sh -c "/wait; npm start"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| command: sh -c "/wait; npm i; npx prisma generate; npx prisma migrate dev; npx prisma db seed; npm run start:dev" | |
| command: sh -c "/wait; npm start" |
🤖 Prompt for AI Agents
In docker-compose.yml around line 34, the service command runs development-only
steps (installing deps, generating prisma client, running migrations/seeds, and
start:dev) which is unsafe for production; split this into two files by moving
the current long sh -c command into docker-compose.dev.yml for local/dev usage
and replace the command in docker-compose.yml with a production-safe start such
as npm start (or the built artifact start command), ensure environment-specific
overrides (env files or compose -f docker-compose.yml -f docker-compose.dev.yml)
are documented, and remove any runtime migration/seed/install logic from the
production compose so builds are deterministic and migrations/seeds are applied
via CI/ops pipelines.
| Since the application uses AES-256 cryptography, it's mandatory to set it's two keys. You can generate them [here](https://acte.ltd/utils/randomkeygen), then set them in the `./.env` file the following way: | ||
|
|
||
| ``` | ||
| # Cryptography Settings | ||
| CRYPTO_SECRET=(use the 'Basic 8' generated key) | ||
| CRYPTO_IV=(use the 'Basic 16' generated key) | ||
| ``` |
There was a problem hiding this comment.
AES‑256 key size guidance is wrong; will cause runtime “Invalid key length.”
AES‑256‑CBC requires a 32‑byte key and a 16‑byte IV. “Basic 8” is invalid. Provide secure generation instructions.
-Since the application uses AES-256 cryptography, it's mandatory to set it's two keys. You can generate them [here](https://acte.ltd/utils/randomkeygen), then set them in the `./.env` file the following way:
+Since the application uses AES-256-CBC, you must set a 32-byte key (CRYPTO_SECRET) and a 16-byte IV (CRYPTO_IV). Generate them securely and paste into `./.env`:
@@
-CRYPTO_SECRET=(use the 'Basic 8' generated key)
-CRYPTO_IV=(use the 'Basic 16' generated key)
+# Example (hex values recommended)
+# macOS/Linux:
+# openssl rand -hex 32 # -> CRYPTO_SECRET
+# openssl rand -hex 16 # -> CRYPTO_IV
+CRYPTO_SECRET=<64-hex-chars, 32 bytes>
+CRYPTO_IV=<32-hex-chars, 16 bytes>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Since the application uses AES-256 cryptography, it's mandatory to set it's two keys. You can generate them [here](https://acte.ltd/utils/randomkeygen), then set them in the `./.env` file the following way: | |
| ``` | |
| # Cryptography Settings | |
| CRYPTO_SECRET=(use the 'Basic 8' generated key) | |
| CRYPTO_IV=(use the 'Basic 16' generated key) | |
| ``` | |
| Since the application uses AES-256-CBC, you must set a 32-byte key (CRYPTO_SECRET) and a 16-byte IV (CRYPTO_IV). Generate them securely and paste into `./.env`: | |
| # Example (hex values recommended) | |
| # macOS/Linux: | |
| # openssl rand -hex 32 # -> CRYPTO_SECRET | |
| # openssl rand -hex 16 # -> CRYPTO_IV | |
| CRYPTO_SECRET=<64-hex-chars, 32 bytes> | |
| CRYPTO_IV=<32-hex-chars, 16 bytes> |
🤖 Prompt for AI Agents
In README.md around lines 40 to 46, the AES-256 key guidance is incorrect (it
suggests a “Basic 8” key) which will cause “Invalid key length.” Update the
instructions to require a 32‑byte key and a 16‑byte IV for AES-256-CBC and show
how to securely generate them (e.g., using a cryptographic random generator such
as openssl or a secure online generator), specifying acceptable encodings (hex
or base64) and the exact expected lengths (e.g., 32 raw bytes — 64 hex chars —
for CRYPTO_SECRET; 16 raw bytes — 32 hex chars — for CRYPTO_IV), and instruct
users to place those generated values into the .env CRYPTO_SECRET and CRYPTO_IV
entries.
| npx prisma generate | ||
|
|
||
| npx prisma migrate <migration-name> | ||
|
|
||
| npm run start:dev | ||
| ``` |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Fix Prisma migrate command and include seeding.
The correct dev command is migrate dev with a name; optionally seed after.
-npx prisma generate
-
-npx prisma migrate <migration-name>
-
-npm run start:dev
+npx prisma generate
+npx prisma migrate dev --name init
+npx prisma db seed
+npm run start:dev📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| npx prisma generate | |
| npx prisma migrate <migration-name> | |
| npm run start:dev | |
| ``` | |
| npx prisma generate | |
| npx prisma migrate dev --name init | |
| npx prisma db seed | |
| npm run start:dev |
🤖 Prompt for AI Agents
In README.md around lines 71–76, the migrate instruction is incorrect and
missing seeding; replace the generic "prisma migrate <migration-name>" step with
the dev migration flow using "prisma migrate dev" with an explicit migration
name, and add a follow-up seed step so users run the project seed after the
migration (e.g., run the project's configured prisma db seed or npm script that
performs seeding). Ensure the README shows the migration command with a required
name and a separate step to run the seed.
| validateEmail, | ||
| validateName, | ||
| validatePassword, | ||
| } from '../../../../shared/infra/utils/functions/validators'; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Verify validators barrel import exists.
Ensure shared/infra/utils/functions/validators/index.ts re-exports validateName/Email/Password; otherwise this import will fail.
🏁 Script executed:
#!/bin/bash
# Check that a validators barrel exists and re-exports the functions
fd -a validators src/shared/infra/utils/functions
rg -nC1 'export\s+\{\s*validate(Name|Email|Password)' src/shared/infra/utils/functions/validators 2>/dev/null || true
rg -n 'export\s+const\s+validate(Name|Email|Password)' src/shared/infra/utils/functions -g '!**/node_modules/**'Length of output: 709
Ensure validators barrel file re-exports functions
Add or update src/shared/infra/utils/functions/validators/index.ts to export validateEmail, validateName, and validatePassword; without these re-exports the import in create-user.usecase.ts will fail.
🤖 Prompt for AI Agents
In src/modules/user/infra/usecases/create-user.usecase.ts around lines 12 to 15,
the import expects validateEmail, validateName, and validatePassword to be
re-exported from src/shared/infra/utils/functions/validators; update or create
src/shared/infra/utils/functions/validators/index.ts to export these three
functions (re-export the named exports from their original module files using
the correct relative paths), ensure the functions are exported as named exports
(not default), and run a TypeScript build or linter to confirm the imports
resolve.
| ): Promise<HomeDataResponseDTO | UserNotFoundException> { | ||
| const user = await this.userRepository.findById(user_id); | ||
|
|
||
| if (!user || !user.id_user || !user.name) | ||
| return new UserNotFoundException(); |
There was a problem hiding this comment.
Don’t return exception objects; throw them and narrow the return type.
Returning new UserNotFoundException() can lead to 200 responses with an error payload.
- async execute(
- user_id: string,
- ): Promise<HomeDataResponseDTO | UserNotFoundException> {
+ async execute(user_id: string): Promise<HomeDataResponseDTO> {
@@
- if (!user || !user.id_user || !user.name)
- return new UserNotFoundException();
+ if (!user || !user.id_user || !user.name) {
+ throw new UserNotFoundException();
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ): Promise<HomeDataResponseDTO | UserNotFoundException> { | |
| const user = await this.userRepository.findById(user_id); | |
| if (!user || !user.id_user || !user.name) | |
| return new UserNotFoundException(); | |
| async execute(user_id: string): Promise<HomeDataResponseDTO> { | |
| const user = await this.userRepository.findById(user_id); | |
| if (!user || !user.id_user || !user.name) { | |
| throw new UserNotFoundException(); | |
| } |
🤖 Prompt for AI Agents
In src/modules/user/infra/usecases/home-data.usecase.ts around lines 15 to 19,
the code currently returns new UserNotFoundException() when the user is missing;
change this to throw new UserNotFoundException() so an error response is
produced instead of a 200 with an error payload, and update the method signature
to remove UserNotFoundException from the Promise union (e.g.,
Promise<HomeDataResponseDTO>), allowing the exception to propagate (or be caught
by upper layers) and adjust any callers/tests to expect the thrown exception
rather than an error object being returned.
|
|
||
| async execute( | ||
| data: UserLoginRequestDTO, | ||
| ip: string, |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Unused ip parameter in login method
The ip parameter is accepted but never used in the implementation. If IP tracking is planned for audit logging or rate limiting, implement it now. Otherwise, remove the parameter to avoid confusion.
async execute(
data: UserLoginRequestDTO,
- ip: string,
): Promise<📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ip: string, | |
| async execute( | |
| data: UserLoginRequestDTO, | |
| ): Promise< |
🤖 Prompt for AI Agents
In src/modules/user/infra/usecases/user-login.usecase.ts around line 21, the
login method declares an ip: string parameter that is never used; either remove
the ip parameter from the method signature and all call sites (and update
related types/interfaces) or implement its use (e.g., pass it into
audit/logging, attach to the created session/token, or feed it into
rate-limiting/auth-logging code) so it is consumed; update tests and types
accordingly to keep signatures consistent.
| const token = this.jwtProvider.generate({ | ||
| payload: { | ||
| user: { | ||
| id_user: user.id_user, | ||
| name: user.name, | ||
| }, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Consider adding expiration time to JWT tokens
The JWT generation doesn't specify an expiration time, which could lead to tokens that never expire. This is a security concern as compromised tokens would remain valid indefinitely.
const token = this.jwtProvider.generate({
payload: {
user: {
id_user: user.id_user,
name: user.name,
},
},
+ expiresIn: '24h', // or configure via environment variable
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const token = this.jwtProvider.generate({ | |
| payload: { | |
| user: { | |
| id_user: user.id_user, | |
| name: user.name, | |
| }, | |
| }, | |
| }); | |
| const token = this.jwtProvider.generate({ | |
| payload: { | |
| user: { | |
| id_user: user.id_user, | |
| name: user.name, | |
| }, | |
| }, | |
| expiresIn: '24h', // or configure via environment variable | |
| }); |
🤖 Prompt for AI Agents
In src/modules/user/infra/usecases/user-login.usecase.ts around lines 40 to 47,
the JWT is generated without an expiration time; modify the call to
this.jwtProvider.generate to include an expiration (use a configurable value,
e.g., process.env.JWT_EXPIRES_IN or a constant like '1h'), either by adding an
expiresIn option to the generate call or by including an exp claim set to
Date.now()/Math.floor + TTL, depending on the jwtProvider API; ensure the value
is configurable via env and update any types/signatures if needed.
| REDIS_PRODUCTION_HOST: process.env.REDIS_PRODUCTION_HOST | ||
| ? process.env.REDIS_PRODUCTION_HOST | ||
| : '', | ||
| REDIS_PRODUCTION_PORT: process.env.REDIS_PRODUCTION_PORT | ||
| ? Number(process.env.REDIS_PRODUCTION_PORT) | ||
| : 0, | ||
| REDIS_PRODUCTION_USER: process.env.REDIS_PRODUCTION_USER | ||
| ? process.env.REDIS_PRODUCTION_USER | ||
| : '', | ||
| REDIS_PRODUCTION_PASSWORD: process.env.REDIS_PRODUCTION_PASSWORD | ||
| ? process.env.REDIS_PRODUCTION_PASSWORD | ||
| : '', | ||
|
|
There was a problem hiding this comment.
Don’t coerce absent optional Redis vars to ''/0 (can trigger bad connections).
Pass undefined when envs are unset so optional fields remain absent.
- REDIS_PRODUCTION_HOST: process.env.REDIS_PRODUCTION_HOST
- ? process.env.REDIS_PRODUCTION_HOST
- : '',
- REDIS_PRODUCTION_PORT: process.env.REDIS_PRODUCTION_PORT
- ? Number(process.env.REDIS_PRODUCTION_PORT)
- : 0,
- REDIS_PRODUCTION_USER: process.env.REDIS_PRODUCTION_USER
- ? process.env.REDIS_PRODUCTION_USER
- : '',
- REDIS_PRODUCTION_PASSWORD: process.env.REDIS_PRODUCTION_PASSWORD
- ? process.env.REDIS_PRODUCTION_PASSWORD
- : '',
+ REDIS_PRODUCTION_HOST: process.env.REDIS_PRODUCTION_HOST || undefined,
+ REDIS_PRODUCTION_PORT: process.env.REDIS_PRODUCTION_PORT
+ ? Number(process.env.REDIS_PRODUCTION_PORT)
+ : undefined,
+ REDIS_PRODUCTION_USER: process.env.REDIS_PRODUCTION_USER || undefined,
+ REDIS_PRODUCTION_PASSWORD: process.env.REDIS_PRODUCTION_PASSWORD || undefined,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| REDIS_PRODUCTION_HOST: process.env.REDIS_PRODUCTION_HOST | |
| ? process.env.REDIS_PRODUCTION_HOST | |
| : '', | |
| REDIS_PRODUCTION_PORT: process.env.REDIS_PRODUCTION_PORT | |
| ? Number(process.env.REDIS_PRODUCTION_PORT) | |
| : 0, | |
| REDIS_PRODUCTION_USER: process.env.REDIS_PRODUCTION_USER | |
| ? process.env.REDIS_PRODUCTION_USER | |
| : '', | |
| REDIS_PRODUCTION_PASSWORD: process.env.REDIS_PRODUCTION_PASSWORD | |
| ? process.env.REDIS_PRODUCTION_PASSWORD | |
| : '', | |
| REDIS_PRODUCTION_HOST: process.env.REDIS_PRODUCTION_HOST || undefined, | |
| REDIS_PRODUCTION_PORT: process.env.REDIS_PRODUCTION_PORT | |
| ? Number(process.env.REDIS_PRODUCTION_PORT) | |
| : undefined, | |
| REDIS_PRODUCTION_USER: process.env.REDIS_PRODUCTION_USER || undefined, | |
| REDIS_PRODUCTION_PASSWORD: process.env.REDIS_PRODUCTION_PASSWORD || undefined, |
🤖 Prompt for AI Agents
In src/shared/config/app.config.ts around lines 57 to 69, optional Redis env
vars are being coerced to '' or 0 when unset; change them to return undefined
instead so optional fields remain absent. For host, user, and password, assign
process.env.VAR directly or use process.env.VAR ?? undefined; for port, parse
Number(process.env.REDIS_PRODUCTION_PORT) only when the env var is set,
otherwise return undefined (avoid coercing to 0). Ensure the config types remain
compatible with possibly undefined values.
Desafio Corelab - Solução
Esta PR contém a solução do desafio para a vaga de Dev Full-stack da Corelab, seguindo os requisitos solicitados para o desenvolvimento de uma aplicação full-stack de anotações.
Features
cryptodo Node.js. Sempre que um dado é solicitado, ele é descriptografado em tempo real, garantindo a segurança das informações, e é sempre criptografado ao ser salvo no banco de dados;JWT, que gera tokens baseados em dados do próprio usuário. Os UseCases de cada implementação garantem que somente o usuário dono daquele item possa acessá-lo, e respostas negativas da API são dadas em caso de tentativa de trazer dados indevidos.zodenestjs-zodpara documentar cada tipo de informação esperada em cada endpoint, e o objeto retornado nos casos bem e mau-sucedidos;JesteSupertest(esta última para testar os métodos dos controllers da aplicação);cache-managere@nestjs/cache-manager, utilizando um banco de dados Redis em ambiente local ou de produção para armazenar alguns dados de forma a evitar stress na aplicação por muitas requisições;Tecnologias Utilizadas
Back-end:
Front-end:
Rodando a aplicação
Rodando - clonagem + configuração das variáveis de ambiente
Efetue a clonagem dos dois repositórios, front-end e back-end em uma única pasta, de modo a manter a estrutura mais limpa.
Na pasta do back-end, crie um arquivo
.envcom as variáveis de ambiente necessárias (veja o arquivo.env.examplepara referência).Para o app rodar corretamente, é necessário gerar duas chaves de criptografia para o AES-256 e inseri-las como as variáveis
CRYPTO_SECRETeCRYPTO_IV. Neste link é possível gerá-las de modo randômico. Após gerá-las, coloque a "Basic 16" na variávelCRYPTO_IVe a "Encryption key 256" na variávelCRYPTO_SECRET.Para rodar o Cache com o Redis, é preciso gerar um container via Docker ou utilizar um banco da própria Redis Cloud. Caso deseje utilizar via Docker Desktop, copie o comando abaixo para a criação de um novo container:
Por padrão, o banco ficará como o host
localhoste a porta6379, e somente isso é necessário para poder rodar ele com o caching local. Caso deseje utilizar um banco de dados Redis em produção, será necessário alterar as variáveis de ambienteREDIS_PRODUCTION_HOST,REDIS_PRODUCTION_PORT,REDIS_PRODUCTION_USEReREDIS_PRODUCTION_PASSWORDno arquivo.envcom os dados fornecidos pela Redis Cloud ou outro provedor do banco.O Container do Redis vem junto com o Redis-Insight, que permite visualizar os dados armazenados no cache da aplicação. Para acessá-lo, basta abrir o navegador e ir até a URL
http://localhost:8001.A aplicação depende de pelo menos um banco de dados MySQL para ser rodada. Para isso, basta utilizar o Shell do MySQL ou um cliente de banco de dados para criar o banco de dados
corenotes_db, e inserir os dados de autenticação. Após criar o banco, copie a sua string de conexão na variávelDATABASE_URL.Caso queira rodar os testes unitários, crie um segundo banco de dados
corenotes_db_teste copie a sua string de conexão na variávelSHADOW_DATABASE_URLpara o Prisma poder reconhecê-lo na hora de rodar os testes dos endpoints da API.Por fim, é necessário preencher algumas variáveis de ambiente para o Nest e os providers utilizarem. Utilize o valor "development" ou "local" para a chave
NODE_ENV, o valor "mysql" para a chaveDATABASE_PROVIDER, uma porta de quatro dígitos para a chavePORT, a url da API e do front-end nas chavesAPI_URLeFRONTEND_URLe a chave para a criação de JWTs na variávelAPP_SECRET. Por segurança, pode-se usar uma das chaves geradas no link acima.Ao fim da configuração, o arquivo
.envdeve ficar parecido com o abaixo:Por fim, no repositório do front-end, copie o arquivo
.env.example, altere seu nome para.enve mude o valor da variávelNEXT_PUBLIC_APIpara a URL da API montada via configuração manual ou via Docker (próximo passo).Rodando com Docker
Ambos os repositórios já vêm pré-configurados com as informações do Docker que serão utilizadas para criar os containers. Para isso, basta rodar o comando
npm run docker:upoudocker-compose upna root de cada um dos projetos, e os containers serão iniciados automaticamente.(OBS: Lembre-se de que é necessário ter o Docker instalado e em execução na sua máquina.)
(OBS 2: Caso esteja utilizando Windows, o Docker pode confundir o localhost da url do banco criado em container com o localhost do seu sistema. Fechar o processo do mysql no Gerenciador de Tarefas pode ajudar a resolver esse problema.)
Por padrão, a API será aberta na porta 3311. Basta utilizar a URL
http://localhost:3311/swaggerpara acessar a documentação.Rodando localmente
Após a configuração, rode o comando
npm installpara instalar as dependências do projeto. Após isso, rode o comandonpx prisma migrate dev, e insira o nome da migration para que o banco seja sincronizado com o modelo das entidades do arquivoschema.prisma. Após isso, rode o comandonpx prisma db seedpara rodar o seed de um usuário e três notas padrão (vê-los na pasta./src\shared\infra\db\prisma\seeders).Por fim, rode o comando
npm run start:devpara iniciar a aplicação em modo de desenvolvimento, e assim que a aplicação iniciar, é possível acessar sua documentação através da URLhttp://localhost:5000/swagger.No repositório do front-end, rode o comando
npm install, e em seguidanpm run devpara iniciar a aplicação front-end.Vídeo
Para assistir o vídeo mostrando a execução do projeto integrado, clique aqui!
Summary by CodeRabbit
New Features
Refactor
Chores
Documentation