Address greptile comments

This commit is contained in:
Carlos Monastyrski
2025-11-10 22:15:16 -03:00
parent 76645ccd23
commit ff07bb643a
5 changed files with 82 additions and 47 deletions

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable @typescript-eslint/no-floating-promises */
import RE2 from "re2";
import { z } from "zod"; import { z } from "zod";
import { CertificatesSchema } from "@app/db/schemas"; import { CertificatesSchema } from "@app/db/schemas";
@@ -632,8 +633,11 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber) serialNumber: z.string().trim().describe(CERTIFICATES.GET.serialNumber)
}), }),
body: z.object({ body: z.object({
password: z.string().describe("Password for the keystore"), password: z
alias: z.string().describe("Alias for the certificate in the keystore") .string()
.min(6, "Password must be at least 6 characters long")
.describe("Password for the keystore (minimum 6 characters)"),
alias: z.string().min(1, "Alias is required").describe("Alias for the certificate in the keystore")
}), }),
response: { response: {
200: z.any().describe("PKCS12 keystore as binary data") 200: z.any().describe("PKCS12 keystore as binary data")
@@ -665,7 +669,10 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
addNoCacheHeaders(reply); addNoCacheHeaders(reply);
reply.header("Content-Type", "application/octet-stream"); reply.header("Content-Type", "application/octet-stream");
reply.header("Content-Disposition", `attachment; filename="certificate-${req.params.serialNumber}.p12"`); reply.header(
"Content-Disposition",
`attachment; filename="certificate-${req.params.serialNumber.replace(new RE2("[^\\w.-]", "g"), "_")}.p12"`
);
return pkcs12Data; return pkcs12Data;
} }

View File

@@ -675,6 +675,12 @@ export const certificateServiceFactory = ({
throw new BadRequestError({ message: "Password is required for PKCS12 keystore generation" }); throw new BadRequestError({ message: "Password is required for PKCS12 keystore generation" });
} }
if (password.length < 6) {
throw new BadRequestError({
message: "Password must be at least 6 characters long for PKCS12 keystore security"
});
}
if (!alias || alias.trim() === "") { if (!alias || alias.trim() === "") {
throw new BadRequestError({ message: "Alias is required for PKCS12 keystore generation" }); throw new BadRequestError({ message: "Alias is required for PKCS12 keystore generation" });
} }

View File

