mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-23 16:59:08 +00:00
feat: create certificate page
This commit is contained in:
@@ -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<CertificateDetailProps> = (props) => {
|
||||
const { cetificate, isOpen, onOpenChange } = props;
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
Modal Title
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div>
|
||||
<h4>Basic Information</h4>
|
||||
<p>
|
||||
<strong>Name:</strong> ${cetificate.name}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Domain:</strong> ${cetificate.domain}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Type:</strong> ${cetificate.cert_type}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Auto Renew:</strong>
|
||||
{cetificate.auto_renew ? 'Yes' : 'No'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Created:</strong>
|
||||
{new Date(cetificate.created_at).toLocaleString()}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Expires:</strong>
|
||||
{new Date(cetificate.expires_at).toLocaleString()}
|
||||
</p>
|
||||
|
||||
<h4>Certificate PEM</h4>
|
||||
<div className="cert-details">
|
||||
{cetificate.certificate_pem || 'Not available'}
|
||||
</div>
|
||||
|
||||
<h4>Private Key</h4>
|
||||
<div className="cert-details">
|
||||
{cetificate.has_private_key
|
||||
? '[Hidden for security]'
|
||||
: 'Not available'}
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="light" onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@@ -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<CertificateEditProps> = (props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { certificateId, isOpen, onOpenChange } = props;
|
||||
const { register, handleSubmit, reset } = useForm<EditCertificateDTO>();
|
||||
|
||||
useEffect(() => {
|
||||
getCertificate(certificateId).then((response) => {
|
||||
const { data } = response;
|
||||
reset({
|
||||
...data,
|
||||
});
|
||||
});
|
||||
}, [certificateId]);
|
||||
|
||||
const onSubmit = (values: EditCertificateDTO) => {
|
||||
dispatch(
|
||||
updateCertificate({
|
||||
id: certificateId,
|
||||
certificate: values
|
||||
}),
|
||||
).then(() => {
|
||||
onOpenChange();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
Modal Title
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div>
|
||||
<div className="form-group">
|
||||
<label>Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register('name', { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Domain:</label>
|
||||
<input
|
||||
type="text"
|
||||
{...register('domain', { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<input type="checkbox" {...register('auto_renew')} /> Auto
|
||||
Renew
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
<Button color="danger" variant="light" onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@@ -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<CertificateList> = ({ certificates }) => {
|
||||
return (
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Domain</th>
|
||||
<th>Type</th>
|
||||
<th>Expires</th>
|
||||
<th>Auto Renew</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
{certificates
|
||||
.map((certificate)=><CertificateView certificate={certificate} key={certificate.id}/>)}
|
||||
</table>
|
||||
);
|
||||
};
|
@@ -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<CertificateViewProps> = ({ certificate }) => {
|
||||
const dispatch = useAppDispatch()
|
||||
const detailDisclosure = useDisclosure();
|
||||
const editDisclosure = useDisclosure();
|
||||
|
||||
const handleDeleteCertificate = () => {
|
||||
if (confirm('Delete certificate?')) {
|
||||
dispatch(deleteCertificateAction(certificate.id));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td>{certificate.name}</td>
|
||||
<td>{certificate.domain}</td>
|
||||
<td>{certificate.cert_type}</td>
|
||||
<td>{new Date(certificate.expires_at).toLocaleDateString()}</td>
|
||||
<td>{certificate.auto_renew ? 'Yes' : 'No'}</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={detailDisclosure.onOpenChange}
|
||||
>
|
||||
View
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={editDisclosure.onOpenChange}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={handleDeleteCertificate}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<CertificateDetail cetificate={certificate} {...detailDisclosure} />
|
||||
<CertificateEdit {...editDisclosure} certificateId={certificate.id} />
|
||||
</>
|
||||
);
|
||||
};
|
@@ -0,0 +1 @@
|
||||
export * from './certificate-list'
|
@@ -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<CreateCertificateDTO>();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onSubmit = (values: CreateCertificateDTO) => {
|
||||
dispatch(createCertificateAction(values)).then(() => {
|
||||
reset();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form id="certificateForm" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" {...register('name', { required: true })} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Domain:</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="example.com"
|
||||
{...register('domain', { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Certificate Type:</label>
|
||||
<select id="certType" {...register('cert_type', { required: true })}>
|
||||
<option value="self_signed">Self-Signed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="certAutoRenew"
|
||||
{...register('auto_renew')}
|
||||
/>{' '}
|
||||
Auto Renew
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Generate Certificate
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
@@ -0,0 +1 @@
|
||||
export * from './create-certificate'
|
1
client/src/features/certificates/components/index.ts
Normal file
1
client/src/features/certificates/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './create-certificate'
|
@@ -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`,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
@@ -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<never, AxiosResponse<CertificateDTO[]>>('/certificates');
|
||||
|
||||
export const createCertificate = (params: CreateCertificateDTO) =>
|
||||
api.post<AxiosResponse>('/certificates', params, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
export const getCertificate = (id: string) => api.get<never, AxiosResponse<CertificateDTO>>(`/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}`);
|
||||
|
@@ -1 +1,24 @@
|
||||
export interface CertificateDTO {}
|
||||
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
|
||||
}
|
@@ -1 +1,2 @@
|
||||
export * from './duck'
|
||||
export * from './components'
|
@@ -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<ServerEditProps> = (props) => {
|
||||
export const TemplateEdit: FC<TemplateEditProps> = (props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { templateId, isOpen, onOpenChange } = props;
|
||||
const { register, handleSubmit, reset } = useForm<EditTemplateForm>();
|
||||
@@ -41,12 +41,10 @@ export const TemplateEdit: FC<ServerEditProps> = (props) => {
|
||||
default_port: parseInt(values.default_port),
|
||||
};
|
||||
|
||||
console.log({data})
|
||||
|
||||
dispatch(
|
||||
updateTemplate({
|
||||
id: templateId,
|
||||
server: data,
|
||||
template: data,
|
||||
}),
|
||||
).then(() => {
|
||||
onOpenChange();
|
||||
|
@@ -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());
|
||||
|
@@ -15,8 +15,8 @@ export const createTemplate = (params: CreateTemplateDTO) =>
|
||||
export const getTemplateById = (id: string) =>
|
||||
api.get<string, AxiosResponse<TemplateDTO>>(`/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' },
|
||||
});
|
||||
|
||||
|
@@ -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 (
|
||||
<div id="certificates" className="tab-content active">
|
||||
<div className="section">
|
||||
<h2>Add Certificate</h2>
|
||||
<form id="certificateForm">
|
||||
<div className="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" id="certName" required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Domain:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="certDomain"
|
||||
placeholder="example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Certificate Type:</label>
|
||||
<select id="certType" required>
|
||||
<option value="self_signed">Self-Signed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="certAutoRenew" checked /> Auto Renew
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Generate Certificate
|
||||
</button>
|
||||
</form>
|
||||
<CreateCertificate/>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h2>Certificates List</h2>
|
||||
<div id="certificatesList" className="loading">
|
||||
Loading...
|
||||
{ loading && 'Loading...' }
|
||||
{ certificates.length ? <CertificateList certificates={certificates}/> : <p>No certificates found</p> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user