Skip to content

Commit e47b5f6

Browse files
authored
feat: Create an Auth Guard in API (#76)
* feat: create UnauthorizedError * feat: implement Next Auth in handleApiRequest * refactor: create HandleApiRequestArgs * feat: implement new handleApiRequest * docs: add unauthorized error * refactor: move test token into needToBeAuthenticated condition * fix: handleApiRequest tests * fix: linter * test: Create tests for BusinessError * test: recreate handle-api-request.test.ts * fix: handleApiRequest import * fix: mocks * fix: test * test: create tests for HashService * fix: test * test: add tests for SignInUseCase * test: add tests for SignInUseCase * test: add tests for handlePrismaError * fix: linter * fix: Makefile
1 parent 43b14bc commit e47b5f6

File tree

11 files changed

+656
-101
lines changed

11 files changed

+656
-101
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,5 @@ clean:
6565
# Aliases
6666
run: up dev
6767
checks: lint tests
68-
ci: checks build
68+
ci: lint coverage build
6969
.PHONY: run checks ci

src/app/api/auth/register/route.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,14 @@ export async function POST(
4646
): Promise<NextResponse<HttpResponseDto<RegisterDataDto>>> {
4747
const payload: RegisterPayloadDto = await request.json();
4848
const registerUseCase: RegisterUseCase = container.resolve(RegisterUseCase);
49-
return await handleApiRequest<RegisterDataDto>(async () => {
50-
const userCreated: UserModelDto = await registerUseCase.handle(payload);
51-
const response: RegisterDataDto = { userCreated };
52-
return response;
53-
}, StatusCodes.CREATED);
49+
return await handleApiRequest<RegisterDataDto>({
50+
request: request,
51+
needToBeAuthenticated: false,
52+
callback: async () => {
53+
const userCreated: UserModelDto = await registerUseCase.handle(payload);
54+
const response: RegisterDataDto = { userCreated };
55+
return response;
56+
},
57+
successStatusCode: StatusCodes.CREATED,
58+
});
5459
}

src/app/api/vaults/[id]/route.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ import type { HttpOptions } from '@shared/dto/input/options/abstract/http-option
2929
* application/json:
3030
* schema:
3131
* $ref: '#/components/schemas/HttpErrorDto'
32+
* 401:
33+
* description: Unauthorized
34+
* content:
35+
* application/json:
36+
* schema:
37+
* $ref: '#/components/schemas/HttpErrorDto'
3238
* 500:
3339
* description: Internal Server Error
3440
* content:
@@ -42,8 +48,10 @@ export async function DELETE(
4248
): Promise<NextResponse> {
4349
const deleteVaultUseCase: DeleteVaultUseCase =
4450
container.resolve(DeleteVaultUseCase);
45-
return await handleApiRequest<void>(
46-
async () => deleteVaultUseCase.handle(await options.params),
47-
StatusCodes.NO_CONTENT
48-
);
51+
return await handleApiRequest<void>({
52+
request: request,
53+
needToBeAuthenticated: true,
54+
callback: async () => deleteVaultUseCase.handle(await options.params),
55+
successStatusCode: StatusCodes.NO_CONTENT,
56+
});
4957
}

src/app/api/vaults/route.ts

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,32 @@ import type { CreateVaultPayloadDto } from '@shared/dto/input/payloads/create-va
2424
* application/json:
2525
* schema:
2626
* $ref: '#/components/schemas/GetMyVaultsBodyDto'
27+
* 401:
28+
* description: Unauthorized
29+
* content:
30+
* application/json:
31+
* schema:
32+
* $ref: '#/components/schemas/HttpErrorDto'
2733
* 500:
2834
* description: Internal Server Error
2935
* content:
3036
* application/json:
3137
* schema:
3238
* $ref: '#/components/schemas/HttpErrorDto'
3339
*/
34-
export async function GET(): Promise<
35-
NextResponse<HttpResponseDto<GetMyVaultsDataDto>>
36-
> {
40+
export async function GET(
41+
request: NextRequest
42+
): Promise<NextResponse<HttpResponseDto<GetMyVaultsDataDto>>> {
3743
const getMyVaultsUseCase: GetMyVaultsUseCase =
3844
container.resolve(GetMyVaultsUseCase);
39-
return await handleApiRequest<GetMyVaultsDataDto>(async () => {
40-
const myVaults: VaultModelDto[] = await getMyVaultsUseCase.handle();
41-
const response: GetMyVaultsDataDto = { myVaults };
42-
return response;
45+
return await handleApiRequest<GetMyVaultsDataDto>({
46+
request: request,
47+
needToBeAuthenticated: true,
48+
callback: async () => {
49+
const myVaults: VaultModelDto[] = await getMyVaultsUseCase.handle();
50+
const response: GetMyVaultsDataDto = { myVaults };
51+
return response;
52+
},
4353
});
4454
}
4555

@@ -74,6 +84,12 @@ export async function GET(): Promise<
7484
* application/json:
7585
* schema:
7686
* $ref: '#/components/schemas/BusinessErrorDto'
87+
* 401:
88+
* description: Unauthorized
89+
* content:
90+
* application/json:
91+
* schema:
92+
* $ref: '#/components/schemas/HttpErrorDto'
7793
* 500:
7894
* description: Internal Server Error
7995
* content:
@@ -87,10 +103,15 @@ export async function POST(
87103
const payload: CreateVaultPayloadDto = await request.json();
88104
const createVaultUseCase: CreateVaultUseCase =
89105
container.resolve(CreateVaultUseCase);
90-
return await handleApiRequest<CreateVaultDataDto>(async () => {
91-
const vaultCreated: VaultModelDto =
92-
await createVaultUseCase.handle(payload);
93-
const response: CreateVaultDataDto = { vaultCreated };
94-
return response;
95-
}, StatusCodes.CREATED);
106+
return await handleApiRequest<CreateVaultDataDto>({
107+
request: request,
108+
needToBeAuthenticated: true,
109+
callback: async () => {
110+
const vaultCreated: VaultModelDto =
111+
await createVaultUseCase.handle(payload);
112+
const response: CreateVaultDataDto = { vaultCreated };
113+
return response;
114+
},
115+
successStatusCode: StatusCodes.CREATED,
116+
});
96117
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { HttpError } from '@shared/errors/http-error';
2+
import { StatusCodes } from 'http-status-codes';
3+
4+
export class UnauthorizedError extends HttpError {
5+
public constructor() {
6+
super('Unauthorized', StatusCodes.UNAUTHORIZED);
7+
}
8+
}

src/modules/api/helpers/api/handle-api-request.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,54 @@
11
import { HttpError } from '@shared/errors/http-error';
22
import { StatusCodes } from 'http-status-codes';
3+
import type { NextRequest } from 'next/server';
34
import { NextResponse } from 'next/server';
45
import type { HttpResponseDto } from '@shared/dto/output/responses/abstract/http.response.dto';
56
import { ApiLogger } from '@api/logs/api.logger';
67
import { InternalServerError } from '@api/errors/http/internal-server.error';
78
import { BusinessError } from '@shared/errors/business-error';
9+
import type { JWT } from 'next-auth/jwt';
10+
import { getToken } from 'next-auth/jwt';
11+
import { UnauthorizedError } from '@api/errors/http/unauthorized.error';
12+
import type { Session } from 'next-auth';
13+
import { getServerSession } from 'next-auth';
14+
import { authOptions } from '@lib/auth';
15+
16+
export type HandleApiRequestArgs<Data> = {
17+
request: NextRequest;
18+
needToBeAuthenticated: boolean;
19+
callback: () => Promise<Data>;
20+
successStatusCode?: StatusCodes;
21+
};
822

923
export async function handleApiRequest<Data>(
10-
callback: () => Promise<Data>,
11-
successStatusCode?: StatusCodes
24+
args: HandleApiRequestArgs<Data>
1225
): Promise<NextResponse<HttpResponseDto<Data>>> {
1326
try {
14-
const data: Awaited<Data> = await callback();
27+
if (args.needToBeAuthenticated) {
28+
const token: JWT | null = await getToken({
29+
req: args.request,
30+
secret: process.env.NEXTAUTH_SECRET,
31+
});
32+
if (!token) {
33+
throw new UnauthorizedError();
34+
}
35+
36+
const session: Session | null = await getServerSession(authOptions);
37+
if (!session) {
38+
throw new UnauthorizedError();
39+
}
40+
}
41+
42+
const data: Awaited<Data> = await args.callback();
1543

16-
if (successStatusCode === StatusCodes.NO_CONTENT) {
44+
if (args.successStatusCode === StatusCodes.NO_CONTENT) {
1745
return new NextResponse(null, { status: StatusCodes.NO_CONTENT });
1846
}
1947

2048
return NextResponse.json(
2149
{ data },
2250
{
23-
status: successStatusCode || StatusCodes.OK,
51+
status: args.successStatusCode || StatusCodes.OK,
2452
}
2553
);
2654
} catch (error: unknown) {

0 commit comments

Comments
 (0)