Skip to content
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
a15cc88
refactor: clean legacy error handler
vbetsch Aug 2, 2025
c2c416f
refactor: rename utils folder to helpers
vbetsch Aug 2, 2025
8b9fe8e
feat: implement handlePrismaError
vbetsch Aug 2, 2025
203d874
fix: type error
vbetsch Aug 2, 2025
df0395b
fix: remove dots
vbetsch Aug 2, 2025
e9d6368
docs: update delete api doc
vbetsch Aug 2, 2025
9848cf0
docs: update delete api doc
vbetsch Aug 2, 2025
2f84004
fix: handle not handled prisma errors
vbetsch Aug 2, 2025
32ac4b7
feat: add P2000 error
vbetsch Aug 2, 2025
8402b72
docs: update create vault doc
vbetsch Aug 2, 2025
9422bca
refactor: use StatusCodes
vbetsch Aug 2, 2025
7eb2c60
refactor: move errors into shared
vbetsch Aug 2, 2025
e90080b
refactor: add typing for handleApiRequest
vbetsch Aug 2, 2025
4bf7f04
revert: re-move HttpError in backend
vbetsch Aug 2, 2025
0c7f6fc
revert: re-move HttpError in backend
vbetsch Aug 2, 2025
a0cba5a
feat: create InternalServerError
vbetsch Aug 2, 2025
10eba15
feat: implement new error handling
vbetsch Aug 2, 2025
ee35509
docs: add doc comment
vbetsch Aug 2, 2025
a26a33f
refactor: rewrite errors dto
vbetsch Aug 2, 2025
a9ce27c
feat: Rewrite DTO (bodies, data, responses, errors)
vbetsch Aug 2, 2025
9550bc0
fix: data doc
vbetsch Aug 2, 2025
eda8baf
fix: api typing
vbetsch Aug 2, 2025
12fba64
fix: api docs typing
vbetsch Aug 2, 2025
afd8a70
fix: api docs typing
vbetsch Aug 2, 2025
372aa96
feat: implement new error handling in ui
vbetsch Aug 2, 2025
cf03e4b
fix: error message display in modal
vbetsch Aug 2, 2025
c40df33
refactor: improve requests services
vbetsch Aug 2, 2025
548de72
refactor: improve requests services
vbetsch Aug 2, 2025
683dc05
fix: body already consumed
vbetsch Aug 2, 2025
61befa6
Revert "fix: body already consumed"
vbetsch Aug 2, 2025
4f34d0a
Revert "refactor: improve requests services"
vbetsch Aug 2, 2025
28d0a3e
Revert "refactor: improve requests services"
vbetsch Aug 2, 2025
8b47570
fix: json parsing error while deleting
vbetsch Aug 2, 2025
431f460
fix: remove length limit on secret on db
vbetsch Aug 2, 2025
f30890b
feat: implement VaultAlreadyExistsError
vbetsch Aug 2, 2025
9170ba5
fix: async on parsing json on request service
vbetsch Aug 2, 2025
147dd4f
refactor: get status of response in frontend
vbetsch Aug 2, 2025
a052bcb
refactor: define options and params
vbetsch Aug 2, 2025
1516c31
refactor: tidy up dto by input or output
vbetsch Aug 2, 2025
f8cc307
chore(linter): improve 'no-restricted-imports'
vbetsch Aug 2, 2025
1c07d78
feat: add createdAt in vault table
vbetsch Aug 2, 2025
f009805
feat: order the vaults list by creation date
vbetsch Aug 2, 2025
a299184
feat: create Logger
vbetsch Aug 3, 2025
90dc96a
feat: make Logger static
vbetsch Aug 3, 2025
71ba118
fix: set LoggerTagEnum to string
vbetsch Aug 3, 2025
0295417
refactor: use Logger in api
vbetsch Aug 3, 2025
cc7d61e
refactor: use Logger in UI
vbetsch Aug 3, 2025
2654560
refactor: create ApiLogger & UiLogger
vbetsch Aug 3, 2025
e4e52ba
refactor: add error objects in console errors
vbetsch Aug 3, 2025
1070559
style: remove prefix in ui logger
vbetsch Aug 3, 2025
ffe7ff3
fix: linter
vbetsch Aug 3, 2025
9cad491
fix: error message tests
vbetsch Aug 3, 2025
d6ac764
fix: handleApiRequest tests
vbetsch Aug 3, 2025
1f5423f
fix: useApi tests
vbetsch Aug 3, 2025
3465b0f
fix: RequestService tests
vbetsch Aug 3, 2025
4e9d38f
test: Create Logger tests
vbetsch Aug 3, 2025
4d888ae
fix: Rename utils to helpers in tests
vbetsch Aug 3, 2025
5607b46
fix: Move request.service.test.ts to abstract folder in tests
vbetsch Aug 3, 2025
0d16778
test: Create ApiLogger
vbetsch Aug 3, 2025
845df46
test: Create UiLogger tests
vbetsch Aug 3, 2025
a655e76
fix: linter
vbetsch Aug 3, 2025
d459e37
fix: linter
vbetsch Aug 3, 2025
2eb953f
fix: linter in request service test
vbetsch Aug 3, 2025
1c61053
fix: linter in request logger
vbetsch Aug 3, 2025
154ff6e
fix: linter in api logger
vbetsch Aug 3, 2025
7ddd5e2
fix: linter in handleApiRequest
vbetsch Aug 3, 2025
7e2f871
fix: typo
vbetsch Aug 3, 2025
b12c5c4
fix: useless tag
vbetsch Aug 3, 2025
afb0d6d
fix: logger tests
vbetsch Aug 3, 2025
c670176
fix: api logger tests
vbetsch Aug 3, 2025
4afae12
fix: ui logger tests
vbetsch Aug 3, 2025
14670a7
fix: handleApiRequest tests
vbetsch Aug 3, 2025
8d83afe
fix: ApiLogger tests
vbetsch Aug 3, 2025
9777925
fix: handleApiRequest tests
vbetsch Aug 3, 2025
7883781
style: Format ESLint
vbetsch Aug 3, 2025
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
40 changes: 26 additions & 14 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import { FlatCompat } from '@eslint/eslintrc';
import {dirname} from 'path';
import {fileURLToPath} from 'url';
import {FlatCompat} from '@eslint/eslintrc';
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import eslintPluginPrettier from 'eslint-plugin-prettier';
Expand Down Expand Up @@ -65,13 +65,13 @@ export default tseslint.config(
'no-inline-comments': 'warn',
'no-undefined': 'warn',
'no-var': 'error',
'prefer-const': ['error', { destructuring: 'all' }],
'prefer-const': ['error', {destructuring: 'all'}],
'require-await': 'error',
'require-object-destructuring': 'off',
'arrow-parens': ['error', 'as-needed'],
/* Formatting */
'max-len': ['warn', { code: 300, ignoreUrls: true }],
'prettier/prettier': ['warn', { semi: true }],
'max-len': ['warn', {code: 300, ignoreUrls: true}],
'prettier/prettier': ['warn', {semi: true}],
semi: ['error', 'always'],

/* Naming conventions */
Expand Down Expand Up @@ -122,29 +122,29 @@ export default tseslint.config(
],
'@typescript-eslint/explicit-function-return-type': [
'error',
{ allowExpressions: false },
{allowExpressions: false},
],
'@typescript-eslint/explicit-member-accessibility': [
'error',
{ accessibility: 'explicit' },
{accessibility: 'explicit'},
],
'@typescript-eslint/explicit-module-boundary-types': 'error',
'@typescript-eslint/no-empty-function': ['warn'],
'@typescript-eslint/no-extraneous-class': [
'error',
{ allowConstructorOnly: false },
{allowConstructorOnly: false},
],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/no-magic-numbers': [
'warn',
{ ignoreEnums: true, ignore: [0, 1], enforceConst: true },
{ignoreEnums: true, ignore: [0, 1], enforceConst: true},
],
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_' },
{argsIgnorePattern: '^_'},
],
'@typescript-eslint/prefer-function-type': 'warn',
'@typescript-eslint/prefer-readonly': 'warn',
Expand All @@ -169,7 +169,7 @@ export default tseslint.config(
'no-restricted-imports': [
'error',
{
patterns: ['@api/*'],
patterns: ['@api/*', '@prisma/*'],
},
],
},
Expand All @@ -187,11 +187,23 @@ export default tseslint.config(
},
},

