Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
9fcc73a
refactor: Move request.service.ts into requests folder
vbetsch Aug 3, 2025
bdf6d36
feat: Create useApiCall & useApiFetch
vbetsch Aug 3, 2025
f4617f7
refactor: Use useApiFetch in get my vaults
vbetsch Aug 3, 2025
2914643
refactor: Remove useApi (legacy)
vbetsch Aug 3, 2025
81911c0
refactor: use refetch
vbetsch Aug 3, 2025
3f70588
fix: url in LockliteApiRequestService
vbetsch Aug 3, 2025
66e5a94
fix: looping fetch
vbetsch Aug 3, 2025
d04f362
fix: eslint
vbetsch Aug 3, 2025
612be09
refactor: add refreshVaults in modal
vbetsch Aug 3, 2025
8cd9d2f
refactor: move api hooks in api folder
vbetsch Aug 3, 2025
7d8751c
refactor: create useVaults
vbetsch Aug 3, 2025
51d974e
refactor: rewrite filteredVaults
vbetsch Aug 3, 2025
60bac23
refactor: consider delete return number
vbetsch Aug 3, 2025
83c9019
refactor: use useApiCall in delete vault
vbetsch Aug 3, 2025
e9c605d
refactor: use CreateVaultParams in gateway
vbetsch Aug 3, 2025
00a801b
refactor: improve useApiCall to accept params
vbetsch Aug 3, 2025
775d0ba
fix: transform array to just object
vbetsch Aug 3, 2025
a47bd01
fix: set params optional
vbetsch Aug 3, 2025
0e3c4fb
refactor: use null
vbetsch Aug 3, 2025
5804c6f
refactor: add params in useApiFetch too
vbetsch Aug 3, 2025
df58d82
refactor: remove comment
vbetsch Aug 3, 2025
2c1b20d
refactor: migrate create vault to use useApiCall
vbetsch Aug 3, 2025
937f404
refactor: rename params to input (it could be request too)
vbetsch Aug 3, 2025
7d4c3ad
refactor: rename data to payload (request dto)
vbetsch Aug 3, 2025
8fefa59
refactor: rename request (dto) to payload
vbetsch Aug 3, 2025
3ff44ca
fix: spam error in console
vbetsch Aug 4, 2025
672c65c
refactor: use Form of next/form for modal forms
vbetsch Aug 4, 2025
3f8f488
fix: move error message
vbetsch Aug 4, 2025
e195b05
fix: import never used
vbetsch Aug 4, 2025
c945ada
fix: clear modal in onClose
vbetsch Aug 4, 2025
74386b2
fix: required fields in modal
vbetsch Aug 4, 2025
9e80427
fix: autofocus first input
vbetsch Aug 4, 2025
d9e7c22
fix: bad display with two lignes
vbetsch Aug 4, 2025
17c3926
feat: improve error display
vbetsch Aug 4, 2025
115f24e
fix: modal width
vbetsch Aug 4, 2025
ab6a67b
feat: add an icon button on "add a vault" button
vbetsch Aug 4, 2025
6b49863
feat: implement skeletons loading
vbetsch Aug 4, 2025
b9a9884
fix: skeletons display
vbetsch Aug 4, 2025
c62ffda
feat: add confirmation modal to delete
vbetsch Aug 4, 2025
d5d1718
refactor: create SearchBar component
vbetsch Aug 4, 2025
1a636d3
refactor: create VaultCardContentLine component
vbetsch Aug 4, 2025
cae77ab
refactor: create VaultCard component
vbetsch Aug 4, 2025
a5da3e9
refactor: create VaultsList component
vbetsch Aug 4, 2025
078bbc1
refactor: remove unused imports
vbetsch Aug 4, 2025
d2ac92f
refactor: move delete logic in VaultCard
vbetsch Aug 4, 2025
289a4e7
refactor: create VaultSkeletons component
vbetsch Aug 4, 2025
dd50c63
refactor: remove unused imports
vbetsch Aug 4, 2025
74c3a74
refactor: create DynamicVaultsList component
vbetsch Aug 4, 2025
1baf491
refactor: rename open to openAddModal and its setter
vbetsch Aug 4, 2025
593c624
refactor: use two params in VaultCardContentLineProps
vbetsch Aug 4, 2025
3a2ab05
refactor: create Modal component
vbetsch Aug 4, 2025
6f07972
refactor: create ConfirmationModal component
vbetsch Aug 5, 2025
2aeadf5
refactor: review modal components architecture
vbetsch Aug 5, 2025
7d1dcdf
refactor: create AppModal component
vbetsch Aug 5, 2025
9fea386
Revert "refactor: create AppModal component"
vbetsch Aug 5, 2025
acbff51
refactor: move AddVaultModal to vaults templates
vbetsch Aug 5, 2025
5cd3c15
chore: Add units tests commands
vbetsch Aug 5, 2025
3dade0b
fix: request service tests
vbetsch Aug 5, 2025
0dd5921
fix: error message tests
vbetsch Aug 5, 2025
376fb30
fix: confirmation conditions in ConfirmationModal
vbetsch Aug 5, 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
11 changes: 10 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ format: node_modules
tests: node_modules
npm test

