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
2 changes: 1 addition & 1 deletion jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module.exports = {
global: {
lines: 85,
statements: 85,
branches: 80,
branches: 85,
functions: 85,
},
},
Expand Down
92 changes: 92 additions & 0 deletions src/modules/perfil-social/perfil-social.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { NextFunction, Request, Response } from "express";

import { CodigoDeErro } from "@/shared/errors/codigos-de-erro";
import { ErroAplicacao } from "@/shared/errors/erro-aplicacao";

import type { PerfilSocialService } from "./perfil-social.service";

type PerfilSocialParams = {
usuarioId: string;
};

type ListarAmigosQuery = {
page?: number;
limit?: number;
nome?: string;
nickname?: string;
};

type RequisicaoAutenticada = Pick<Request, "usuario" | "headers">;

export class PerfilSocialController {
constructor(private readonly perfilSocialService: PerfilSocialService) {}

listarAmigos = async (
request: Request<unknown, unknown, unknown, ListarAmigosQuery>,
response: Response,
next: NextFunction,
) => {
try {
const usuario = this.obterUsuario(request);
const authorization = this.obterAuthorization(request);
const resultado = await this.perfilSocialService.listarAmigosSociais(
usuario,
authorization,
request.query,
);

return response.status(200).json(resultado);
} catch (error) {
return next(error);
}
};

buscarPerfil = async (
request: Request<PerfilSocialParams>,
response: Response,
next: NextFunction,
) => {
try {
const usuario = this.obterUsuario(request);
const authorization = this.obterAuthorization(request);
const perfil = await this.perfilSocialService.buscarPerfilSocial(
usuario,
authorization,
request.params.usuarioId,
);

return response.status(200).json({
mensagem: "Perfil social encontrado.",
dados: perfil,
});
} catch (error) {
return next(error);
}
};

private obterUsuario(request: RequisicaoAutenticada) {
if (!request.usuario?.id) {
throw new ErroAplicacao({
codigoStatus: 401,
codigo: CodigoDeErro.NAO_AUTORIZADO,
mensagem: "Usuario nao autenticado.",
});
}

return request.usuario;
}

private obterAuthorization(request: RequisicaoAutenticada): string {
const authorization = request.headers.authorization;

if (!authorization) {
throw new ErroAplicacao({
codigoStatus: 401,
codigo: CodigoDeErro.NAO_AUTORIZADO,
mensagem: "Token de acesso ausente.",
});
}

return authorization;
}
}
31 changes: 31 additions & 0 deletions src/modules/perfil-social/perfil-social.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Router } from "express";
import { z } from "zod";

import { middlewareAutenticacao } from "@/shared/middlewares/autenticacao.middleware";

import { PerfilSocialController } from "./perfil-social.controller";
import { PerfilSocialService } from "./perfil-social.service";

const perfilSocialService = new PerfilSocialService();
const perfilSocialController = new PerfilSocialController(perfilSocialService);

const perfilSocialRouter = Router();

perfilSocialRouter.use(middlewareAutenticacao);

perfilSocialRouter.get("/:usuarioId/social", (request, response, next) => {
const resultado = z.string().trim().min(1).safeParse(request.params.usuarioId);

if (!resultado.success) {
return response.status(400).json({
erro: {
codigo: "REQUISICAO_INVALIDA",
mensagem: "Identificador do usuario invalido.",
},
});
}

return perfilSocialController.buscarPerfil(request, response, next);
});

export { perfilSocialController, perfilSocialRouter };
154 changes: 154 additions & 0 deletions src/modules/perfil-social/perfil-social.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { env } from "@/config/env";
import { backendClient } from "@/shared/clients/backend.client";
import { quizClient } from "@/shared/clients/quiz.client";
import { CodigoDeErro } from "@/shared/errors/codigos-de-erro";
import { ErroAplicacao } from "@/shared/errors/erro-aplicacao";

import type {
DadosSociaisQuiz,
PerfilSocial,
RespostaAmizades,
ResumoAmigo,
ResumoAmigoSocial,
UsuarioRequisicao,
} from "./perfil-social.types";

type ListarAmigosQuery = {
page?: number;
limit?: number;
nome?: string;
nickname?: string;
};

type RespostaMapa<T> = {
dados: Record<string, T[]>;
};

const LIMITE_BUSCA_AMIZADE = 100;

