Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
NODE_ENV=
PORT=

JWT_SECRET=""

DATABASE_URL=""
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ pnpm-debug.log*

# Environment variables
.env
.env.*

# Logs
logs/
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,21 @@
"author": "",
"license": "ISC",
"dependencies": {
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.0.1",
"@fastify/jwt": "^9.1.0",
"@prisma/client": "6.10.1",
"@types/bcryptjs": "^3.0.0",
"bcryptjs": "^3.0.2",
"fastify": "^5.4.0",
"zod": "^3.25.67"
},
"devDependencies": {
"@biomejs/biome": "2.1.1",
"@faker-js/faker": "^9.9.0",
"@types/node": "^24.0.4",
"@vitest/coverage-v8": "3.2.4",
"@vitest/ui": "3.2.4",
"biome": "^0.3.3",
"prisma": "^6.10.1",
"tsup": "^8.5.0",
"tsx": "^4.20.3",
Expand Down
905 changes: 197 additions & 708 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/@types/fastify-jwt.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import "@fastify/jwt"

declare module "@fastify/jwt" {
interface FastifyJWT {
user: {
sub: string,
}
}
}
3 changes: 2 additions & 1 deletion src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { z } from 'zod/v4';
const envSchema = z.object({
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(['dev', 'test', 'prod']).default('prod'),
DATABASE_URL: z.string()
DATABASE_URL: z.string(),
JWT_SECRET: z.string(),
});

const _env = envSchema.safeParse(process.env)
Expand Down
26 changes: 23 additions & 3 deletions src/http/controllers/orgs/authOrgsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,30 @@ export const authOrgsController = async (
try
{
const authOrgsUseCase = makeAuthOrgsUseCase();
await authOrgsUseCase.execute(bodyData);
const { org } = await authOrgsUseCase.execute(bodyData);

const token = await reply.jwtSign({}, {
sign: {
sub: org.id
}
})

const refreshToken = await reply.jwtSign({}, {
sign: {
sub: org.id,
expiresIn: '7d'
}
})

reply
.setCookie('refreshToken', refreshToken, {
path: '/',
secure: true,
sameSite: true,
httpOnly: true,
})
.status(200)
.send({ token });
} catch (error)
{
if (error instanceof InvalidCredentialsError)
Expand All @@ -28,6 +50,4 @@ export const authOrgsController = async (

throw error;
}

reply.status(200).send();
}
15 changes: 6 additions & 9 deletions src/http/controllers/orgs/getOrgsProfileController.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
import { ResourceNotFoundError } from '@/use-cases/errors/resourceNotFound';
import { makeAuthOrgsUseCase } from "@/use-cases/factories/makeAuthOrgsUseCase";
import { makeGetOrgsProfileUseCase } from "@/use-cases/factories/makeGetProfileOrgsUseCase";
import { FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod/v4";

export const getOrgsProfileController = async (
request: FastifyRequest,
reply: FastifyReply,
) => {
const getOrgsProfileBodySchema = z.object({
orgId: z.string()
});

const paramsData = getOrgsProfileBodySchema.parse(request.params);
const orgId = request.user.sub;

try
{
const getOrgsProfileUseCase = makeGetOrgsProfileUseCase();
const { org } = await getOrgsProfileUseCase.execute(paramsData);
const { org } = await getOrgsProfileUseCase.execute({ orgId });

reply.status(200).send(org);
reply.status(200).send({
...org,
password: undefined
});
} catch (error)
{
if (error instanceof ResourceNotFoundError)
Expand Down
7 changes: 6 additions & 1 deletion src/http/controllers/orgs/orgsRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import { FastifyInstance } from "fastify";
import { createOrgsController } from "./createOrgsController";
import { authOrgsController } from "./authOrgsController";
import { getOrgsProfileController } from "./getOrgsProfileController";
import { authMiddleware } from "@/http/middlewares/authMiddleware";
import { RefreshOrgsController } from "./refreshOrgsController";

export const orgsRoutes = async (app: FastifyInstance) => {
app.post('/orgs', createOrgsController);

app.post('/orgs/auth', authOrgsController);
app.get('/orgs/:orgId', getOrgsProfileController);
app.patch('/orgs/refresh', RefreshOrgsController);

app.get('/orgs/profile', { onRequest: [authMiddleware] }, getOrgsProfileController);
}
43 changes: 43 additions & 0 deletions src/http/controllers/orgs/refreshOrgsController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { InvalidCredentialsError } from "@/use-cases/errors/invalidCredentialsError";
import type { FastifyReply, FastifyRequest } from "fastify";

export const RefreshOrgsController = async (
request: FastifyRequest,
reply: FastifyReply,
) => {
await request.jwtVerify({ onlyCookie: true });

try
{
const token = await reply.jwtSign({}, {
sign: {
sub: request.user.sub
}
})

const refreshToken = await reply.jwtSign({}, {
sign: {
sub: request.user.sub,
expiresIn: '7d'
}
})

reply
.setCookie('refreshToken', refreshToken, {
path: '/',
secure: true,
sameSite: true,
httpOnly: true,
})
.status(200)
.send({ token });
} catch (error)
{
if (error instanceof InvalidCredentialsError)
{
return reply.status(400).send(error.message);
}

throw error;
}
}
8 changes: 6 additions & 2 deletions src/http/controllers/pets/createPetsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ export const createPetsController = async (
request: FastifyRequest,
reply: FastifyReply,
) => {
const orgId = request.user.sub;

const createOrgsBodySchema = z.object({
idOrg: z.string(),
name: z.string(),
about: z.string(),
age: z.string(),
Expand All @@ -22,7 +23,10 @@ export const createPetsController = async (
try
{
const createOrgsUseCase = makeCreatePetsUseCase();
await createOrgsUseCase.execute(bodyData);
await createOrgsUseCase.execute({
...bodyData,
orgId
});

} catch (error)
{
Expand Down
3 changes: 2 additions & 1 deletion src/http/controllers/pets/petsRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { FastifyInstance } from "fastify";
import { createPetsController } from "./createPetsController";
import { getPetsController } from "./getPetsController";
import { listPetsController } from "./listPetsController";
import { authMiddleware } from "@/http/middlewares/authMiddleware";


export const petsRoutes = async (app: FastifyInstance) => {
app.post('/pets', createPetsController);
app.post('/pets', { onRequest: [authMiddleware] }, createPetsController);
app.get('/pets/:id', getPetsController);
app.get('/pets', listPetsController);
}
14 changes: 14 additions & 0 deletions src/http/middlewares/authMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { FastifyReply, FastifyRequest } from "fastify";

export const authMiddleware = async (
request: FastifyRequest,
reply: FastifyReply
) => {
try
{
await request.jwtVerify();
} catch (error)
{
return reply.status(401).send({ message: 'Acesso não Autorizado, token inválido.' });
}
}
19 changes: 17 additions & 2 deletions src/http/server.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
import fastify from "fastify";
import { appRoutes } from "./routes";
import { fastifyCors } from '@fastify/cors';
import { env } from "@/env";
import fastifyCookie from "@fastify/cookie";
import fastifyJwt from "@fastify/jwt";
import { ZodError } from "zod/v4";
import { env } from "@/env";

import { appRoutes } from "./routes";

export const app = fastify();

app.register(fastifyCors, { origin: '*' });
app.register(fastifyCookie);
app.register(fastifyJwt, {
secret: env.JWT_SECRET,
cookie: {
cookieName: 'refreshToken',
signed: false,
},
sign: {
expiresIn: '10m'
}
});

app.register(appRoutes);

app.setErrorHandler((error, _, reply) => {
Expand Down
54 changes: 17 additions & 37 deletions src/use-cases/pets/createPetsUseCase.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,31 @@ import { CreatePetsUseCase } from './createPetsUseCase';
import { PetsRepositoryInMemory } from '@/repositories/in-memory/petsRepositoryInMemory';
import { OrgsRepositoryInMemory } from '@/repositories/in-memory/orgsRepositoryInMemory';
import { OrgNotFoundError } from '../errors/orgNotFoundError';
import { makeOrg } from '../factories/makeOrg.factory';

describe("Caso de Uso: Criação de Pets", () => {
let orgsRepository: OrgsRepositoryInMemory;
let petsRepository: PetsRepositoryInMemory;
let sut: CreatePetsUseCase;

let idOrg: string;

beforeEach(async () => {
petsRepository = new PetsRepositoryInMemory();
orgsRepository = new OrgsRepositoryInMemory();

petsRepository = new PetsRepositoryInMemory(orgsRepository);
sut = new CreatePetsUseCase(petsRepository, orgsRepository);

const org = await orgsRepository.create({
name: "Org Test",
author_name: "John Doe",
email: "john@org.com",
whatsapp: "123456789",
password: "123456",
cep: "12345-000",
state: "SP",
city: "São Paulo",
neighborhood: "Centro",
street: "Rua A",
latitude: -23.5,
longitude: -46.6,
});

idOrg = org.id;
})

it("Deve ser possivel cadastrar um Pet", async () => {
const org = await orgsRepository.create(makeOrg());

const input = {
const { pet } = await sut.execute({
name: 'Alfredo',
about: 'Cachoro',
age: '12',
size: 'Grande',
energy_level: '2',
environment: 'asdf',
idOrg: idOrg,
};

const { pet } = await sut.execute(input);
orgId: org.id
});

expect(pet).toHaveProperty('id')
expect(pet.id).toEqual(expect.any(String))
Expand All @@ -56,17 +36,17 @@ describe("Caso de Uso: Criação de Pets", () => {

it("Não deve ser possivel cadastrar um Pet para uma organização inexistente", async () => {

const input = {
name: 'Alfredo',
about: 'Cachoro',
age: '12',
size: 'Grande',
energy_level: '2',
environment: 'asdf',
idOrg: 'non-existing-id',
};

await expect(sut.execute(input)).rejects.instanceOf(OrgNotFoundError);
await expect(
sut.execute({
name: 'Alfredo',
about: 'Cachoro',
age: '12',
size: 'Grande',
energy_level: '2',
environment: 'asdf',
orgId: 'non-existing-id',
})
).rejects.instanceOf(OrgNotFoundError);
});

});
8 changes: 4 additions & 4 deletions src/use-cases/pets/createPetsUseCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Pets } from "prisma/generated/prisma";
import { OrgNotFoundError } from "../errors/orgNotFoundError";

type CreatePetsCaseRequest = {
idOrg: string;
orgId: string;
name: string;
about: string;
age: string;
Expand All @@ -26,18 +26,18 @@ export class CreatePetsUseCase {
) { }

async execute({
idOrg,
orgId,
...data
}: CreatePetsCaseRequest): Promise<CreatePetsCaseResponse> {
const org = await this.orgsRepository.findById(idOrg)
const org = await this.orgsRepository.findById(orgId)
if (!org)
{
throw new OrgNotFoundError()
}

const pet = await this.petsRepository.create({
...data,
org_id: idOrg,
org_id: orgId,
})

return { pet };
Expand Down