{
files: ['src/modules/shared/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: ['@api/*', '@ui/*', '@prisma/*'],
},
],
},
},

{
files: ['**/*.test.ts', '**/*.spec.ts', '**/*.test.tsx', '**/*.spec.tsx'],
plugins: { jest: eslintPluginJest },
plugins: {jest: eslintPluginJest},
settings: {
jest: { version: 29 },
jest: {version: 29},
},
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Vault" ALTER COLUMN "secret" SET DATA TYPE TEXT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Vault" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
7 changes: 4 additions & 3 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ datasource db {
}

model Vault {
uuid String @id @default(uuid())
label String @db.VarChar(255)
secret String @db.VarChar(255)
uuid String @id @default(uuid())
label String @db.VarChar(255)
secret String
createdAt DateTime @default(now())
}
19 changes: 12 additions & 7 deletions src/app/api/vaults/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import 'reflect-metadata';
import type { NextRequest, NextResponse } from 'next/server';
import type { IdParam } from '@shared/dto/params/id.param';
import { container } from 'tsyringe';
import { handleApiRequest } from '@api/utils/handle-api-request';
import { handleApiRequest } from '@api/helpers/handle-api-request';
import { StatusCodes } from 'http-status-codes';
import { DeleteVaultUseCase } from '@api/usecases/vaults/delete-vault.usecase';
import type { CreateVaultParams } from '@shared/dto/input/params/create-vault.params';
import type { HttpOptions } from '@shared/dto/input/options/abstract/http-options';

/**
* @swagger
Expand All @@ -24,22 +25,26 @@ import { DeleteVaultUseCase } from '@api/usecases/vaults/delete-vault.usecase';
* 204:
* description: The vault has been successfully deleted
* 404:
* description: Vault not found
* description: Resource not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/HttpErrorDto'
* 500:
* description: Internal Server Error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/HttpResponseDto'
* $ref: '#/components/schemas/HttpErrorDto'
*/
export async function DELETE(
request: NextRequest,
params: IdParam
options: HttpOptions<CreateVaultParams>
): Promise<NextResponse> {
const deleteVaultUseCase: DeleteVaultUseCase =
container.resolve(DeleteVaultUseCase);
return await handleApiRequest(
() => deleteVaultUseCase.handle(params),
return await handleApiRequest<void>(
async () => deleteVaultUseCase.handle(await options.params),
StatusCodes.NO_CONTENT
);
}
39 changes: 25 additions & 14 deletions src/app/api/vaults/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import 'reflect-metadata';
import type { NextRequest, NextResponse } from 'next/server';
import { container } from 'tsyringe';
import { handleApiRequest } from '@api/utils/handle-api-request';
import { handleApiRequest } from '@api/helpers/handle-api-request';
import type { VaultModelDto } from '@shared/dto/models/vault.model.dto';
import type { CreateVaultResponseDto } from '@shared/dto/responses/create-vault.response.dto';
import { CreateVaultUseCase } from '@api/usecases/vaults/create-vault.usecase';
import type { GetMyVaultsResponseDto } from '@shared/dto/responses/get-my-vaults.response.dto';
import { GetMyVaultsUseCase } from '@api/usecases/vaults/get-my-vaults.usecase';
import type { CreateVaultRequestDto } from '@shared/dto/requests/create-vault.request.dto';
import type { CreateVaultRequestDto } from '@shared/dto/input/requests/create-vault.request.dto';
import { StatusCodes } from 'http-status-codes';
import type { CreateVaultDataDto } from '@shared/dto/output/data/create-vault.data.dto';
import type { GetMyVaultsDataDto } from '@shared/dto/output/data/get-my-vaults.data.dto';
import type { HttpResponseDto } from '@shared/dto/output/responses/abstract/http.response.dto';

/**
* @swagger
Expand All @@ -23,20 +24,22 @@ import { StatusCodes } from 'http-status-codes';
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/GetMyVaultsResponseDto'
* $ref: '#/components/schemas/GetMyVaultsBodyDto'
* 500:
* description: Internal Server Error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/HttpResponseDto'
* $ref: '#/components/schemas/HttpErrorDto'
*/
export async function GET(): Promise<NextResponse> {
export async function GET(): Promise<
NextResponse<HttpResponseDto<GetMyVaultsDataDto>>
> {
const getMyVaultsUseCase: GetMyVaultsUseCase =
container.resolve(GetMyVaultsUseCase);
return await handleApiRequest(async () => {
return await handleApiRequest<GetMyVaultsDataDto>(async () => {
const myVaults: VaultModelDto[] = await getMyVaultsUseCase.handle();
const response: GetMyVaultsResponseDto = { myVaults };
const response: GetMyVaultsDataDto = { myVaults };
return response;
});
}
Expand All @@ -60,21 +63,29 @@ export async function GET(): Promise<NextResponse> {
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateVaultResponseDto'
* $ref: '#/components/schemas/CreateVaultBodyDto'
* 400:
* description: One of the requested values is too long
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/HttpErrorDto'
* 500:
* description: Internal Server Error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/HttpResponseDto'
* $ref: '#/components/schemas/HttpErrorDto'
*/
export async function POST(request: NextRequest): Promise<NextResponse> {
export async function POST(
request: NextRequest
): Promise<NextResponse<HttpResponseDto<CreateVaultDataDto>>> {
const params: CreateVaultRequestDto = await request.json();
const createVaultUseCase: CreateVaultUseCase =
container.resolve(CreateVaultUseCase);
return await handleApiRequest(async () => {
return await handleApiRequest<CreateVaultDataDto>(async () => {
const vaultCreated: VaultModelDto = await createVaultUseCase.handle(params);
const response: CreateVaultResponseDto = { vaultCreated };
const response: CreateVaultDataDto = { vaultCreated };
return response;
}, StatusCodes.CREATED);
}
7 changes: 4 additions & 3 deletions src/app/ui/workspace/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import ErrorMessage from '@ui/components/common/ErrorMessage';
import CircularLoader from '@ui/components/common/CircularLoader';
import { VaultsGateway } from '@ui/gateways/vaults.gateway';
import { container } from 'tsyringe';
import type { GetMyVaultsResponseDto } from '@shared/dto/responses/get-my-vaults.response.dto';
import { useApi } from '@ui/hooks/useApi';
import {
Box,
Expand All @@ -23,6 +22,8 @@ import {
Typography,
} from '@mui/material';
import AddVaultModal from '@ui/components/modals/AddVaultModal';
import type { GetMyVaultsDataDto } from '@shared/dto/output/data/get-my-vaults.data.dto';
import { UiLogger } from '@ui/logs/ui.logger';

export default function WorkspacePage(): JSX.Element {
const [vaults, setVaults] = useState<VaultModelDto[]>([]);
Expand All @@ -31,7 +32,7 @@ export default function WorkspacePage(): JSX.Element {
const [deleteLoading, setDeleteLoading] = useState(false);
const vaultsGateway: VaultsGateway = container.resolve(VaultsGateway);

const { loading } = useApi<GetMyVaultsResponseDto>({
const { loading } = useApi<GetMyVaultsDataDto>({
request: () => vaultsGateway.getMyVaults(),
onSuccess: data => setVaults(data.myVaults),
onError: error => setError(error),
Expand All @@ -53,7 +54,7 @@ export default function WorkspacePage(): JSX.Element {
await vaultsGateway.deleteVault(id);
} catch (error) {
if (error instanceof Error) setError(error);
else console.error('Unhandled API error:', error);
else UiLogger.error('Unhandled API error: ', error);
} finally {
setDeleteLoading(false);
}
Expand Down
8 changes: 8 additions & 0 deletions src/modules/api/errors/internal-server.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { HttpError } from '@api/errors/abstract/http-error';
import { StatusCodes } from 'http-status-codes';

export class InternalServerError extends HttpError {
public constructor() {
super('Internal Server Error', StatusCodes.INTERNAL_SERVER_ERROR);
}
}
8 changes: 8 additions & 0 deletions src/modules/api/errors/prisma/invalid-request-data.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { HttpError } from '@api/errors/abstract/http-error';
import { StatusCodes } from 'http-status-codes';

export class InvalidRequestDataError extends HttpError {
public constructor() {
super('Invalid request data', StatusCodes.BAD_REQUEST);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { HttpError } from '@api/errors/abstract/http-error';
import { StatusCodes } from 'http-status-codes';

export class RequestedValueTooLongError extends HttpError {
public constructor() {
super('One of the requested values is too long', StatusCodes.BAD_REQUEST);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { HttpError } from '@api/errors/abstract/http-error';
import { StatusCodes } from 'http-status-codes';

export class ResourceAlreadyExistsError extends HttpError {
public constructor() {
super('Resource already exists', StatusCodes.CONFLICT);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { HttpError } from '@api/errors/abstract/http-error';
import { StatusCodes } from 'http-status-codes';

export class NoVaultFoundError extends HttpError {
export class ResourceNotFoundError extends HttpError {
public constructor() {
super('Vault not found', StatusCodes.NOT_FOUND);
super('Resource not found', StatusCodes.NOT_FOUND);
}
}
8 changes: 8 additions & 0 deletions src/modules/api/errors/vault-already-exists.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { HttpError } from '@api/errors/abstract/http-error';
import { StatusCodes } from 'http-status-codes';

export class VaultAlreadyExistsError extends HttpError {
public constructor(label: string) {
super(`Vault with label '${label}' already exists`, StatusCodes.CONFLICT);
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
import { HttpError } from '@api/errors/abstract/http-error';
import { StatusCodes } from 'http-status-codes';
import { NextResponse } from 'next/server';
import type { HttpResponseDto } from '@shared/dto/output/responses/abstract/http.response.dto';
import { ApiLogger } from '@api/logs/api.logger';

export async function handleApiRequest<T>(
callback: () => Promise<T>,
export async function handleApiRequest<Data>(
callback: () => Promise<Data>,
successStatusCode?: StatusCodes
): Promise<NextResponse> {
): Promise<NextResponse<HttpResponseDto<Data>>> {
try {
const data: Awaited<T> = await callback();
const data: Awaited<Data> = await callback();

if (successStatusCode === StatusCodes.NO_CONTENT) {
return new NextResponse(null, { status: StatusCodes.NO_CONTENT });
}

return NextResponse.json(data, {
status: successStatusCode || StatusCodes.OK,
});
} catch (error) {
return NextResponse.json(
{ data },
{
status: successStatusCode || StatusCodes.OK,
}
);
} catch (error: unknown) {
if (error instanceof HttpError) {
return NextResponse.json(
{ error: error.message },
{ status: error.status }
);
}
console.error(error);

ApiLogger.error('Error while handling API errors: ', error);
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: StatusCodes.INTERNAL_SERVER_ERROR }
Expand Down
Loading
Loading