tests-shared: node_modules
npm run tests:units:shared

tests-api: tests-shared
npm run tests:units:api

tests-ui: tests-shared
npm run tests:units:ui

coverage: node_modules
npm run test:cov

Expand All @@ -51,7 +60,7 @@ clean:
rm -rf .next node_modules package-lock.json
npm install

.PHONY: up down dev build lint format tests coverage migrate reset_db clean
.PHONY: up down dev build lint format tests tests-shared tests-api tests-ui coverage migrate reset_db clean

# Aliases
run: up dev
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
"format:c": "prettier --check .",
"format:w": "prettier --write .",
"test": "jest",
"tests:units": "jest tests/units",
"tests:units:ui": "jest tests/units/modules/ui",
"tests:units:shared": "jest tests/units/modules/shared",
"tests:units:api": "jest tests/units/modules/api",
"test:cov": "jest --coverage",
"prisma:reset": "prisma migrate reset",
"prisma:migrate": "prisma migrate dev --name"
Expand Down
9 changes: 5 additions & 4 deletions src/app/api/vaults/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { handleApiRequest } from '@api/helpers/api/handle-api-request';
import type { VaultModelDto } from '@shared/dto/models/vault.model.dto';
import { CreateVaultUseCase } from '@api/usecases/vaults/create-vault.usecase';
import { GetMyVaultsUseCase } from '@api/usecases/vaults/get-my-vaults.usecase';
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';
import type { CreateVaultPayloadDto } from '@shared/dto/input/payloads/create-vault.payload.dto';

