From 743ca729653b7ee1bf2c5c6c4e253a946e4697c2 Mon Sep 17 00:00:00 2001 From: Home Date: Sat, 18 Oct 2025 18:53:38 +0300 Subject: [PATCH] feat: create certificate page --- .../certificate-list/certificate-details.tsx | 76 +++++++++++++++ .../certificate-list/certificate-edit.tsx | 93 +++++++++++++++++++ .../certificate-list/certificate-list.tsx | 24 +++++ .../certificate-list/certificate-view.tsx | 56 +++++++++++ .../components/certificate-list/index.ts | 1 + .../create-certificate/create-certificate.tsx | 51 ++++++++++ .../components/create-certificate/index.ts | 1 + .../features/certificates/components/index.ts | 1 + .../src/features/certificates/duck/actions.ts | 75 ++++++++++++++- client/src/features/certificates/duck/api.ts | 16 +++- client/src/features/certificates/duck/dto.ts | 25 ++++- client/src/features/certificates/index.ts | 3 +- .../template-list/template-edit.tsx | 8 +- client/src/features/templates/duck/actions.ts | 6 +- client/src/features/templates/duck/api.ts | 4 +- .../src/pages/certificates/certificates.tsx | 46 ++++----- 16 files changed, 440 insertions(+), 46 deletions(-) create mode 100644 client/src/features/certificates/components/certificate-list/certificate-details.tsx create mode 100644 client/src/features/certificates/components/certificate-list/certificate-edit.tsx create mode 100644 client/src/features/certificates/components/certificate-list/certificate-list.tsx create mode 100644 client/src/features/certificates/components/certificate-list/certificate-view.tsx create mode 100644 client/src/features/certificates/components/certificate-list/index.ts create mode 100644 client/src/features/certificates/components/create-certificate/create-certificate.tsx create mode 100644 client/src/features/certificates/components/create-certificate/index.ts create mode 100644 client/src/features/certificates/components/index.ts diff --git a/client/src/features/certificates/components/certificate-list/certificate-details.tsx b/client/src/features/certificates/components/certificate-list/certificate-details.tsx new file mode 100644 index 0000000..8dfd059 --- /dev/null +++ b/client/src/features/certificates/components/certificate-list/certificate-details.tsx @@ -0,0 +1,76 @@ +import type { FC } from 'react'; +import { + Modal, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + Button, +} from '@heroui/react'; +import type { CertificateDTO } from '../../duck'; + +export interface CertificateDetailProps { + cetificate: CertificateDTO; + isOpen: boolean; + onOpenChange: () => void; +} + +export const CertificateDetail: FC = (props) => { + const { cetificate, isOpen, onOpenChange } = props; + return ( + + + {(onClose) => ( + <> + + Modal Title + + +
+

Basic Information

+

+ Name: ${cetificate.name} +

+

+ Domain: ${cetificate.domain} +

+

+ Type: ${cetificate.cert_type} +

+

+ Auto Renew: + {cetificate.auto_renew ? 'Yes' : 'No'} +

+

+ Created: + {new Date(cetificate.created_at).toLocaleString()} +

+

+ Expires: + {new Date(cetificate.expires_at).toLocaleString()} +

+ +

Certificate PEM

+
+ {cetificate.certificate_pem || 'Not available'} +
+ +

Private Key

+
+ {cetificate.has_private_key + ? '[Hidden for security]' + : 'Not available'} +
+
+
+ + + + + )} +
+
+ ); +}; diff --git a/client/src/features/certificates/components/certificate-list/certificate-edit.tsx b/client/src/features/certificates/components/certificate-list/certificate-edit.tsx new file mode 100644 index 0000000..03778a6 --- /dev/null +++ b/client/src/features/certificates/components/certificate-list/certificate-edit.tsx @@ -0,0 +1,93 @@ +import { useEffect, type FC } from 'react'; +import { useForm } from 'react-hook-form'; +import { + Modal, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + Button, +} from '@heroui/react'; +import { useAppDispatch } from '../../../../common/hooks'; +import { getCertificate } from '../../duck/api'; +import { updateCertificate, type EditCertificateDTO } from '../../duck'; + +export interface CertificateEditProps { + certificateId: string; + isOpen: boolean; + onOpenChange: () => void; +} + +export const CertificateEdit: FC = (props) => { + const dispatch = useAppDispatch(); + const { certificateId, isOpen, onOpenChange } = props; + const { register, handleSubmit, reset } = useForm(); + + useEffect(() => { + getCertificate(certificateId).then((response) => { + const { data } = response; + reset({ + ...data, + }); + }); + }, [certificateId]); + + const onSubmit = (values: EditCertificateDTO) => { + dispatch( + updateCertificate({ + id: certificateId, + certificate: values + }), + ).then(() => { + onOpenChange(); + }); + }; + + return ( + +
+ + {(onClose) => ( + <> + + Modal Title + + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + + + + + )} +
+
+
+ ); +}; diff --git a/client/src/features/certificates/components/certificate-list/certificate-list.tsx b/client/src/features/certificates/components/certificate-list/certificate-list.tsx new file mode 100644 index 0000000..2eeebbe --- /dev/null +++ b/client/src/features/certificates/components/certificate-list/certificate-list.tsx @@ -0,0 +1,24 @@ +import type { FC } from 'react'; +import type { CertificateDTO } from '../../duck'; +import { CertificateView } from './certificate-view'; + +export interface CertificateList { + certificates: CertificateDTO[]; +} + +export const CertificateList: FC = ({ certificates }) => { + return ( + + + + + + + + + + {certificates + .map((certificate)=>)} +
NameDomainTypeExpiresAuto RenewActions
+ ); +}; diff --git a/client/src/features/certificates/components/certificate-list/certificate-view.tsx b/client/src/features/certificates/components/certificate-list/certificate-view.tsx new file mode 100644 index 0000000..413368d --- /dev/null +++ b/client/src/features/certificates/components/certificate-list/certificate-view.tsx @@ -0,0 +1,56 @@ +import type { FC } from 'react'; +import { deleteCertificateAction, type CertificateDTO } from '../../duck'; +import { useDisclosure } from '@heroui/react'; +import { CertificateDetail } from './certificate-details'; +import { CertificateEdit } from './certificate-edit'; +import { useAppDispatch } from '../../../../common/hooks'; + +export interface CertificateViewProps { + certificate: CertificateDTO; +} + +export const CertificateView: FC = ({ certificate }) => { + const dispatch = useAppDispatch() + const detailDisclosure = useDisclosure(); + const editDisclosure = useDisclosure(); + + const handleDeleteCertificate = () => { + if (confirm('Delete certificate?')) { + dispatch(deleteCertificateAction(certificate.id)); + } + }; + + return ( + <> + + {certificate.name} + {certificate.domain} + {certificate.cert_type} + {new Date(certificate.expires_at).toLocaleDateString()} + {certificate.auto_renew ? 'Yes' : 'No'} + + + + + + + + + + ); +}; diff --git a/client/src/features/certificates/components/certificate-list/index.ts b/client/src/features/certificates/components/certificate-list/index.ts new file mode 100644 index 0000000..55cf361 --- /dev/null +++ b/client/src/features/certificates/components/certificate-list/index.ts @@ -0,0 +1 @@ +export * from './certificate-list' \ No newline at end of file diff --git a/client/src/features/certificates/components/create-certificate/create-certificate.tsx b/client/src/features/certificates/components/create-certificate/create-certificate.tsx new file mode 100644 index 0000000..69dc5c4 --- /dev/null +++ b/client/src/features/certificates/components/create-certificate/create-certificate.tsx @@ -0,0 +1,51 @@ +import type { FC } from 'react'; +import { useForm } from 'react-hook-form'; +import { createCertificateAction, type CreateCertificateDTO } from '../../duck'; +import { useAppDispatch } from '../../../../common/hooks'; + +export const CreateCertificate: FC = () => { + const { handleSubmit, register, reset } = useForm(); + const dispatch = useAppDispatch(); + + const onSubmit = (values: CreateCertificateDTO) => { + dispatch(createCertificateAction(values)).then(() => { + reset(); + }); + }; + + return ( +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ ); +}; diff --git a/client/src/features/certificates/components/create-certificate/index.ts b/client/src/features/certificates/components/create-certificate/index.ts new file mode 100644 index 0000000..8449911 --- /dev/null +++ b/client/src/features/certificates/components/create-certificate/index.ts @@ -0,0 +1 @@ +export * from './create-certificate' \ No newline at end of file diff --git a/client/src/features/certificates/components/index.ts b/client/src/features/certificates/components/index.ts new file mode 100644 index 0000000..8449911 --- /dev/null +++ b/client/src/features/certificates/components/index.ts @@ -0,0 +1 @@ +export * from './create-certificate' \ No newline at end of file diff --git a/client/src/features/certificates/duck/actions.ts b/client/src/features/certificates/duck/actions.ts index 1e278d3..c9a0e57 100644 --- a/client/src/features/certificates/duck/actions.ts +++ b/client/src/features/certificates/duck/actions.ts @@ -1,10 +1,12 @@ import { certificateSlice } from './slice'; -import { getCertificates } from './api'; +import { createCertificate, deleteCertificate, getCertificates, patchCertificate } from './api'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { getCertificatesState } from './selectors'; import type { RootState } from '../../../store'; +import { appNotificator } from '../../../utils/notification/app-notificator'; +import type { CreateCertificateDTO, EditCertificateDTO } from './dto'; -const PREFFIX = 'certificates' +const PREFFIX = 'certificates'; export const fetchCertificates = createAsyncThunk( `${PREFFIX}/fetchAll`, @@ -18,7 +20,6 @@ export const fetchCertificates = createAsyncThunk( dispatch(certificateSlice.actions.setLoading(true)); const response = await getCertificates().then(({ data }) => data); dispatch(certificateSlice.actions.setUsers(response)); - } catch (e) { const message = e instanceof Error ? e.message : `Unknown error in ${PREFFIX}/fetchAll`; @@ -28,3 +29,71 @@ export const fetchCertificates = createAsyncThunk( } }, ); + +export const createCertificateAction = createAsyncThunk( + `${PREFFIX}/createCertificates`, + async (params: CreateCertificateDTO, { dispatch }) => { + try { + await createCertificate(params); + dispatch(fetchCertificates()); + } catch (e) { + appNotificator.add({ + message: + e instanceof Error + ? e.message + : `Unknown error in ${PREFFIX}/createCertificates`, + type: 'error', + }); + } + }, +); + +export const updateCertificate = createAsyncThunk( + `${PREFFIX}/updateCertificate`, + async ( + params: { + id: string; + certificate: EditCertificateDTO; + }, + { dispatch }, + ) => { + try { + await patchCertificate(params.id, params.certificate); + dispatch(fetchCertificates()); + appNotificator.add({ + message: 'Template updated', + type: 'success', + }); + } catch (e) { + appNotificator.add({ + type: 'error', + message: + e instanceof Error + ? `Error updating: ${e.message}` + : `Unknown error in ${PREFFIX}/updateTemplate`, + }); + } + }, +); + +export const deleteCertificateAction = createAsyncThunk( + `${PREFFIX}/deleteCertificate`, + async (id: string, { dispatch }) => { + try { + await deleteCertificate(id); + appNotificator.add({ + message: 'Certificate deleted', + type: 'success', + }); + dispatch(fetchCertificates()); + } catch (e) { + appNotificator.add({ + type: 'error', + message: + e instanceof Error + ? `Delete error: ${e.message}` + : `Unknown error in ${PREFFIX}/deleteCertificate`, + }); + } + }, +); \ No newline at end of file diff --git a/client/src/features/certificates/duck/api.ts b/client/src/features/certificates/duck/api.ts index cd53463..1402e6f 100644 --- a/client/src/features/certificates/duck/api.ts +++ b/client/src/features/certificates/duck/api.ts @@ -1,6 +1,20 @@ import type { AxiosResponse } from 'axios'; import { api } from '../../../api/api'; -import type { CertificateDTO } from './dto'; +import type { CertificateDTO, CreateCertificateDTO, EditCertificateDTO } from './dto'; export const getCertificates = () => api.get>('/certificates'); + +export const createCertificate = (params: CreateCertificateDTO) => + api.post('/certificates', params, { + headers: { 'Content-Type': 'application/json' }, + }); + +export const getCertificate = (id: string) => api.get>(`/certificates/${id}`) + +export const patchCertificate = (id: string, certificate: EditCertificateDTO) => + api.put(`/certificates/${id}`, certificate, { + headers: { 'Content-Type': 'application/json' }, + }); + +export const deleteCertificate = (id: string) => api.delete(`/certificates/${id}`); diff --git a/client/src/features/certificates/duck/dto.ts b/client/src/features/certificates/duck/dto.ts index 50430d6..f2e7e25 100644 --- a/client/src/features/certificates/duck/dto.ts +++ b/client/src/features/certificates/duck/dto.ts @@ -1 +1,24 @@ -export interface CertificateDTO {} \ No newline at end of file +export interface CertificateDTO { + name: string; + domain: string; + cert_type: string; + expires_at: string; + auto_renew: boolean; + id: string + created_at: string + certificate_pem: string + has_private_key: boolean +} + +export interface CreateCertificateDTO { + name: string; + domain: string; + cert_type: string; + auto_renew: boolean; +} + +export interface EditCertificateDTO { + name: string + domain: string + auto_renew: boolean +} \ No newline at end of file diff --git a/client/src/features/certificates/index.ts b/client/src/features/certificates/index.ts index edc3f9b..f37e401 100644 --- a/client/src/features/certificates/index.ts +++ b/client/src/features/certificates/index.ts @@ -1 +1,2 @@ -export * from './duck' \ No newline at end of file +export * from './duck' +export * from './components' \ No newline at end of file diff --git a/client/src/features/templates/components/template-list/template-edit.tsx b/client/src/features/templates/components/template-list/template-edit.tsx index 7bb58ed..9556c95 100644 --- a/client/src/features/templates/components/template-list/template-edit.tsx +++ b/client/src/features/templates/components/template-list/template-edit.tsx @@ -14,13 +14,13 @@ import { getTemplateById } from '../../duck/api'; import { protocolOptions } from '../add-template/util'; import { updateTemplate } from '../../duck'; -export interface ServerEditProps { +export interface TemplateEditProps { templateId: string; isOpen: boolean; onOpenChange: () => void; } -export const TemplateEdit: FC = (props) => { +export const TemplateEdit: FC = (props) => { const dispatch = useAppDispatch(); const { templateId, isOpen, onOpenChange } = props; const { register, handleSubmit, reset } = useForm(); @@ -41,12 +41,10 @@ export const TemplateEdit: FC = (props) => { default_port: parseInt(values.default_port), }; - console.log({data}) - dispatch( updateTemplate({ id: templateId, - server: data, + template: data, }), ).then(() => { onOpenChange(); diff --git a/client/src/features/templates/duck/actions.ts b/client/src/features/templates/duck/actions.ts index bb28966..cf13fa5 100644 --- a/client/src/features/templates/duck/actions.ts +++ b/client/src/features/templates/duck/actions.ts @@ -56,12 +56,12 @@ export const updateTemplate = createAsyncThunk( async ( params: { id: string; - server: EditTemplateDTO; + template: EditTemplateDTO; }, { dispatch }, ) => { try { - await patchTemplate(params.id, params.server); + await patchTemplate(params.id, params.template); dispatch(fetchTemplates()); appNotificator.add({ message: 'Template updated', @@ -85,7 +85,7 @@ export const deleteTemplateAction = createAsyncThunk( try { await deleteTemplate(id); appNotificator.add({ - message: 'Server deleted', + message: 'Template deleted', type: 'success', }); dispatch(fetchTemplates()); diff --git a/client/src/features/templates/duck/api.ts b/client/src/features/templates/duck/api.ts index ac2edb2..3bab4ca 100644 --- a/client/src/features/templates/duck/api.ts +++ b/client/src/features/templates/duck/api.ts @@ -15,8 +15,8 @@ export const createTemplate = (params: CreateTemplateDTO) => export const getTemplateById = (id: string) => api.get>(`/templates/${id}`); -export const patchTemplate = (id: string, server: EditTemplateDTO) => - api.put(`/templates/${id}`, server, { +export const patchTemplate = (id: string, template: EditTemplateDTO) => + api.put(`/templates/${id}`, template, { headers: { 'Content-Type': 'application/json' }, }); diff --git a/client/src/pages/certificates/certificates.tsx b/client/src/pages/certificates/certificates.tsx index c0f9a21..5ee9d54 100644 --- a/client/src/pages/certificates/certificates.tsx +++ b/client/src/pages/certificates/certificates.tsx @@ -1,45 +1,31 @@ import type { RouteObject } from 'react-router'; +import { useAppDispatch, useAppSelector } from '../../common/hooks'; +import { useEffect } from 'react'; +import { fetchCertificates, getCertificatesState } from '../../features'; +import { CreateCertificate } from '../../features/certificates'; +import { CertificateList } from '../../features/certificates/components/certificate-list'; export const Certificates = () => { + const dispatch = useAppDispatch() + + const { loading, certificates } = useAppSelector(getCertificatesState) + + useEffect(()=>{ + dispatch(fetchCertificates()) + }, [dispatch]) + return (

Add Certificate

-
-
- - -
-
- - -
-
- - -
-
- -
- -
+

Certificates List

- Loading... + { loading && 'Loading...' } + { certificates.length ? :

No certificates found

}