export class PerfilSocialService {
async listarAmigosSociais(
usuario: UsuarioRequisicao,
authorization: string,
query: ListarAmigosQuery,
) {
const headers = this.montarHeaders(usuario, authorization);
const { data: amizades } = await backendClient.get<RespostaAmizades>("/api/v1/amizade", {
params: query,
headers,
});

const usuarioIds = amizades.dados.map((amizade) => amizade.amigo.id);
const sociais = await this.buscarDadosSociais(usuarioIds, headers);

const dados: ResumoAmigoSocial[] = amizades.dados.map((amizade) => ({
...amizade,
cosmeticos: sociais.cosmeticos[amizade.amigo.id] ?? [],
conquistasDestacadas: sociais.destaques[amizade.amigo.id] ?? [],
}));

return {
dados,
metadados: amizades.metadados,
};
}

async buscarPerfilSocial(
usuario: UsuarioRequisicao,
authorization: string,
usuarioId: string,
): Promise<PerfilSocial> {
const headers = this.montarHeaders(usuario, authorization);
const amigo = await this.buscarAmigoConfirmado(usuarioId, headers);

if (!amigo) {
throw new ErroAplicacao({
codigoStatus: 404,
codigo: CodigoDeErro.NAO_ENCONTRADO,
mensagem: "Perfil social nao encontrado entre as amizades confirmadas.",
});
}

const sociais = await this.buscarDadosSociais([usuarioId], headers);

return {
usuario: amigo,
cosmeticos: sociais.cosmeticos[usuarioId] ?? [],
conquistasDestacadas: sociais.destaques[usuarioId] ?? [],
};
}

private async buscarAmigoConfirmado(
usuarioId: string,
headers: Record<string, string>,
): Promise<ResumoAmigo | null> {
let page = 1;
let totalPages = 1;

do {
const { data } = await backendClient.get<RespostaAmizades>("/api/v1/amizade", {
params: {
page,
limit: LIMITE_BUSCA_AMIZADE,
},
headers,
});

const amizade = data.dados.find((item) => item.amigo.id === usuarioId);

if (amizade) {
return amizade.amigo;
}

totalPages = data.metadados.totalPages;
page += 1;
} while (page <= totalPages);

return null;
}

private async buscarDadosSociais(
usuarioIds: string[],
headers: Record<string, string>,
): Promise<DadosSociaisQuiz> {
if (usuarioIds.length === 0) {
return {
cosmeticos: {},
destaques: {},
};
}

const ids = usuarioIds.join(",");
const [cosmeticos, destaques] = await Promise.all([
quizClient.get<RespostaMapa<DadosSociaisQuiz["cosmeticos"][string][number]>>(
"/api/v1/inventario/usuarios/equipados",
{
params: { usuarioIds: ids },
headers,
},
),
quizClient.get<RespostaMapa<DadosSociaisQuiz["destaques"][string][number]>>(
"/api/v1/conquistas/usuarios/destaques",
{
params: { usuarioIds: ids },
headers,
},
),
]);

return {
cosmeticos: cosmeticos.data.dados,
destaques: destaques.data.dados,
};
}

private montarHeaders(usuario: UsuarioRequisicao, authorization: string): Record<string, string> {
return {
Authorization: authorization,
"x-internal-token": env.INTERNAL_TOKEN,
"x-user-id": usuario.id,
"x-user-papel": usuario.papel,
"x-user-status": usuario.status,
};
}
}
67 changes: 67 additions & 0 deletions src/modules/perfil-social/perfil-social.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { MetadadosPaginacao } from "@/shared/types/api.types";

export type UsuarioRequisicao = {
id: string;
papel: string;
status: string;
};

export type ResumoAmigo = {
id: string;
nome: string;
nickname: string | null;
curso: string | null;
semestre: string | null;
};

export type ResumoAmizade = {
id: string;
statusAmizade: string;
amigo: ResumoAmigo;
};

export type RespostaAmizades = {
dados: ResumoAmizade[];
metadados: MetadadosPaginacao;
};

export type ItemCosmetico = {
id: string;
codigo: string;
nome: string;
descricao: string | null;
tipo: string;
valor: string | null;
imagemUrl: string | null;
previewImagemUrl: string | null;
};

export type ConquistaDestaque = {
desbloqueioId: string;
conquistaId: string;
nome: string;
descricao: string;
tier: string;
tipoConquista: string;
tema: {
id: string;
nome: string;
} | null;
conquistadoEm: string;
};

export type DadosSociaisQuiz = {
cosmeticos: Record<string, ItemCosmetico[]>;
destaques: Record<string, ConquistaDestaque[]>;
};

export type PerfilSocial = {
usuario: ResumoAmigo;
cosmeticos: ItemCosmetico[];
conquistasDestacadas: ConquistaDestaque[];
};

export type ResumoAmigoSocial = ResumoAmizade & {
cosmeticos: ItemCosmetico[];
conquistasDestacadas: ConquistaDestaque[];
};
2 changes: 2 additions & 0 deletions src/routes/amizade.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { Router } from "express";
import { backendClient } from "@/shared/clients/backend.client";
import { middlewareAutenticacao } from "@/shared/middlewares/autenticacao.middleware";
import { criarProxyHandler } from "@/shared/middlewares/proxy.middleware";
import { perfilSocialController } from "@/modules/perfil-social/perfil-social.routes";

const router = Router();

router.use(middlewareAutenticacao);
router.get("/amigos/perfis", perfilSocialController.listarAmigos);
router.all(/.*/, criarProxyHandler(backendClient));

export { router as amizadeRouter };
Loading
Loading