/**
* @swagger
Expand Down Expand Up @@ -56,7 +56,7 @@ export async function GET(): Promise<
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateVaultRequestDto'
* $ref: '#/components/schemas/CreateVaultPayloadDto'
* responses:
* 201:
* description: Returns the vault created
Expand Down Expand Up @@ -86,11 +86,12 @@ export async function GET(): Promise<
export async function POST(
request: NextRequest
): Promise<NextResponse<HttpResponseDto<CreateVaultDataDto>>> {
const params: CreateVaultRequestDto = await request.json();
const payload: CreateVaultPayloadDto = await request.json();
const createVaultUseCase: CreateVaultUseCase =
container.resolve(CreateVaultUseCase);
return await handleApiRequest<CreateVaultDataDto>(async () => {
const vaultCreated: VaultModelDto = await createVaultUseCase.handle(params);
const vaultCreated: VaultModelDto =
await createVaultUseCase.handle(payload);
const response: CreateVaultDataDto = { vaultCreated };
return response;
}, StatusCodes.CREATED);
Expand Down
142 changes: 4 additions & 138 deletions src/app/ui/workspace/page.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,12 @@
'use client';

import 'reflect-metadata';
import React, { useMemo, useState } from 'react';
import React from 'react';
import type { JSX } from 'react';
import type { VaultModelDto } from '@shared/dto/models/vault.model.dto';
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 { useApi } from '@ui/hooks/useApi';
import {
Box,
Button,
Card,
CardActions,
CardContent,
CardHeader,
Container,
Grid,
TextField,
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';
import DynamicVaultsList from '@ui/components/vaults/templates/DynamicVaultsList';
import { Container, Typography } from '@mui/material';

export default function WorkspacePage(): JSX.Element {
const [vaults, setVaults] = useState<VaultModelDto[]>([]);
const [error, setError] = useState<Error | null>(null);
const [open, setOpen] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
const vaultsGateway: VaultsGateway = container.resolve(VaultsGateway);

const { loading } = useApi<GetMyVaultsDataDto>({
request: () => vaultsGateway.getMyVaults(),
onSuccess: data => setVaults(data.myVaults),
onError: error => setError(error),
deps: [open, deleteLoading],
});

const [searchTerm, setSearchTerm] = useState('');
const filteredVaults: VaultModelDto[] = useMemo(
() =>
vaults.filter(v =>
v.label.toLowerCase().includes(searchTerm.toLowerCase())
),
[vaults, searchTerm]
);

async function onDelete(id: string): Promise<void> {
setDeleteLoading(true);
try {
await vaultsGateway.deleteVault(id);
} catch (error) {
if (error instanceof Error) setError(error);
else UiLogger.error('Unhandled API error: ', error);
} finally {
setDeleteLoading(false);
}
}

async function deleteVault(id: string): Promise<void> {
await onDelete(id);
}

return (
<Container
sx={{
Expand All @@ -73,87 +16,10 @@ export default function WorkspacePage(): JSX.Element {
gap: '3rem',
}}
>
<AddVaultModal open={open} onClose={() => setOpen(false)} />
<Typography variant={'h3'} textAlign={'left'}>
My vaults
</Typography>
<Box
sx={{
display: 'flex',
gap: '1rem',
width: '100%',
}}
>
<TextField
fullWidth
placeholder="Search…"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
<Button
variant="contained"
sx={{ minWidth: 150 }}
onClick={() => setOpen(true)}
>
Add a vault
</Button>
</Box>
<ErrorMessage error={error} />
<CircularLoader loading={loading || deleteLoading} />
{!loading && !deleteLoading && filteredVaults.length === 0 && (
<Typography>
{searchTerm ? 'No vaults match your search' : 'No results found'}
</Typography>
)}
{filteredVaults.length > 0 && (
<Grid
container
spacing={{ xs: 2, md: 3, lg: 3, xl: 4 }}
columns={{ xs: 1, md: 2, lg: 3, xl: 3 }}
overflow={'auto'}
height={'65vh'}
>
{filteredVaults.map(vault => (
<Grid key={vault.id} size={1}>
<Card
sx={{
bgcolor: 'background.paper',
}}
>
<CardHeader title={vault.label} />
<CardContent>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '1rem',
}}
>
<Typography variant="body2" color="text.secondary">
Secret:
</Typography>
<Typography
variant="body2"
color="text.secondary"
fontFamily={'monospace'}
overflow={'scroll'}
textOverflow={'ellipsis'}
>
{vault.secret}
</Typography>
</Box>
</CardContent>
<CardActions>
<Button color={'error'} onClick={() => deleteVault(vault.id)}>
Delete
</Button>
</CardActions>
</Card>
</Grid>
))}
</Grid>
)}
<DynamicVaultsList />
</Container>
);
}
6 changes: 3 additions & 3 deletions src/modules/api/repositories/vaults.repository.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { injectable } from 'tsyringe';
import { Vault } from '@prisma/generated';
import prisma from '@lib/prisma';
import { CreateVaultRequestDto } from '@shared/dto/input/requests/create-vault.request.dto';
import { handlePrismaRequest } from '@api/helpers/prisma/handle-prisma-request';
import { CreateVaultPayloadDto } from '@shared/dto/input/payloads/create-vault.payload.dto';

@injectable()
export class VaultsRepository {
Expand All @@ -22,9 +22,9 @@ export class VaultsRepository {
);
}

public async create(params: CreateVaultRequestDto): Promise<Vault> {
public async create(payload: CreateVaultPayloadDto): Promise<Vault> {
return await handlePrismaRequest<Vault>(() =>
prisma.vault.create({ data: params })
prisma.vault.create({ data: payload })
);
}

Expand Down
6 changes: 3 additions & 3 deletions src/modules/api/usecases/vaults/create-vault.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { IUseCaseWithInput } from '@api/usecases/abstract/usecase.with-input.int
import { Vault } from '@prisma/generated';
import { VaultAdapter } from '@api/adapters/vault.adapter';
import { VaultsRepository } from '@api/repositories/vaults.repository';
import { CreateVaultRequestDto } from '@shared/dto/input/requests/create-vault.request.dto';
import { VaultAlreadyExistsError } from '@api/errors/business/vaults/vault-already-exists.error';
import { RequestedValueTooLongError } from '@api/errors/http/prisma/requested-value-too-long.error';
import { VaultLabelTooLongError } from '@api/errors/business/vaults/vault-label-too-long.error';
import { CreateVaultPayloadDto } from '@shared/dto/input/payloads/create-vault.payload.dto';

@injectable()
export class CreateVaultUseCase
implements IUseCaseWithInput<CreateVaultRequestDto, VaultModelDto>
implements IUseCaseWithInput<CreateVaultPayloadDto, VaultModelDto>
{
public constructor(
@inject(VaultsRepository)
Expand All @@ -20,7 +20,7 @@ export class CreateVaultUseCase
private readonly _vaultAdapter: VaultAdapter
) {}

public async handle(input: CreateVaultRequestDto): Promise<VaultModelDto> {
public async handle(input: CreateVaultPayloadDto): Promise<VaultModelDto> {
const vaultsFound: number = await this._vaultsRepository.countByLabel(
input.label
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @swagger
* components:
* schemas:
* CreateVaultRequestDto:
* CreateVaultPayloadDto:
* type: object
* required:
* - label
Expand All @@ -15,7 +15,7 @@
* type: string
* description: Password, token, or other sensitive string to store
*/
export type CreateVaultRequestDto = {
export type CreateVaultPayloadDto = {
label: string;
secret: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,9 @@ export abstract class RequestService {
});
}

