From dead0d904ec58861b40d91046b047986bfff545e Mon Sep 17 00:00:00 2001 From: Martin Brocker Date: Thu, 12 Sep 2024 09:06:03 +0200 Subject: [PATCH 1/4] Fix regression - Incorrect export in route file Reverting a code regression which was causing the production build to fail. Nextjs routes doesnt support export of unknown fields in route files --- src/app/api/auth/[...nextauth]/route.ts | 29 +++---------------------- src/app/api/auth/authOptions.ts | 5 +++++ 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 76d031b..1ac159b 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,30 +1,7 @@ -import NextAuth, { AuthOptions } from "next-auth"; -import KeycloakProvider from "next-auth/providers/keycloak"; +import NextAuth from 'next-auth'; + +import { authOptions } from '../authOptions'; -export const authOptions: AuthOptions = { - providers: [ - KeycloakProvider({ - jwks_endpoint: `${process.env.NEXT_CONTAINER_KEYCLOAK_ENDPOINT}/realms/shlp/protocol/openid-connect/certs`, - wellKnown: undefined, - clientId: process.env.KEYCLOAK_CLIENT_ID, - clientSecret: process.env.KEYCLOAK_CLIENT_SECRET, - issuer: process.env.KEYCLOAK_ISSUER, - authorization: { - params: { - scope: "openid email profile", - }, - url: `${process.env.NEXT_LOCAL_KEYCLOAK_URL}/realms/shlp/protocol/openid-connect/auth`, - }, - token: `${process.env.NEXT_CONTAINER_KEYCLOAK_ENDPOINT}/realms/shlp/protocol/openid-connect/token`, - userinfo: `${process.env.NEXT_CONTAINER_KEYCLOAK_ENDPOINT}/realms/shlp/protocol/openid-connect/userinfo`, - }), - ], - callbacks: { - session: async({session, token, user}) => { - return {...session, token, user}; - } - } -}; const handler = NextAuth(authOptions); export { handler as GET, handler as POST }; diff --git a/src/app/api/auth/authOptions.ts b/src/app/api/auth/authOptions.ts index 23d25d3..0b42785 100644 --- a/src/app/api/auth/authOptions.ts +++ b/src/app/api/auth/authOptions.ts @@ -19,4 +19,9 @@ export const authOptions: AuthOptions = { userinfo: `${process.env.NEXT_CONTAINER_KEYCLOAK_ENDPOINT}/realms/shlp/protocol/openid-connect/userinfo`, }), ], + callbacks: { + session: async ({ session, token, user }) => { + return { ...session, token, user }; + }, + }, }; From 29a10864e3606986a2859a1e4f4b5abf11c2e0f5 Mon Sep 17 00:00:00 2001 From: Martin Brocker Date: Thu, 12 Sep 2024 09:07:54 +0200 Subject: [PATCH 2/4] Run Prettier to apply code formats --- src/app/api/docs/page.tsx | 6 +- src/app/api/docs/react-swagger.tsx | 4 +- .../share-links/[id]/accesses/route.test.ts | 64 ++-- .../api/v1/share-links/[id]/accesses/route.ts | 64 ++-- .../share-links/[id]/deactivate/route.test.ts | 8 +- .../v1/share-links/[id]/deactivate/route.ts | 41 +-- .../[id]/endpoints/[endpointId]/route.ts | 3 +- .../v1/share-links/[id]/endpoints/route.ts | 17 +- src/app/api/v1/share-links/route.test.ts | 282 +++++++++--------- src/app/api/v1/share-links/route.ts | 63 ++-- src/app/api/v1/users/[id]/ips/route.test.ts | 39 +-- src/app/api/v1/users/[id]/ips/route.ts | 19 +- src/app/api/v1/users/[id]/route.test.ts | 4 +- src/app/api/v1/users/[id]/route.ts | 24 +- src/app/utils/authentication.ts | 35 ++- src/app/utils/error-handler.ts | 11 +- src/domain/dtos/server-config.ts | 2 +- src/domain/dtos/shlink-access.ts | 12 +- src/domain/dtos/shlink-endpoint.ts | 2 +- src/domain/dtos/shlink.ts | 12 +- src/domain/dtos/user.ts | 1 - src/infrastructure/clients/prisma.ts | 4 +- src/lib/swagger.ts | 18 +- src/mappers/shlink-access-mapper.test.ts | 13 +- src/mappers/shlink-access-mapper.ts | 41 +-- src/middleware.ts | 18 +- .../shlink-access/get-shlink-accesses.test.ts | 24 +- .../shlink-access/get-shlink-accesses.ts | 21 +- .../shlinks/deactivate-shlink.test.ts | 41 ++- src/usecases/shlinks/deactivate-shlink.ts | 25 +- src/usecases/shlinks/get-shlink.test.ts | 22 +- src/usecases/shlinks/get-shlink.ts | 2 +- 32 files changed, 531 insertions(+), 411 deletions(-) diff --git a/src/app/api/docs/page.tsx b/src/app/api/docs/page.tsx index 287a181..edb35cc 100644 --- a/src/app/api/docs/page.tsx +++ b/src/app/api/docs/page.tsx @@ -1,6 +1,6 @@ -import { getApiDocs } from "@/lib/swagger"; +import { getApiDocs } from '@/lib/swagger'; -import ReactSwagger from "./react-swagger"; +import ReactSwagger from './react-swagger'; export default async function IndexPage() { const spec = await getApiDocs(); @@ -9,4 +9,4 @@ export default async function IndexPage() { ); -} \ No newline at end of file +} diff --git a/src/app/api/docs/react-swagger.tsx b/src/app/api/docs/react-swagger.tsx index 7d4bd6a..ff08ba9 100644 --- a/src/app/api/docs/react-swagger.tsx +++ b/src/app/api/docs/react-swagger.tsx @@ -4,11 +4,11 @@ import SwaggerUI from 'swagger-ui-react'; import 'swagger-ui-react/swagger-ui.css'; type Props = { - spec: Record, + spec: Record; }; function ReactSwagger({ spec }: Props) { return ; } -export default ReactSwagger; \ No newline at end of file +export default ReactSwagger; diff --git a/src/app/api/v1/share-links/[id]/accesses/route.test.ts b/src/app/api/v1/share-links/[id]/accesses/route.test.ts index c51d8c6..b18e7e0 100644 --- a/src/app/api/v1/share-links/[id]/accesses/route.test.ts +++ b/src/app/api/v1/share-links/[id]/accesses/route.test.ts @@ -2,15 +2,15 @@ * @jest-environment node */ -import { NOT_FOUND } from "@/app/constants/http-constants"; -import { handleApiValidationError } from "@/app/utils/error-handler"; -import { mapModelToDto } from "@/mappers/shlink-access-mapper"; -import { getSHLinkAccessesUseCase } from "@/usecases/shlink-access/get-shlink-accesses"; -import { getSingleSHLinkUseCase } from "@/usecases/shlinks/get-single-shlink"; +import { NOT_FOUND } from '@/app/constants/http-constants'; +import { handleApiValidationError } from '@/app/utils/error-handler'; +import { mapModelToDto } from '@/mappers/shlink-access-mapper'; +import { getSHLinkAccessesUseCase } from '@/usecases/shlink-access/get-shlink-accesses'; +import { getSingleSHLinkUseCase } from '@/usecases/shlinks/get-single-shlink'; -import { POST } from "./route"; +import { POST } from './route'; -jest.mock("@/container", () => ({ +jest.mock('@/container', () => ({ container: { get: jest.fn(), }, @@ -18,23 +18,23 @@ jest.mock("@/container", () => ({ SHLinkRepositoryToken: Symbol('SHLinkRepositoryToken'), })); -jest.mock("@/usecases/shlink-access/get-shlink-accesses", () => ({ +jest.mock('@/usecases/shlink-access/get-shlink-accesses', () => ({ getSHLinkAccessesUseCase: jest.fn(), })); -jest.mock("@/usecases/shlinks/get-single-shlink", () => ({ +jest.mock('@/usecases/shlinks/get-single-shlink', () => ({ getSingleSHLinkUseCase: jest.fn(), })); -jest.mock("@/mappers/shlink-access-mapper", () => ({ +jest.mock('@/mappers/shlink-access-mapper', () => ({ mapModelToDto: jest.fn(), })); -jest.mock("@/app/utils/error-handler", () => ({ +jest.mock('@/app/utils/error-handler', () => ({ handleApiValidationError: jest.fn(), })); -describe("POST function", () => { +describe('POST function', () => { let mockGetSingleSHLinkUseCase: jest.Mock; let mockGetSHLinkAccessesUseCase: jest.Mock; let mockMapModelToDto: jest.Mock; @@ -47,13 +47,13 @@ describe("POST function", () => { mockHandleApiValidationError = handleApiValidationError as jest.Mock; }); - it("should return 404 if shlink is not found", async () => { + it('should return 404 if shlink is not found', async () => { // Arrange - const request = new Request("http://localhost", { - method: "POST", - body: JSON.stringify({ managementToken: "token" }), + const request = new Request('http://localhost', { + method: 'POST', + body: JSON.stringify({ managementToken: 'token' }), }); - const params = { id: "non-existent-id" }; + const params = { id: 'non-existent-id' }; mockGetSingleSHLinkUseCase.mockResolvedValue(null); @@ -65,16 +65,16 @@ describe("POST function", () => { expect(await response.json()).toEqual({ message: NOT_FOUND }); }); - it("should return 200 with access data if shlink is found", async () => { + it('should return 200 with access data if shlink is found', async () => { // Arrange - const request = new Request("http://localhost", { - method: "POST", - body: JSON.stringify({ managementToken: "token" }), + const request = new Request('http://localhost', { + method: 'POST', + body: JSON.stringify({ managementToken: 'token' }), }); - const params = { id: "existent-id" }; - const shlink = { getId: jest.fn().mockReturnValue("shlink-id") }; - const accesses = [{ id: "access-id-1" }, { id: "access-id-2" }]; - const dto = { id: "access-id-1" }; + const params = { id: 'existent-id' }; + const shlink = { getId: jest.fn().mockReturnValue('shlink-id') }; + const accesses = [{ id: 'access-id-1' }, { id: 'access-id-2' }]; + const dto = { id: 'access-id-1' }; mockGetSingleSHLinkUseCase.mockResolvedValue(shlink); mockGetSHLinkAccessesUseCase.mockResolvedValue(accesses); @@ -85,18 +85,18 @@ describe("POST function", () => { // Assert expect(response.status).toBe(200); - expect(await response.json()).toEqual(accesses.map(x => ({ id: x.id }))); + expect(await response.json()).toEqual(accesses.map((x) => ({ id: x.id }))); }); - it("should handle errors and call handleApiValidationError", async () => { + it('should handle errors and call handleApiValidationError', async () => { // Arrange - const request = new Request("http://localhost", { - method: "POST", - body: JSON.stringify({ managementToken: "token" }), + const request = new Request('http://localhost', { + method: 'POST', + body: JSON.stringify({ managementToken: 'token' }), }); - const params = { id: "some-id" }; + const params = { id: 'some-id' }; - mockGetSingleSHLinkUseCase.mockRejectedValue(new Error("Test error")); + mockGetSingleSHLinkUseCase.mockRejectedValue(new Error('Test error')); // Act await POST(request, params); diff --git a/src/app/api/v1/share-links/[id]/accesses/route.ts b/src/app/api/v1/share-links/[id]/accesses/route.ts index 3009c79..a482ce3 100644 --- a/src/app/api/v1/share-links/[id]/accesses/route.ts +++ b/src/app/api/v1/share-links/[id]/accesses/route.ts @@ -1,16 +1,22 @@ -import { NextResponse } from "next/server"; +import { NextResponse } from 'next/server'; -import { NOT_FOUND } from "@/app/constants/http-constants"; -import { handleApiValidationError } from "@/app/utils/error-handler"; -import { container, SHLinkAccessRepositoryToken, SHLinkRepositoryToken } from "@/container"; -import { SHLinkAccessRequestDto } from "@/domain/dtos/shlink-access"; -import { ISHLinkAccessRepository } from "@/infrastructure/repositories/interfaces/shlink-access-repository"; -import { ISHLinkRepository } from "@/infrastructure/repositories/interfaces/shlink-repository"; -import { mapModelToDto } from "@/mappers/shlink-access-mapper"; -import { getSHLinkAccessesUseCase } from "@/usecases/shlink-access/get-shlink-accesses"; -import { getSingleSHLinkUseCase } from "@/usecases/shlinks/get-single-shlink"; +import { NOT_FOUND } from '@/app/constants/http-constants'; +import { handleApiValidationError } from '@/app/utils/error-handler'; +import { + container, + SHLinkAccessRepositoryToken, + SHLinkRepositoryToken, +} from '@/container'; +import { SHLinkAccessRequestDto } from '@/domain/dtos/shlink-access'; +import { ISHLinkAccessRepository } from '@/infrastructure/repositories/interfaces/shlink-access-repository'; +import { ISHLinkRepository } from '@/infrastructure/repositories/interfaces/shlink-repository'; +import { mapModelToDto } from '@/mappers/shlink-access-mapper'; +import { getSHLinkAccessesUseCase } from '@/usecases/shlink-access/get-shlink-accesses'; +import { getSingleSHLinkUseCase } from '@/usecases/shlinks/get-single-shlink'; -const repo = container.get(SHLinkAccessRepositoryToken); +const repo = container.get( + SHLinkAccessRepositoryToken, +); const shlinkRepo = container.get(SHLinkRepositoryToken); /** @@ -40,18 +46,26 @@ const shlinkRepo = container.get(SHLinkRepositoryToken); * items: * $ref: '#/components/schemas/SHLinkAccess' */ -export async function POST(request: Request, params: {id: string }) { - try{ - const dto: SHLinkAccessRequestDto = await request.json(); - const shlink = await getSingleSHLinkUseCase({ repo: shlinkRepo }, { id: params.id, managementToken: dto.managementToken }); - if(!shlink){ - return NextResponse.json({ message: NOT_FOUND }, { status: 404 }); - } - - const accesses = await getSHLinkAccessesUseCase({ repo }, { shlinkId: shlink.getId() }); - return NextResponse.json(accesses.map(x => mapModelToDto(x)), { status: 200 }); +export async function POST(request: Request, params: { id: string }) { + try { + const dto: SHLinkAccessRequestDto = await request.json(); + const shlink = await getSingleSHLinkUseCase( + { repo: shlinkRepo }, + { id: params.id, managementToken: dto.managementToken }, + ); + if (!shlink) { + return NextResponse.json({ message: NOT_FOUND }, { status: 404 }); } - catch(error){ - return handleApiValidationError(error); - } -} \ No newline at end of file + + const accesses = await getSHLinkAccessesUseCase( + { repo }, + { shlinkId: shlink.getId() }, + ); + return NextResponse.json( + accesses.map((x) => mapModelToDto(x)), + { status: 200 }, + ); + } catch (error) { + return handleApiValidationError(error); + } +} diff --git a/src/app/api/v1/share-links/[id]/deactivate/route.test.ts b/src/app/api/v1/share-links/[id]/deactivate/route.test.ts index fb6f769..ccc7bbc 100644 --- a/src/app/api/v1/share-links/[id]/deactivate/route.test.ts +++ b/src/app/api/v1/share-links/[id]/deactivate/route.test.ts @@ -69,12 +69,16 @@ describe('DELETE /api/v1/share-link/[id]/deactivate', () => { }); it('should return deactivated link DTO and status 200 when link is found', async () => { - (getUserProfile as jest.Mock).mockResolvedValue({id: 'user-123456', name: '', email: ''}); + (getUserProfile as jest.Mock).mockResolvedValue({ + id: 'user-123456', + name: '', + email: '', + }); (deactivateSHLinksUseCase as jest.Mock).mockResolvedValue(mockModel); (mapModelToDto as jest.Mock).mockReturnValue(mockDto); const request = mockRequest(); - const response = await DELETE(request, {params: {id: 'user-123456',}}); + const response = await DELETE(request, { params: { id: 'user-123456' } }); expect(response).toBeInstanceOf(NextResponse); expect(response.status).toBe(200); diff --git a/src/app/api/v1/share-links/[id]/deactivate/route.ts b/src/app/api/v1/share-links/[id]/deactivate/route.ts index 9bdf496..356273a 100644 --- a/src/app/api/v1/share-links/[id]/deactivate/route.ts +++ b/src/app/api/v1/share-links/[id]/deactivate/route.ts @@ -1,12 +1,12 @@ -import { NextResponse } from "next/server"; +import { NextResponse } from 'next/server'; -import { NOT_FOUND } from "@/app/constants/http-constants"; -import { getUserProfile } from "@/app/utils/authentication"; -import { handleApiValidationError } from "@/app/utils/error-handler"; -import { container, SHLinkRepositoryToken } from "@/container"; -import { ISHLinkRepository } from "@/infrastructure/repositories/interfaces/shlink-repository"; -import { mapModelToDto } from "@/mappers/shlink-mapper"; -import { deactivateSHLinksUseCase } from "@/usecases/shlinks/deactivate-shlink"; +import { NOT_FOUND } from '@/app/constants/http-constants'; +import { getUserProfile } from '@/app/utils/authentication'; +import { handleApiValidationError } from '@/app/utils/error-handler'; +import { container, SHLinkRepositoryToken } from '@/container'; +import { ISHLinkRepository } from '@/infrastructure/repositories/interfaces/shlink-repository'; +import { mapModelToDto } from '@/mappers/shlink-mapper'; +import { deactivateSHLinksUseCase } from '@/usecases/shlinks/deactivate-shlink'; const repo = container.get(SHLinkRepositoryToken); @@ -30,15 +30,20 @@ const repo = container.get(SHLinkRepositoryToken); * type: object * $ref: '#/components/schemas/SHLinkMini' */ -export async function DELETE(request: Request, { params }: { params: { id: string } }) { - try{ - const user = await getUserProfile(request); - const result = await deactivateSHLinksUseCase({ repo }, { id: params.id, user }); - const data = mapModelToDto(result); - if(result) return NextResponse.json(data, { status: 200 }); - return NextResponse.json({ message: NOT_FOUND }, { status: 404}); - } - catch(error){ - return handleApiValidationError(error); +export async function DELETE( + request: Request, + { params }: { params: { id: string } }, +) { + try { + const user = await getUserProfile(request); + const result = await deactivateSHLinksUseCase( + { repo }, + { id: params.id, user }, + ); + const data = mapModelToDto(result); + if (result) return NextResponse.json(data, { status: 200 }); + return NextResponse.json({ message: NOT_FOUND }, { status: 404 }); + } catch (error) { + return handleApiValidationError(error); } } diff --git a/src/app/api/v1/share-links/[id]/endpoints/[endpointId]/route.ts b/src/app/api/v1/share-links/[id]/endpoints/[endpointId]/route.ts index d78d48c..37a1979 100644 --- a/src/app/api/v1/share-links/[id]/endpoints/[endpointId]/route.ts +++ b/src/app/api/v1/share-links/[id]/endpoints/[endpointId]/route.ts @@ -80,7 +80,8 @@ export async function GET( { id: params.id }, ); - if(!shlink) return NextResponse.json({ message: NOT_FOUND }, { status: 404 }); + if (!shlink) + return NextResponse.json({ message: NOT_FOUND }, { status: 404 }); const user = await getUserUseCase( { repo: userRepo }, diff --git a/src/app/api/v1/share-links/[id]/endpoints/route.ts b/src/app/api/v1/share-links/[id]/endpoints/route.ts index 20c1038..679e78d 100644 --- a/src/app/api/v1/share-links/[id]/endpoints/route.ts +++ b/src/app/api/v1/share-links/[id]/endpoints/route.ts @@ -2,7 +2,12 @@ import { NextResponse } from 'next/server'; import { NOT_FOUND } from '@/app/constants/http-constants'; import { handleApiValidationError } from '@/app/utils/error-handler'; -import { container, ServerConfigRepositoryToken, SHLinkEndpointRepositoryToken, SHLinkRepositoryToken } from '@/container'; +import { + container, + ServerConfigRepositoryToken, + SHLinkEndpointRepositoryToken, + SHLinkRepositoryToken, +} from '@/container'; import { CreateSHLinkEndpointDto, SHLinkEndpointDto, @@ -19,9 +24,13 @@ import { getServerConfigsUseCase } from '@/usecases/server-configs/get-server-co import { addEndpointUseCase } from '@/usecases/shlink-endpoint/add-endpoint'; import { getSingleSHLinkUseCase } from '@/usecases/shlinks/get-single-shlink'; -const shlRepo = container.get(SHLinkRepositoryToken); -const shlEndpointRepo = container.get(SHLinkEndpointRepositoryToken); -const serverConfigRepo = container.get(ServerConfigRepositoryToken); +const shlRepo = container.get(SHLinkRepositoryToken); +const shlEndpointRepo = container.get( + SHLinkEndpointRepositoryToken, +); +const serverConfigRepo = container.get( + ServerConfigRepositoryToken, +); /** * @swagger diff --git a/src/app/api/v1/share-links/route.test.ts b/src/app/api/v1/share-links/route.test.ts index 3c50920..9dfee5a 100644 --- a/src/app/api/v1/share-links/route.test.ts +++ b/src/app/api/v1/share-links/route.test.ts @@ -1,88 +1,88 @@ /** * @jest-environment node */ -import { NextRequest, NextResponse } from "next/server"; - -import { getUserProfile, validateUser } from "@/app/utils/authentication"; -import { handleApiValidationError } from "@/app/utils/error-handler"; -import { CreateSHLinkDto, SHLinkDto } from "@/domain/dtos/shlink"; -import { SHLinkModel } from "@/domain/models/shlink"; -import { SHLinkEntity } from "@/entities/shlink"; -import { SHLinkPrismaRepository } from "@/infrastructure/repositories/prisma/shlink-repository"; -import { - mapDtoToModel, - mapEntityToModel, - mapModelToDto, - mapModelToMiniDto -} from "@/mappers/shlink-mapper"; -import { addShlinkUseCase } from "@/usecases/shlinks/add-shlink"; -import { getSHLinkUseCase } from "@/usecases/shlinks/get-shlink"; - -import { POST, GET } from "./route"; +import { NextRequest, NextResponse } from 'next/server'; + +import { getUserProfile, validateUser } from '@/app/utils/authentication'; +import { handleApiValidationError } from '@/app/utils/error-handler'; +import { CreateSHLinkDto, SHLinkDto } from '@/domain/dtos/shlink'; +import { SHLinkModel } from '@/domain/models/shlink'; +import { SHLinkEntity } from '@/entities/shlink'; +import { SHLinkPrismaRepository } from '@/infrastructure/repositories/prisma/shlink-repository'; +import { + mapDtoToModel, + mapEntityToModel, + mapModelToDto, + mapModelToMiniDto, +} from '@/mappers/shlink-mapper'; +import { addShlinkUseCase } from '@/usecases/shlinks/add-shlink'; +import { getSHLinkUseCase } from '@/usecases/shlinks/get-shlink'; + +import { POST, GET } from './route'; // Mocks -jest.mock("@/app/utils/error-handler"); -jest.mock("@/app/utils/authentication"); -jest.mock("@/mappers/shlink-mapper"); -jest.mock("@/infrastructure/repositories/prisma/shlink-repository"); -jest.mock("@/infrastructure/clients/prisma"); +jest.mock('@/app/utils/error-handler'); +jest.mock('@/app/utils/authentication'); +jest.mock('@/mappers/shlink-mapper'); +jest.mock('@/infrastructure/repositories/prisma/shlink-repository'); +jest.mock('@/infrastructure/clients/prisma'); jest.mock('@/usecases/shlinks/add-shlink', () => ({ - addShlinkUseCase: jest.fn(), + addShlinkUseCase: jest.fn(), })); jest.mock('@/usecases/shlinks/get-shlink', () => ({ - getSHLinkUseCase: jest.fn(), + getSHLinkUseCase: jest.fn(), })); jest.mock('@/mappers/shlink-mapper', () => ({ - mapDtoToModel: jest.fn(), - mapEntityToModel: jest.fn(), - mapModelToDto: jest.fn(), - mapModelToMiniDto: jest.fn(), + mapDtoToModel: jest.fn(), + mapEntityToModel: jest.fn(), + mapModelToDto: jest.fn(), + mapModelToMiniDto: jest.fn(), })); jest.mock('@/app/utils/error-handler', () => ({ - handleApiValidationError: jest.fn(), + handleApiValidationError: jest.fn(), })); jest.mock('@/app/constants/http-constants', () => ({ - NOT_FOUND: 'Not Found', + NOT_FOUND: 'Not Found', })); // Constants const dataDto = { - userId: "1234567890", - name: 'name', - passcodeFailuresRemaining: 3, - active: true, - managementToken: "token-xyz1234", - configPasscode: "passcode-abcde", - configExp: new Date("2024-01-01T00:00:00Z"), + userId: '1234567890', + name: 'name', + passcodeFailuresRemaining: 3, + active: true, + managementToken: 'token-xyz1234', + configPasscode: 'passcode-abcde', + configExp: new Date('2024-01-01T00:00:00Z'), }; const mockDto: CreateSHLinkDto = { - userId: "1234567890", - name: 'name', - configPasscode: "passcode-abcde", - configExp: new Date("2024-01-01T00:00:00Z"), + userId: '1234567890', + name: 'name', + configPasscode: 'passcode-abcde', + configExp: new Date('2024-01-01T00:00:00Z'), }; const mockModel = new SHLinkModel( - dataDto.userId, - dataDto.name, - dataDto.passcodeFailuresRemaining, - dataDto.active, - dataDto.managementToken, - dataDto.configPasscode, - dataDto.configExp, - "1" + dataDto.userId, + dataDto.name, + dataDto.passcodeFailuresRemaining, + dataDto.active, + dataDto.managementToken, + dataDto.configPasscode, + dataDto.configExp, + '1', ); const mockEntity: SHLinkEntity = { - id: "1", - name: dataDto.name, - user_id: dataDto.userId, - passcode_failures_remaining: dataDto.passcodeFailuresRemaining, - active: dataDto.active, - management_token: dataDto.managementToken, - config_passcode: dataDto.configPasscode, - config_exp: dataDto.configExp, + id: '1', + name: dataDto.name, + user_id: dataDto.userId, + passcode_failures_remaining: dataDto.passcodeFailuresRemaining, + active: dataDto.active, + management_token: dataDto.managementToken, + config_passcode: dataDto.configPasscode, + config_exp: dataDto.configExp, }; // Mocks for repository methods @@ -92,91 +92,107 @@ const mockFindMany = jest.fn().mockResolvedValue([mockEntity]); SHLinkPrismaRepository.prototype.insert = mockInsert; SHLinkPrismaRepository.prototype.findMany = mockFindMany; -describe("API Route Handlers", () => { - describe("POST /shlink", () => { - it("should handle successful POST request", async () => { - (validateUser as jest.Mock).mockResolvedValue({id: '1234567890'}); - (mapDtoToModel as jest.Mock).mockReturnValue(mockModel); - (addShlinkUseCase as jest.Mock).mockResolvedValue(mockModel); - (mapModelToDto as jest.Mock).mockReturnValue(mockDto); +describe('API Route Handlers', () => { + describe('POST /shlink', () => { + it('should handle successful POST request', async () => { + (validateUser as jest.Mock).mockResolvedValue({ id: '1234567890' }); + (mapDtoToModel as jest.Mock).mockReturnValue(mockModel); + (addShlinkUseCase as jest.Mock).mockResolvedValue(mockModel); + (mapModelToDto as jest.Mock).mockReturnValue(mockDto); - const request = new NextRequest('http://localhost/api/share-link', { method: 'POST', body: JSON.stringify(mockDto) }); + const request = new NextRequest('http://localhost/api/share-link', { + method: 'POST', + body: JSON.stringify(mockDto), + }); - const response = await POST(request); + const response = await POST(request); - expect(validateUser).toHaveBeenCalledWith(request, mockDto.userId); - expect(response.status).toBe(200); - expect(response).toBeInstanceOf(NextResponse); + expect(validateUser).toHaveBeenCalledWith(request, mockDto.userId); + expect(response.status).toBe(200); + expect(response).toBeInstanceOf(NextResponse); - const responseBody = await response.json(); - responseBody.configExp = new Date(responseBody.configExp) - expect(responseBody).toEqual(mockDto); - }); + const responseBody = await response.json(); + responseBody.configExp = new Date(responseBody.configExp); + expect(responseBody).toEqual(mockDto); + }); - it("should handle validation errors", async () => { - const error = new Error('Validation error'); - (validateUser as jest.Mock).mockResolvedValue({id: '1234567890'}); - (addShlinkUseCase as jest.Mock).mockRejectedValue(error); - (handleApiValidationError as jest.Mock).mockReturnValue(NextResponse.json({ message: 'Validation error' }, { status: 400 })); + it('should handle validation errors', async () => { + const error = new Error('Validation error'); + (validateUser as jest.Mock).mockResolvedValue({ id: '1234567890' }); + (addShlinkUseCase as jest.Mock).mockRejectedValue(error); + (handleApiValidationError as jest.Mock).mockReturnValue( + NextResponse.json({ message: 'Validation error' }, { status: 400 }), + ); - const request = new NextRequest('http://localhost/api/share-link', { method: 'POST', body: JSON.stringify(mockDto) }); + const request = new NextRequest('http://localhost/api/share-link', { + method: 'POST', + body: JSON.stringify(mockDto), + }); - const response = await POST(request); + const response = await POST(request); - expect(validateUser).toHaveBeenCalledWith(request, mockDto.userId); - expect(handleApiValidationError).toHaveBeenCalledWith(error); - expect(response).toBeInstanceOf(NextResponse); - expect(response.status).toBe(400); + expect(validateUser).toHaveBeenCalledWith(request, mockDto.userId); + expect(handleApiValidationError).toHaveBeenCalledWith(error); + expect(response).toBeInstanceOf(NextResponse); + expect(response.status).toBe(400); - const json = await response.json(); - expect(json).toEqual({ message: 'Validation error' }); - }); + const json = await response.json(); + expect(json).toEqual({ message: 'Validation error' }); + }); + }); + + describe('GET /shlink', () => { + it('should handle successful GET request with valid user_id', async () => { + (getUserProfile as jest.Mock).mockResolvedValue({ id: '1234567890' }); + (getSHLinkUseCase as jest.Mock).mockResolvedValue([mockEntity]); + (mapEntityToModel as jest.Mock).mockReturnValue(mockModel); + (mapModelToMiniDto as jest.Mock).mockReturnValue({ + id: mockModel.getId(), + name: mockModel.getName(), + // Map other properties as necessary + }); + + const request = new NextRequest('http://localhost/api/share-link', { + method: 'GET', + }); + + const response = await GET(request); + + expect(getUserProfile).toHaveBeenCalledWith(request); + expect(response.status).toBe(200); + expect(response).toBeInstanceOf(NextResponse); + + const responseBody = await response.json(); + expect(responseBody).toEqual([ + { + id: mockModel.getId(), + name: mockModel.getName(), + // Match other properties as necessary + }, + ]); }); - describe("GET /shlink", () => { - it("should handle successful GET request with valid user_id", async () => { - (getUserProfile as jest.Mock).mockResolvedValue({id: '1234567890'}); - (getSHLinkUseCase as jest.Mock).mockResolvedValue([mockEntity]); - (mapEntityToModel as jest.Mock).mockReturnValue(mockModel); - (mapModelToMiniDto as jest.Mock).mockReturnValue({ - id: mockModel.getId(), - name: mockModel.getName(), - // Map other properties as necessary - }); - - const request = new NextRequest('http://localhost/api/share-link', { method: 'GET' }); - - const response = await GET(request); - - expect(getUserProfile).toHaveBeenCalledWith(request); - expect(response.status).toBe(200); - expect(response).toBeInstanceOf(NextResponse); - - const responseBody = await response.json(); - expect(responseBody).toEqual([{ - id: mockModel.getId(), - name: mockModel.getName(), - // Match other properties as necessary - }]); - }); - - it("should handle errors during GET request", async () => { - const error = new Error('Database error'); - (getUserProfile as jest.Mock).mockResolvedValue({id: '1234567890'}); - (getSHLinkUseCase as jest.Mock).mockRejectedValue(error); - (handleApiValidationError as jest.Mock).mockReturnValue(NextResponse.json({ message: 'Database error' }, { status: 500 })); - - const request = new NextRequest('http://localhost/api/share-link', { method: 'GET' }); - - const response = await GET(request); - - expect(getUserProfile).toHaveBeenCalledWith(request); - expect(handleApiValidationError).toHaveBeenCalledWith(error); - expect(response).toBeInstanceOf(NextResponse); - expect(response.status).toBe(500); - - const json = await response.json(); - expect(json).toEqual({ message: 'Database error' }); - }); + it('should handle errors during GET request', async () => { + const error = new Error('Database error'); + (getUserProfile as jest.Mock).mockResolvedValue({ id: '1234567890' }); + (getSHLinkUseCase as jest.Mock).mockRejectedValue(error); + (handleApiValidationError as jest.Mock).mockReturnValue( + NextResponse.json({ message: 'Database error' }, { status: 500 }), + ); + + const request = new NextRequest('http://localhost/api/share-link', { + method: 'GET', + }); + + const response = await GET(request); + + expect(getUserProfile).toHaveBeenCalledWith(request); + expect(handleApiValidationError).toHaveBeenCalledWith(error); + expect(response).toBeInstanceOf(NextResponse); + expect(response.status).toBe(500); + + const json = await response.json(); + expect(json).toEqual({ message: 'Database error' }); }); + }); }); diff --git a/src/app/api/v1/share-links/route.ts b/src/app/api/v1/share-links/route.ts index fc24294..3f5ed7f 100644 --- a/src/app/api/v1/share-links/route.ts +++ b/src/app/api/v1/share-links/route.ts @@ -1,17 +1,17 @@ -import { NextResponse } from "next/server"; +import { NextResponse } from 'next/server'; -import { getUserProfile, validateUser } from "@/app/utils/authentication"; -import { handleApiValidationError } from "@/app/utils/error-handler"; -import { container, SHLinkRepositoryToken } from "@/container"; -import { CreateSHLinkDto, SHLinkDto } from "@/domain/dtos/shlink"; -import { ISHLinkRepository } from "@/infrastructure/repositories/interfaces/shlink-repository"; -import { - mapDtoToModel, - mapModelToDto, - mapModelToMiniDto -} from "@/mappers/shlink-mapper"; -import { addShlinkUseCase } from "@/usecases/shlinks/add-shlink"; -import { getSHLinkUseCase } from "@/usecases/shlinks/get-shlink"; +import { getUserProfile, validateUser } from '@/app/utils/authentication'; +import { handleApiValidationError } from '@/app/utils/error-handler'; +import { container, SHLinkRepositoryToken } from '@/container'; +import { CreateSHLinkDto, SHLinkDto } from '@/domain/dtos/shlink'; +import { ISHLinkRepository } from '@/infrastructure/repositories/interfaces/shlink-repository'; +import { + mapDtoToModel, + mapModelToDto, + mapModelToMiniDto, +} from '@/mappers/shlink-mapper'; +import { addShlinkUseCase } from '@/usecases/shlinks/add-shlink'; +import { getSHLinkUseCase } from '@/usecases/shlinks/get-shlink'; const repo = container.get(SHLinkRepositoryToken); @@ -37,16 +37,15 @@ const repo = container.get(SHLinkRepositoryToken); * $ref: '#/components/schemas/SHLink' */ export async function POST(request: Request) { - try{ - const dto: CreateSHLinkDto = await request.json(); - await validateUser(request, dto.userId); - const model = mapDtoToModel(dto as SHLinkDto) - const newShlink = await addShlinkUseCase({ repo }, { shlink: model }) - return NextResponse.json(mapModelToDto(newShlink), { status: 200 }); - } - catch(error){ - return handleApiValidationError(error); - } + try { + const dto: CreateSHLinkDto = await request.json(); + await validateUser(request, dto.userId); + const model = mapDtoToModel(dto as SHLinkDto); + const newShlink = await addShlinkUseCase({ repo }, { shlink: model }); + return NextResponse.json(mapModelToDto(newShlink), { status: 200 }); + } catch (error) { + return handleApiValidationError(error); + } } /** @@ -66,13 +65,15 @@ export async function POST(request: Request) { * $ref: '#/components/schemas/SHLinkMini' */ export async function GET(request: Request) { - try{ - const user = await getUserProfile(request); + try { + const user = await getUserProfile(request); - const newShlink = await getSHLinkUseCase({ repo }, { user_id: user.id }) - return NextResponse.json(newShlink.map(shlink => mapModelToMiniDto(shlink)), { status: 200 }); - } - catch(error){ - return handleApiValidationError(error); - } + const newShlink = await getSHLinkUseCase({ repo }, { user_id: user.id }); + return NextResponse.json( + newShlink.map((shlink) => mapModelToMiniDto(shlink)), + { status: 200 }, + ); + } catch (error) { + return handleApiValidationError(error); + } } diff --git a/src/app/api/v1/users/[id]/ips/route.test.ts b/src/app/api/v1/users/[id]/ips/route.test.ts index 8165734..60b1387 100644 --- a/src/app/api/v1/users/[id]/ips/route.test.ts +++ b/src/app/api/v1/users/[id]/ips/route.test.ts @@ -4,16 +4,16 @@ import { NextRequest } from 'next/server'; -import { NOT_FOUND } from "@/app/constants/http-constants"; -import { validateUser } from "@/app/utils/authentication"; -import { ExternalDataFetchError } from "@/services/hapi-fhir.service"; -import { getPatientDataUseCase } from "@/usecases/patient/get-patient-data"; -import { getUserUseCase } from "@/usecases/users/get-user"; +import { NOT_FOUND } from '@/app/constants/http-constants'; +import { validateUser } from '@/app/utils/authentication'; +import { ExternalDataFetchError } from '@/services/hapi-fhir.service'; +import { getPatientDataUseCase } from '@/usecases/patient/get-patient-data'; +import { getUserUseCase } from '@/usecases/users/get-user'; -import { GET } from "./route"; +import { GET } from './route'; -jest.mock("@/app/utils/authentication", () => ({ - validateUser: jest.fn() +jest.mock('@/app/utils/authentication', () => ({ + validateUser: jest.fn(), })); jest.mock('@/usecases/patient/get-patient-data', () => ({ @@ -24,13 +24,14 @@ jest.mock('@/usecases/users/get-user', () => ({ getUserUseCase: jest.fn(), })); -describe("GET handler", () => { - const mockRequest = (id: string) => new NextRequest(`http://localhost/api/users/${id}/ips`, { - headers: new Headers(), - method: "GET" - }); +describe('GET handler', () => { + const mockRequest = (id: string) => + new NextRequest(`http://localhost/api/users/${id}/ips`, { + headers: new Headers(), + method: 'GET', + }); - it("should return 404 if user is not found", async () => { + it('should return 404 if user is not found', async () => { (validateUser as jest.Mock).mockResolvedValue(undefined); // Mock successful validation (getUserUseCase as jest.Mock).mockResolvedValue(null); @@ -58,17 +59,17 @@ describe("GET handler", () => { expect(jsonResponse).toEqual(mockPatientData); }); - it("should handle errors correctly", async () => { - const mockUser = { id: "existing-id", name: "John Doe" }; - + it('should handle errors correctly', async () => { + const mockUser = { id: 'existing-id', name: 'John Doe' }; + (validateUser as jest.Mock).mockResolvedValue(undefined); // Mock successful validation (getUserUseCase as jest.Mock).mockResolvedValue(mockUser); (getPatientDataUseCase as jest.Mock).mockRejectedValue( new ExternalDataFetchError('Test error'), ); - const request = mockRequest("existing-id"); - const response = await GET(request, { params: { id: "existing-id" } }); + const request = mockRequest('existing-id'); + const response = await GET(request, { params: { id: 'existing-id' } }); expect(response.status).toBe(412); }); diff --git a/src/app/api/v1/users/[id]/ips/route.ts b/src/app/api/v1/users/[id]/ips/route.ts index a171e33..fc14e29 100644 --- a/src/app/api/v1/users/[id]/ips/route.ts +++ b/src/app/api/v1/users/[id]/ips/route.ts @@ -38,14 +38,21 @@ const serverConfigRepo = container.get( * type: object */ export async function GET( - request: Request, { params }: { params: { id: string } } + request: Request, + { params }: { params: { id: string } }, ) { - - try{ + try { await validateUser(request, params.id); - const user = await getUserUseCase({repo: userRepo}, {userId: params.id}); - if(!user) return NextResponse.json({message: NOT_FOUND}, { status: 404}); - const result = await getPatientDataUseCase({repo: serverConfigRepo }, {user}); + const user = await getUserUseCase( + { repo: userRepo }, + { userId: params.id }, + ); + if (!user) + return NextResponse.json({ message: NOT_FOUND }, { status: 404 }); + const result = await getPatientDataUseCase( + { repo: serverConfigRepo }, + { user }, + ); return NextResponse.json(result, { status: 200 }); } catch (error) { diff --git a/src/app/api/v1/users/[id]/route.test.ts b/src/app/api/v1/users/[id]/route.test.ts index 0ea9a25..0f579b2 100644 --- a/src/app/api/v1/users/[id]/route.test.ts +++ b/src/app/api/v1/users/[id]/route.test.ts @@ -83,7 +83,9 @@ describe('GET /api/users/[id]', () => { const error = new Error('Validation error'); (validateUser as jest.Mock).mockResolvedValue(undefined); // Assuming validateUser returns undefined on success (getUserUseCase as jest.Mock).mockRejectedValue(error); - (handleApiValidationError as jest.Mock).mockReturnValue(NextResponse.json({ message: 'Validation error' }, { status: 400 })); + (handleApiValidationError as jest.Mock).mockReturnValue( + NextResponse.json({ message: 'Validation error' }, { status: 400 }), + ); const mockRequest = new NextRequest('http://localhost/api/users/user-id', { method: 'GET', diff --git a/src/app/api/v1/users/[id]/route.ts b/src/app/api/v1/users/[id]/route.ts index 8b04465..4f5a6a6 100644 --- a/src/app/api/v1/users/[id]/route.ts +++ b/src/app/api/v1/users/[id]/route.ts @@ -1,12 +1,12 @@ -import { NextResponse } from "next/server"; +import { NextResponse } from 'next/server'; -import { NOT_FOUND } from "@/app/constants/http-constants"; -import { validateUser } from "@/app/utils/authentication"; -import { handleApiValidationError } from "@/app/utils/error-handler"; -import { container, UserRepositoryToken } from "@/container"; -import { IUserRepository } from "@/infrastructure/repositories/interfaces/user-repository"; -import { mapModelToDto } from "@/mappers/user-mapper"; -import { getUserUseCase } from "@/usecases/users/get-user"; +import { NOT_FOUND } from '@/app/constants/http-constants'; +import { validateUser } from '@/app/utils/authentication'; +import { handleApiValidationError } from '@/app/utils/error-handler'; +import { container, UserRepositoryToken } from '@/container'; +import { IUserRepository } from '@/infrastructure/repositories/interfaces/user-repository'; +import { mapModelToDto } from '@/mappers/user-mapper'; +import { getUserUseCase } from '@/usecases/users/get-user'; const repo = container.get(UserRepositoryToken); @@ -34,17 +34,15 @@ export async function GET( request: Request, { params }: { params: { id: string } }, ) { - try { validateUser(request, params.id); const result = await getUserUseCase({ repo }, { userId: params.id }); - if (result) return NextResponse.json(mapModelToDto(result), { status: 200 }); + if (result) + return NextResponse.json(mapModelToDto(result), { status: 200 }); return NextResponse.json({ message: NOT_FOUND }, { status: 404 }); - } - catch(error){ + } catch (error) { return handleApiValidationError(error); } - } diff --git a/src/app/utils/authentication.ts b/src/app/utils/authentication.ts index b844e06..0a839e3 100644 --- a/src/app/utils/authentication.ts +++ b/src/app/utils/authentication.ts @@ -1,33 +1,36 @@ -import { NextRequest } from "next/server"; -import { getToken } from "next-auth/jwt"; +import { NextRequest } from 'next/server'; +import { getToken } from 'next-auth/jwt'; -import { UNAUTHORIZED_REQUEST } from "../constants/http-constants"; +import { UNAUTHORIZED_REQUEST } from '../constants/http-constants'; -export interface UserProfile{ - name: string; - id: string; - email: string; +export interface UserProfile { + name: string; + id: string; + email: string; } -export const getUserProfile = async(req: Request): Promise => { - const token = await getToken({req: req as NextRequest}); +export const getUserProfile = async (req: Request): Promise => { + const token = await getToken({ req: req as NextRequest }); - return { name: token.name, id: token.sub, email: token.email }; -} + return { name: token.name, id: token.sub, email: token.email }; +}; -export const validateUser = async(req: Request, userId: string) => { +export const validateUser = async (req: Request, userId: string) => { const user = await getUserProfile(req); - if(user.id !== userId){ + if (user.id !== userId) { throw new AuthenticationError(); } return true; -} +}; export class AuthenticationError extends Error { - constructor(message: string = UNAUTHORIZED_REQUEST, public code: number = 401) { + constructor( + message: string = UNAUTHORIZED_REQUEST, + public code: number = 401, + ) { super(message); this.name = 'AuthenticationError'; } -} \ No newline at end of file +} diff --git a/src/app/utils/error-handler.ts b/src/app/utils/error-handler.ts index b3ff60f..aa26290 100644 --- a/src/app/utils/error-handler.ts +++ b/src/app/utils/error-handler.ts @@ -28,14 +28,13 @@ export function handleApiValidationError(error: unknown) { return NextResponse.json( { error: error.name, detail: error.message }, { status: error.code }, - ) - } else if(error instanceof AuthenticationError) { + ); + } else if (error instanceof AuthenticationError) { return NextResponse.json( { error: error.name, detail: error.message }, - { status: error.code } - ) - } - else { + { status: error.code }, + ); + } else { return NextResponse.json( { error: SERVER_ERROR, detail: error }, { status: 500 }, diff --git a/src/domain/dtos/server-config.ts b/src/domain/dtos/server-config.ts index a16fd8a..78964d9 100644 --- a/src/domain/dtos/server-config.ts +++ b/src/domain/dtos/server-config.ts @@ -17,7 +17,7 @@ * clientSecret: * type: string * description: A string representing a client's secret for the external endpoint. - * example: + * example: * required: false * tokenEndpoint: * type: string diff --git a/src/domain/dtos/shlink-access.ts b/src/domain/dtos/shlink-access.ts index 7ae6175..4af6e5e 100644 --- a/src/domain/dtos/shlink-access.ts +++ b/src/domain/dtos/shlink-access.ts @@ -11,7 +11,7 @@ * example: hWWNwskdGOnEdq0KIQ3S */ export class SHLinkAccessRequestDto { - managementToken: string + managementToken: string; } /** @@ -39,8 +39,8 @@ export class SHLinkAccessRequestDto { * example: 2024-09-09T09:45:00.479+00:00 */ export class SHLinkAccessDto { - shlinkId: string; - accessTime: Date; - recipient: string; - id?: string; -} \ No newline at end of file + shlinkId: string; + accessTime: Date; + recipient: string; + id?: string; +} diff --git a/src/domain/dtos/shlink-endpoint.ts b/src/domain/dtos/shlink-endpoint.ts index d2026c3..433d2b1 100644 --- a/src/domain/dtos/shlink-endpoint.ts +++ b/src/domain/dtos/shlink-endpoint.ts @@ -47,7 +47,7 @@ export class CreateSHLinkEndpointDto { * urlPath: * type: string * description: An optional string representing the share link endpoint's query parameters. - * example: + * example: */ export class SHLinkEndpointDto extends CreateSHLinkEndpointDto { id: string; diff --git a/src/domain/dtos/shlink.ts b/src/domain/dtos/shlink.ts index b11cca7..35ecea9 100644 --- a/src/domain/dtos/shlink.ts +++ b/src/domain/dtos/shlink.ts @@ -16,11 +16,11 @@ * configPasscode: * type: string * description: An optional string representing the share link's password. - * example: + * example: * configExp: * type: datetime * description: An optional date representing the date when the share link expires. - * example: + * example: */ export class CreateSHLinkDto { userId: string; @@ -77,11 +77,11 @@ export class SHLinkFileDto { * configPasscode: * type: string * description: An optional string representing the share link's password. - * example: + * example: * configExp: * type: datetime * description: An optional date representing the date when the share link expires. - * example: + * example: * passcodeFailureRemaining: * type: number * description: An integer representing how many passcode failure attempts are remaining for the share link. @@ -185,7 +185,7 @@ export class SHLinkRequestDto { * passcode: * type: string * description: An optional string representing the share link's new passcode. - * example: + * example: * managementToken: * type: string * description: A string representing the share link's unique management token used for managing the share link. @@ -193,7 +193,7 @@ export class SHLinkRequestDto { * oldPasscode: * type: string * description: An optional string representing the share link's old passcode. - * example: + * example: * expiryDate: * type: datetime * required: false diff --git a/src/domain/dtos/user.ts b/src/domain/dtos/user.ts index 4e15968..0cd60c8 100644 --- a/src/domain/dtos/user.ts +++ b/src/domain/dtos/user.ts @@ -1,4 +1,3 @@ - /** * @swagger * components: diff --git a/src/infrastructure/clients/prisma.ts b/src/infrastructure/clients/prisma.ts index 84fde0a..ae48330 100644 --- a/src/infrastructure/clients/prisma.ts +++ b/src/infrastructure/clients/prisma.ts @@ -2,9 +2,7 @@ import { PrismaClient } from '@prisma/client'; import { fieldEncryptionExtension } from 'prisma-field-encryption'; const prismaClientSingleton = () => { - return new PrismaClient().$extends( - fieldEncryptionExtension() - ); + return new PrismaClient().$extends(fieldEncryptionExtension()); }; declare const globalThis: { diff --git a/src/lib/swagger.ts b/src/lib/swagger.ts index bdfdceb..1105851 100644 --- a/src/lib/swagger.ts +++ b/src/lib/swagger.ts @@ -1,22 +1,22 @@ -import { createSwaggerSpec } from "next-swagger-doc"; +import { createSwaggerSpec } from 'next-swagger-doc'; export const getApiDocs = async () => { const spec = createSwaggerSpec({ - apiFolder: "./src/app/api/v1", // define api folder under app folder + apiFolder: './src/app/api/v1', // define api folder under app folder outputFile: '/swagger.json', schemaFolders: ['./src/domain/dtos'], definition: { - openapi: "3.0.0", + openapi: '3.0.0', info: { - title: "Share Link API", - version: "1.0", + title: 'Share Link API', + version: '1.0', }, components: { securitySchemes: { BearerAuth: { - type: "http", - scheme: "bearer", - bearerFormat: "JWT", + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', }, }, }, @@ -24,4 +24,4 @@ export const getApiDocs = async () => { }, }); return spec; -}; \ No newline at end of file +}; diff --git a/src/mappers/shlink-access-mapper.test.ts b/src/mappers/shlink-access-mapper.test.ts index c13e074..fc4f023 100644 --- a/src/mappers/shlink-access-mapper.test.ts +++ b/src/mappers/shlink-access-mapper.test.ts @@ -2,7 +2,11 @@ import { SHLinkAccessDto } from '@/domain/dtos/shlink-access'; import { SHLinkAccessModel } from '@/domain/models/shlink-access'; import { SHLinkAccessEntity } from '@/entities/shlink-access'; -import { mapModelToEntity, mapEntityToModel, mapModelToDto } from './shlink-access-mapper'; +import { + mapModelToEntity, + mapEntityToModel, + mapModelToDto, +} from './shlink-access-mapper'; // Mock the SHLinkAccessModel class jest.mock('@/domain/models/shlink-access', () => { @@ -70,7 +74,12 @@ describe('mapEntityToModel', () => { id: id, }; - const expectedModel = new SHLinkAccessModel(shlinkId, accessTime, recipient, id); + const expectedModel = new SHLinkAccessModel( + shlinkId, + accessTime, + recipient, + id, + ); const result = mapEntityToModel(entity); diff --git a/src/mappers/shlink-access-mapper.ts b/src/mappers/shlink-access-mapper.ts index f5f9e2a..68eb7f6 100644 --- a/src/mappers/shlink-access-mapper.ts +++ b/src/mappers/shlink-access-mapper.ts @@ -15,21 +15,28 @@ export const mapModelToEntity = ( : undefined; }; +export const mapEntityToModel = ( + shlinkAccessEntity?: SHLinkAccessEntity, +): SHLinkAccessModel | undefined => { + return shlinkAccessEntity + ? new SHLinkAccessModel( + shlinkAccessEntity.shlink_id, + shlinkAccessEntity.access_time, + shlinkAccessEntity.recipient, + shlinkAccessEntity.id, + ) + : undefined; +}; -export const mapEntityToModel = (shlinkAccessEntity?: SHLinkAccessEntity): SHLinkAccessModel | undefined => { - return shlinkAccessEntity ? new SHLinkAccessModel( - shlinkAccessEntity.shlink_id, - shlinkAccessEntity.access_time, - shlinkAccessEntity.recipient, - shlinkAccessEntity.id - ) : undefined; -} - -export const mapModelToDto = (shlinkAccessModel?: SHLinkAccessModel): SHLinkAccessDto | undefined => { - return shlinkAccessModel ? { - id: shlinkAccessModel.getId(), - shlinkId: shlinkAccessModel.getSHLinkId(), - accessTime: shlinkAccessModel.getAccessTime(), - recipient: shlinkAccessModel.getRecipient(), - } : undefined; -} +export const mapModelToDto = ( + shlinkAccessModel?: SHLinkAccessModel, +): SHLinkAccessDto | undefined => { + return shlinkAccessModel + ? { + id: shlinkAccessModel.getId(), + shlinkId: shlinkAccessModel.getSHLinkId(), + accessTime: shlinkAccessModel.getAccessTime(), + recipient: shlinkAccessModel.getRecipient(), + } + : undefined; +}; diff --git a/src/middleware.ts b/src/middleware.ts index 51cede7..b66de6f 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -2,17 +2,19 @@ import { NextResponse } from 'next/server'; import { getToken } from 'next-auth/jwt'; export async function middleware(req: Request) { - try{ - const token = await getToken({req: req as any}); - if(!token) throw new Error(); - + try { + const token = await getToken({ req: req as any }); + if (!token) throw new Error(); + return NextResponse.next(); - } - catch(error){ - return NextResponse.json({message: 'Invalid token or token expired'}, { status: 401 }); + } catch (error) { + return NextResponse.json( + { message: 'Invalid token or token expired' }, + { status: 401 }, + ); } } export const config = { matcher: '/api/v1/:path*', -} \ No newline at end of file +}; diff --git a/src/usecases/shlink-access/get-shlink-accesses.test.ts b/src/usecases/shlink-access/get-shlink-accesses.test.ts index 6e4f1df..03540fa 100644 --- a/src/usecases/shlink-access/get-shlink-accesses.test.ts +++ b/src/usecases/shlink-access/get-shlink-accesses.test.ts @@ -7,7 +7,7 @@ import { getSHLinkAccessesUseCase } from './get-shlink-accesses'; // Mock the mapping function and repository jest.mock('@/mappers/shlink-access-mapper', () => ({ - mapEntityToModel: jest.fn(), + mapEntityToModel: jest.fn(), })); jest.mock( @@ -19,7 +19,9 @@ jest.mock( describe('getSHLinkAccessesUseCase', () => { const mockFindMany = jest.fn(); - const mockRepo = { findMany: mockFindMany } as unknown as ISHLinkAccessRepository; + const mockRepo = { + findMany: mockFindMany, + } as unknown as ISHLinkAccessRepository; const mockEntityToModel = mapEntityToModel as jest.Mock; beforeEach(() => { @@ -31,36 +33,42 @@ describe('getSHLinkAccessesUseCase', () => { 'shlink-id', new Date('2024-01-01T00:00:00Z'), 'recipient@example.com', - '1' + '1', ); const accessEntity: SHLinkAccessEntity = { access_time: new Date('2024-01-01T00:00:00Z'), recipient: 'recipient@example.com', shlink_id: 'shlink-id', - id: '1' + id: '1', }; mockFindMany.mockReturnValue([accessEntity]); - await getSHLinkAccessesUseCase({ repo: mockRepo }, {shlinkId: 'shlink-id'}); + await getSHLinkAccessesUseCase( + { repo: mockRepo }, + { shlinkId: 'shlink-id' }, + ); // Assert that mapEntityToModel was called with the correct entity expect(mockEntityToModel).toHaveBeenCalledWith(accessEntity); // Assert that repo.findMany was called with the mapped correct filter. - expect(mockFindMany).toHaveBeenCalledWith({ shlink_id: 'shlink-id'}); + expect(mockFindMany).toHaveBeenCalledWith({ shlink_id: 'shlink-id' }); }); it('should handle cases where findMany returns an empty array', async () => { mockFindMany.mockReturnValue([]); - await getSHLinkAccessesUseCase({ repo: mockRepo }, { shlinkId: 'shlink-id'}); + await getSHLinkAccessesUseCase( + { repo: mockRepo }, + { shlinkId: 'shlink-id' }, + ); // Assert that mapModelToEntity was called with the correct model expect(mockEntityToModel).not.toHaveBeenCalled(); // Assert that repo.insert was called with undefined - expect(mockFindMany).toHaveBeenCalledWith({ shlink_id: 'shlink-id'}); + expect(mockFindMany).toHaveBeenCalledWith({ shlink_id: 'shlink-id' }); }); }); diff --git a/src/usecases/shlink-access/get-shlink-accesses.ts b/src/usecases/shlink-access/get-shlink-accesses.ts index d24b11a..f132652 100644 --- a/src/usecases/shlink-access/get-shlink-accesses.ts +++ b/src/usecases/shlink-access/get-shlink-accesses.ts @@ -1,9 +1,14 @@ -import { SHLinkAccessModel } from "@/domain/models/shlink-access"; -import { ISHLinkAccessRepository } from "@/infrastructure/repositories/interfaces/shlink-access-repository"; -import { mapEntityToModel } from "@/mappers/shlink-access-mapper"; +import { SHLinkAccessModel } from '@/domain/models/shlink-access'; +import { ISHLinkAccessRepository } from '@/infrastructure/repositories/interfaces/shlink-access-repository'; +import { mapEntityToModel } from '@/mappers/shlink-access-mapper'; -export const getSHLinkAccessesUseCase = async(context: { repo: ISHLinkAccessRepository }, data: { shlinkId: string }): Promise => { - const shlinkAccesses = await context.repo.findMany({ shlink_id: data.shlinkId }); - - return shlinkAccesses.map(x => mapEntityToModel(x)); -} +export const getSHLinkAccessesUseCase = async ( + context: { repo: ISHLinkAccessRepository }, + data: { shlinkId: string }, +): Promise => { + const shlinkAccesses = await context.repo.findMany({ + shlink_id: data.shlinkId, + }); + + return shlinkAccesses.map((x) => mapEntityToModel(x)); +}; diff --git a/src/usecases/shlinks/deactivate-shlink.test.ts b/src/usecases/shlinks/deactivate-shlink.test.ts index 40d2f17..18e23ff 100644 --- a/src/usecases/shlinks/deactivate-shlink.test.ts +++ b/src/usecases/shlinks/deactivate-shlink.test.ts @@ -2,12 +2,12 @@ * @jest-environment node */ -import { SHLinkModel } from "@/domain/models/shlink"; -import { SHLinkEntity } from "@/entities/shlink"; -import { ISHLinkRepository } from "@/infrastructure/repositories/interfaces/shlink-repository"; -import { mapEntityToModel } from "@/mappers/shlink-mapper"; +import { SHLinkModel } from '@/domain/models/shlink'; +import { SHLinkEntity } from '@/entities/shlink'; +import { ISHLinkRepository } from '@/infrastructure/repositories/interfaces/shlink-repository'; +import { mapEntityToModel } from '@/mappers/shlink-mapper'; -import { deactivateSHLinksUseCase } from "./deactivate-shlink"; +import { deactivateSHLinksUseCase } from './deactivate-shlink'; // Mock the dependencies jest.mock('@/mappers/shlink-mapper', () => ({ @@ -66,20 +66,29 @@ describe('deactivateSHLinksUseCase', () => { (mapEntityToModel as jest.Mock).mockReturnValue(mockReturnedSHLinkModel); }); - it("should call the repository's findById method with the correct id", async () => { - await deactivateSHLinksUseCase(mockContext, { id: mockId, user: { id: 'user-123567', name: '', email: ''} }); + it("should call the repository's findById method with the correct id", async () => { + await deactivateSHLinksUseCase(mockContext, { + id: mockId, + user: { id: 'user-123567', name: '', email: '' }, + }); expect(mockRepo.findById).toHaveBeenCalledWith(mockId); }); - it("should deactivate the SHLinkEntity and update it in the repository", async () => { - await deactivateSHLinksUseCase(mockContext, { id: mockId, user: { id: 'user-123567', name: '', email: ''} }); + it('should deactivate the SHLinkEntity and update it in the repository', async () => { + await deactivateSHLinksUseCase(mockContext, { + id: mockId, + user: { id: 'user-123567', name: '', email: '' }, + }); expect(mockRepo.update).toHaveBeenCalledWith(mockUpdatedSHLinkEntity); }); - it("should map the updated SHLinkEntity back to SHLinkModel", async () => { - const result = await deactivateSHLinksUseCase(mockContext, { id: mockId, user: { id: 'user-123567', name: '', email: ''} }); + it('should map the updated SHLinkEntity back to SHLinkModel', async () => { + const result = await deactivateSHLinksUseCase(mockContext, { + id: mockId, + user: { id: 'user-123567', name: '', email: '' }, + }); expect(mapEntityToModel).toHaveBeenCalledWith(mockUpdatedSHLinkEntity); expect(result).toBe(mockReturnedSHLinkModel); @@ -89,11 +98,13 @@ describe('deactivateSHLinksUseCase', () => { const error = new Error('Test error'); mockRepo.findById.mockRejectedValue(error); + await expect( + deactivateSHLinksUseCase(mockContext, { + id: mockId, + user: { id: 'user-123567', name: '', email: '' }, + }), + ).rejects.toThrow(error); - - await expect(deactivateSHLinksUseCase(mockContext, { id: mockId, user: { id: 'user-123567', name: '', email: ''} })).rejects - .toThrow(error); - expect(mockRepo.findById).toHaveBeenCalledWith(mockId); }); }); diff --git a/src/usecases/shlinks/deactivate-shlink.ts b/src/usecases/shlinks/deactivate-shlink.ts index 40fa2da..2bd9d43 100644 --- a/src/usecases/shlinks/deactivate-shlink.ts +++ b/src/usecases/shlinks/deactivate-shlink.ts @@ -1,22 +1,25 @@ -import { AuthenticationError, UserProfile } from "@/app/utils/authentication"; -import { SHLinkModel } from "@/domain/models/shlink"; -import { SHLinkEntity } from "@/entities/shlink"; -import { ISHLinkRepository } from "@/infrastructure/repositories/interfaces/shlink-repository"; -import { mapEntityToModel } from "@/mappers/shlink-mapper"; +import { AuthenticationError, UserProfile } from '@/app/utils/authentication'; +import { SHLinkModel } from '@/domain/models/shlink'; +import { SHLinkEntity } from '@/entities/shlink'; +import { ISHLinkRepository } from '@/infrastructure/repositories/interfaces/shlink-repository'; +import { mapEntityToModel } from '@/mappers/shlink-mapper'; export const deactivateSHLinksUseCase = async ( context: { repo: ISHLinkRepository }, - data: { id: string, user: UserProfile }, + data: { id: string; user: UserProfile }, ): Promise => { const entity = await context.repo.findById(data.id); let newShlink: SHLinkEntity; - if(entity) { - if(data.user.id !== entity.user_id) throw new AuthenticationError('User not authorized to deactivate shlink.'); + if (entity) { + if (data.user.id !== entity.user_id) + throw new AuthenticationError( + 'User not authorized to deactivate shlink.', + ); - entity.active = false; - newShlink = await context.repo.update(entity); + entity.active = false; + newShlink = await context.repo.update(entity); } return mapEntityToModel(newShlink); -} +}; diff --git a/src/usecases/shlinks/get-shlink.test.ts b/src/usecases/shlinks/get-shlink.test.ts index fde3cb9..44838f1 100644 --- a/src/usecases/shlinks/get-shlink.test.ts +++ b/src/usecases/shlinks/get-shlink.test.ts @@ -45,8 +45,26 @@ describe('getSHLinkUseCase', () => { }, ]; mockModels = [ - new SHLinkModel(mockUserId, 'name 1', 3, true, 'token-xyz1234', 'passcode-abcde',new Date('2024-01-01T00:00:00Z'), '1'), - new SHLinkModel(mockUserId, 'name 2', 1, false, 'token-uvw5678', 'passcode-fghij', new Date('2024-06-01T00:00:00Z'), '2') + new SHLinkModel( + mockUserId, + 'name 1', + 3, + true, + 'token-xyz1234', + 'passcode-abcde', + new Date('2024-01-01T00:00:00Z'), + '1', + ), + new SHLinkModel( + mockUserId, + 'name 2', + 1, + false, + 'token-uvw5678', + 'passcode-fghij', + new Date('2024-06-01T00:00:00Z'), + '2', + ), ]; // Set up mock implementations (mockRepo.findMany as jest.Mock).mockResolvedValue(mockSHLinkEntities); diff --git a/src/usecases/shlinks/get-shlink.ts b/src/usecases/shlinks/get-shlink.ts index 4663b19..0d29de0 100644 --- a/src/usecases/shlinks/get-shlink.ts +++ b/src/usecases/shlinks/get-shlink.ts @@ -8,5 +8,5 @@ export const getSHLinkUseCase = async ( ): Promise => { const entities = await context.repo.findMany({ user_id: data.user_id }); - return entities.map(x => mapEntityToModel(x)); + return entities.map((x) => mapEntityToModel(x)); }; From 2aa85762714d44362a2d5b1ee2d36275d48bbdc2 Mon Sep 17 00:00:00 2001 From: jacob-khoza-symb <157815500+jacob-khoza-symb@users.noreply.github.com> Date: Thu, 12 Sep 2024 10:28:48 +0200 Subject: [PATCH 3/4] Ensuring that the prismaClientSingleton returns a prisma client. --- .env | 22 +++++++++++++++------- docker-compose.yml | 4 ++-- src/infrastructure/clients/prisma.ts | 4 +++- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.env b/.env index 9d3b0d1..50d1085 100644 --- a/.env +++ b/.env @@ -1,8 +1,16 @@ -POSTGRES_DB=replaceme -POSTGRES_USER=replaceme -POSTGRES_PASSWORD=replaceme -KEYCLOAK_ADMIN_PASSWORD=replaceme -KEYCLOAK_ADMIN=replaceme -EXTERNAL_URL=replaceme PRISMA_FIELD_ENCRYPTION_KEY=k1.aesgcm256.DbQoar8ZLuUsOHZNyrnjlskInHDYlzF3q6y1KGM7DUN= -PRISMA_FIELD_ENCRYPTION_HASH_SALT=replaceme +PRISMA_FIELD_ENCRYPTION_HASH_SALT=0be97e77063ea3f7a0f128b06ef9b1eg +POSTGRES_DB=postgres +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +KEYCLOAK_ADMIN_PASSWORD=admin +KEYCLOAK_ADMIN=admin +EXTERNAL_URL=http://localhost:3001 +KEYCLOAK_ADMIN_PASSWORD=password +KEYCLOAK_CLIENT_ID="nextjs" +KEYCLOAK_CLIENT_SECRET="gX0nfIp57bO3bY68br3kdTXung2Auwpr" +KEYCLOAK_ISSUER="http://localhost:8082/realms/shlp" +NEXTAUTH_URL="http://localhost:3000" +NEXTAUTH_SECRET="ArmMS4LBmH3VTR77UrSAY2lPs04HO0Nk2/4BcU0jMvc=" +NEXT_CONTAINER_KEYCLOAK_ENDPOINT="http://localhost:8082" +NEXT_LOCAL_KEYCLOAK_URL="http://localhost:8082" diff --git a/docker-compose.yml b/docker-compose.yml index 1e45a8c..50e7d2d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: - ./public:/app/public - /app/.next ports: - - "3000:3000" + - "3080:3000" networks: - smart-health-links-portal-network healthcheck: @@ -50,7 +50,7 @@ services: KC_DB_USERNAME: ${POSTGRES_USER} KC_DB_PASSWORD: ${POSTGRES_PASSWORD} ports: - - 8080:8080 + - 8082:8080 volumes: - ./import/config/shlp:/opt/keycloak/data/import restart: always diff --git a/src/infrastructure/clients/prisma.ts b/src/infrastructure/clients/prisma.ts index ae48330..c4a29a3 100644 --- a/src/infrastructure/clients/prisma.ts +++ b/src/infrastructure/clients/prisma.ts @@ -2,7 +2,9 @@ import { PrismaClient } from '@prisma/client'; import { fieldEncryptionExtension } from 'prisma-field-encryption'; const prismaClientSingleton = () => { - return new PrismaClient().$extends(fieldEncryptionExtension()); + return new PrismaClient().$extends( + fieldEncryptionExtension(), + ) as unknown as PrismaClient; }; declare const globalThis: { From a4013d6404448798cf9a23765fd91671acc09705 Mon Sep 17 00:00:00 2001 From: Martin Brocker Date: Thu, 12 Sep 2024 11:06:21 +0200 Subject: [PATCH 4/4] Remove duplciate env (keycloak_admin_password) --- .env | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.env b/.env index 50d1085..bc67c9a 100644 --- a/.env +++ b/.env @@ -3,9 +3,8 @@ PRISMA_FIELD_ENCRYPTION_HASH_SALT=0be97e77063ea3f7a0f128b06ef9b1eg POSTGRES_DB=postgres POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres -KEYCLOAK_ADMIN_PASSWORD=admin -KEYCLOAK_ADMIN=admin EXTERNAL_URL=http://localhost:3001 +KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=password KEYCLOAK_CLIENT_ID="nextjs" KEYCLOAK_CLIENT_SECRET="gX0nfIp57bO3bY68br3kdTXung2Auwpr"