@@ -139,6 +139,7 @@ export const useUpdateRenewalConfig = () => {
export const useDownloadCertPkcs12 = () => { export const useDownloadCertPkcs12 = () => {
return useMutation<void, object, TDownloadPkcs12DTO>({ return useMutation<void, object, TDownloadPkcs12DTO>({
mutationFn: async ({ serialNumber, projectSlug, password, alias }) => { mutationFn: async ({ serialNumber, projectSlug, password, alias }) => {
try {
const response = await apiRequest.post( const response = await apiRequest.post(
`/api/v1/pki/certificates/${serialNumber}/pkcs12`, `/api/v1/pki/certificates/${serialNumber}/pkcs12`,
{ {
@@ -161,6 +162,15 @@ export const useDownloadCertPkcs12 = () => {
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
} catch (error: any) {
if (error.response?.data instanceof ArrayBuffer) {
const decoder = new TextDecoder();
const errorText = decoder.decode(error.response.data);
const errorData = JSON.parse(errorText);
throw new Error(errorData.message);
}
throw error;
}
} }
}); });
}; };

View File

@@ -19,13 +19,17 @@ type Props = {
popUpName: keyof UsePopUpState<["certificateExport"]>, popUpName: keyof UsePopUpState<["certificateExport"]>,
state?: boolean state?: boolean
) => void; ) => void;
onFormatSelected: (format: "pem" | "jks", serialNumber: string, options?: ExportOptions) => void; onFormatSelected: (
format: "pem" | "pkcs12",
serialNumber: string,
options?: ExportOptions
) => void;
}; };
export type CertificateExportFormat = "pem" | "jks"; export type CertificateExportFormat = "pem" | "pkcs12";
export type ExportOptions = { export type ExportOptions = {
jks?: { pkcs12?: {
password: string; password: string;
alias: string; alias: string;
}; };
@@ -33,7 +37,7 @@ export type ExportOptions = {
export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelected }: Props) => { export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelected }: Props) => {
const [selectedFormat, setSelectedFormat] = useState<CertificateExportFormat>("pem"); const [selectedFormat, setSelectedFormat] = useState<CertificateExportFormat>("pem");
const [jksOptions, setJksOptions] = useState({ const [pkcs12Options, setPkcs12Options] = useState({
password: "", password: "",
alias: "" alias: ""
}); });
@@ -45,7 +49,7 @@ export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelec
useEffect(() => { useEffect(() => {
if (popUp?.certificateExport?.isOpen) { if (popUp?.certificateExport?.isOpen) {
setSelectedFormat("pem"); setSelectedFormat("pem");
setJksOptions({ setPkcs12Options({
password: "", password: "",
alias: "" alias: ""
}); });
@@ -53,8 +57,8 @@ export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelec
}, [popUp?.certificateExport?.isOpen]); }, [popUp?.certificateExport?.isOpen]);
const isFormValid = () => { const isFormValid = () => {
if (selectedFormat === "jks") { if (selectedFormat === "pkcs12") {
return jksOptions.password.trim() !== "" && jksOptions.alias.trim() !== ""; return pkcs12Options.password.length >= 6 && pkcs12Options.alias.trim() !== "";
} }
return true; return true;
}; };
@@ -63,8 +67,8 @@ export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelec
if (serialNumber && isFormValid()) { if (serialNumber && isFormValid()) {
const options: ExportOptions = {}; const options: ExportOptions = {};
if (selectedFormat === "jks") { if (selectedFormat === "pkcs12") {
options.jks = jksOptions; options.pkcs12 = pkcs12Options;
} }
onFormatSelected(selectedFormat, serialNumber, options); onFormatSelected(selectedFormat, serialNumber, options);
@@ -97,20 +101,28 @@ export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelec
onValueChange={(value) => setSelectedFormat(value as CertificateExportFormat)} onValueChange={(value) => setSelectedFormat(value as CertificateExportFormat)}
> >
<SelectItem value="pem">PEM Format</SelectItem> <SelectItem value="pem">PEM Format</SelectItem>
<SelectItem value="jks">PKCS12 Format</SelectItem> <SelectItem value="pkcs12">PKCS12 Format</SelectItem>
</Select> </Select>
</FormControl> </FormControl>
{selectedFormat === "jks" && ( {selectedFormat === "pkcs12" && (
<> <>
<FormControl <FormControl
label="Keystore Password" label="Keystore Password"
helperText="Password to protect the PKCS12 keystore" helperText={
pkcs12Options.password.length > 0 && pkcs12Options.password.length < 6
? undefined
: "Password to protect the PKCS12 keystore (minimum 6 characters)"
}
isError={pkcs12Options.password.length > 0 && pkcs12Options.password.length < 6}
errorText="Password must be at least 6 characters long"
> >
<Input <Input
placeholder="Enter keystore password" placeholder="Enter keystore password"
value={jksOptions.password} value={pkcs12Options.password}
onChange={(e) => setJksOptions((prev) => ({ ...prev, password: e.target.value }))} onChange={(e) =>
setPkcs12Options((prev) => ({ ...prev, password: e.target.value }))
}
type="password" type="password"
/> />
</FormControl> </FormControl>
@@ -121,8 +133,8 @@ export const CertificateExportModal = ({ popUp, handlePopUpToggle, onFormatSelec
> >
<Input <Input
placeholder="Enter certificate alias" placeholder="Enter certificate alias"
value={jksOptions.alias} value={pkcs12Options.alias}
onChange={(e) => setJksOptions((prev) => ({ ...prev, alias: e.target.value }))} onChange={(e) => setPkcs12Options((prev) => ({ ...prev, alias: e.target.value }))}
/> />
</FormControl> </FormControl>
</> </>

View File

@@ -53,16 +53,16 @@ export const CertificatesSection = () => {
}; };
const handleCertificateExport = async ( const handleCertificateExport = async (
format: "pem" | "jks", format: "pem" | "pkcs12",
serialNumber: string, serialNumber: string,
options?: ExportOptions options?: ExportOptions
) => { ) => {
if (format === "pem") { if (format === "pem") {
handlePopUpOpen("certificateCert", { serialNumber }); handlePopUpOpen("certificateCert", { serialNumber });
} else if (format === "jks") { } else if (format === "pkcs12") {
if (!currentProject?.slug) return; if (!currentProject?.slug) return;
if (!options?.jks?.password || !options?.jks?.alias) { if (!options?.pkcs12?.password || !options?.pkcs12?.alias) {
createNotification({ createNotification({
text: "Password and alias are required for PKCS12 export", text: "Password and alias are required for PKCS12 export",
type: "error" type: "error"
@@ -74,17 +74,17 @@ export const CertificatesSection = () => {
await downloadCertPkcs12({ await downloadCertPkcs12({
serialNumber, serialNumber,
projectSlug: currentProject.slug, projectSlug: currentProject.slug,
password: options.jks.password, password: options.pkcs12.password,
alias: options.jks.alias alias: options.pkcs12.alias
}); });
createNotification({ createNotification({
text: "PKCS12 certificate downloaded successfully", text: "PKCS12 certificate downloaded successfully",
type: "success" type: "success"
}); });
} catch { } catch (error: any) {
createNotification({ createNotification({
text: "Failed to download PKCS12 certificate", text: error?.message || "Failed to download PKCS12 certificate",
type: "error" type: "error"
}); });
} }