public async delete<T>(url: string): Promise<number> {
const output: RequestServiceOutputType<T> = await this._request<T>(url, {
public async delete<T>(url: string): Promise<RequestServiceOutputType<T>> {
return await this._request<T>(url, {
method: 'DELETE',
});
return output.status;
}
}
2 changes: 0 additions & 2 deletions src/modules/ui/components/common/ErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import React from 'react';
import type { JSX } from 'react';
import { Typography } from '@mui/material';
import { UiLogger } from '@ui/logs/ui.logger';

type ErrorProps = {
error: Error | null;
};

export default function ErrorMessage(props: ErrorProps): JSX.Element | null {
if (!props.error) return null;
UiLogger.error(null, props.error);
return (
<Typography sx={{ color: 'red' }}>
Error: {props.error.message || 'Unknown error'}
Expand Down
19 changes: 19 additions & 0 deletions src/modules/ui/components/common/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import type { JSX, Dispatch, SetStateAction } from 'react';
import { TextField } from '@mui/material';

type SearchBarProps = {
searchTerm: string;
setSearchTerm: Dispatch<SetStateAction<string>>;
};

export default function SearchBar(props: SearchBarProps): JSX.Element {
return (
<TextField
fullWidth
placeholder="Search…"
value={props.searchTerm}
onChange={e => props.setSearchTerm(e.target.value)}
/>
);
}
Loading