misc: addressed type issue and ux improvements

This commit is contained in:
Sheen Capadngan
2025-05-17 13:00:04 +08:00
parent edefa7698c
commit 56b7328231
12 changed files with 161 additions and 136 deletions
@@ -67,6 +67,8 @@ export async function up(knex: Knex): Promise<void> {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("type").notNullable(); t.string("type").notNullable();
t.string("name").notNullable(); t.string("name").notNullable();
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.uuid("appConnectionId").nullable(); t.uuid("appConnectionId").nullable();
t.foreign("appConnectionId").references("id").inTable(TableName.AppConnection); t.foreign("appConnectionId").references("id").inTable(TableName.AppConnection);
t.uuid("dnsAppConnectionId").nullable(); t.uuid("dnsAppConnectionId").nullable();
@@ -79,6 +81,13 @@ export async function up(knex: Knex): Promise<void> {
t.binary("credentials"); t.binary("credentials");
t.json("configuration"); t.json("configuration");
t.string("status").notNullable(); t.string("status").notNullable();
t.unique(["projectId", "name"]);
});
}
if (await knex.schema.hasTable(TableName.PkiSubscriber)) {
await knex.schema.alterTable(TableName.PkiSubscriber, (t) => {
t.string("ttl").nullable().alter();
}); });
} }
} }
+2 -2
View File
@@ -25,8 +25,8 @@ export const CertificatesSchema = z.object({
certificateTemplateId: z.string().uuid().nullable().optional(), certificateTemplateId: z.string().uuid().nullable().optional(),
keyUsages: z.string().array().nullable().optional(), keyUsages: z.string().array().nullable().optional(),
extendedKeyUsages: z.string().array().nullable().optional(), extendedKeyUsages: z.string().array().nullable().optional(),
projectId: z.string(), pkiSubscriberId: z.string().uuid().nullable().optional(),
pkiSubscriberId: z.string().uuid().nullable().optional() projectId: z.string()
}); });
export type TCertificates = z.infer<typeof CertificatesSchema>; export type TCertificates = z.infer<typeof CertificatesSchema>;
@@ -13,6 +13,7 @@ export const ExternalCertificateAuthoritiesSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
type: z.string(), type: z.string(),
name: z.string(), name: z.string(),
projectId: z.string(),
appConnectionId: z.string().uuid().nullable().optional(), appConnectionId: z.string().uuid().nullable().optional(),
dnsAppConnectionId: z.string().uuid().nullable().optional(), dnsAppConnectionId: z.string().uuid().nullable().optional(),
certificateAuthorityId: z.string().uuid(), certificateAuthorityId: z.string().uuid(),
+1 -1
View File
@@ -16,7 +16,7 @@ export const PkiSubscribersSchema = z.object({
name: z.string(), name: z.string(),
commonName: z.string(), commonName: z.string(),
subjectAlternativeNames: z.string().array(), subjectAlternativeNames: z.string().array(),
ttl: z.string(), ttl: z.string().nullable().optional(),
keyUsages: z.string().array(), keyUsages: z.string().array(),
extendedKeyUsages: z.string().array(), extendedKeyUsages: z.string().array(),
status: z.string() status: z.string()
@@ -1997,7 +1997,7 @@ interface CreatePkiSubscriber {
caId?: string; caId?: string;
name: string; name: string;
commonName: string; commonName: string;
ttl: string; ttl?: string;
subjectAlternativeNames: string[]; subjectAlternativeNames: string[];
keyUsages: CertKeyUsage[]; keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[]; extendedKeyUsages: CertExtendedKeyUsage[];
@@ -6,15 +6,9 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { import { TCertificateAuthority } from "@app/services/certificate-authority/certificate-authority-types";
TCertificateAuthority,
TCertificateAuthorityInput
} from "@app/services/certificate-authority/certificate-authority-types";
export const registerCertificateAuthorityEndpoints = < export const registerCertificateAuthorityEndpoints = <T extends TCertificateAuthority>({
T extends TCertificateAuthority,
I extends TCertificateAuthorityInput
>({
server, server,
caType, caType,
createSchema, createSchema,
@@ -27,13 +21,13 @@ export const registerCertificateAuthorityEndpoints = <
name: string; name: string;
projectId: string; projectId: string;
status: CaStatus; status: CaStatus;
configuration: I["configuration"]; configuration: T["configuration"];
disableDirectIssuance: boolean; disableDirectIssuance: boolean;
}>; }>;
updateSchema: z.ZodType<{ updateSchema: z.ZodType<{
name?: string; name?: string;
status?: CaStatus; status?: CaStatus;
configuration?: I["configuration"]; configuration?: T["configuration"];
disableDirectIssuance?: boolean; disableDirectIssuance?: boolean;
}>; }>;
responseSchema: z.ZodTypeAny; responseSchema: z.ZodTypeAny;
@@ -90,7 +90,8 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
ttl: z ttl: z
.string() .string()
.trim() .trim()
.refine((val) => ms(val) > 0, "TTL must be a positive number") .refine((val) => !val || ms(val) > 0, "TTL must be a positive number")
.optional()
.describe(PKI_SUBSCRIBERS.CREATE.ttl), .describe(PKI_SUBSCRIBERS.CREATE.ttl),
subjectAlternativeNames: validateAltNameField subjectAlternativeNames: validateAltNameField
.array() .array()
@@ -134,7 +135,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
caId: subscriber.caId ?? undefined, caId: subscriber.caId ?? undefined,
name: subscriber.name, name: subscriber.name,
commonName: subscriber.commonName, commonName: subscriber.commonName,
ttl: subscriber.ttl, ttl: subscriber.ttl ?? undefined,
subjectAlternativeNames: subscriber.subjectAlternativeNames, subjectAlternativeNames: subscriber.subjectAlternativeNames,
keyUsages: subscriber.keyUsages as CertKeyUsage[], keyUsages: subscriber.keyUsages as CertKeyUsage[],
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[] extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
@@ -219,7 +220,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
caId: subscriber.caId ?? undefined, caId: subscriber.caId ?? undefined,
name: subscriber.name, name: subscriber.name,
commonName: subscriber.commonName, commonName: subscriber.commonName,
ttl: subscriber.ttl, ttl: subscriber.ttl ?? undefined,
subjectAlternativeNames: subscriber.subjectAlternativeNames, subjectAlternativeNames: subscriber.subjectAlternativeNames,
keyUsages: subscriber.keyUsages as CertKeyUsage[], keyUsages: subscriber.keyUsages as CertKeyUsage[],
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[] extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
@@ -203,32 +203,44 @@ export const AcmeCertificateAuthorityFns = ({
await appConnectionService.connectAppConnectionById(appConnection.app as AppConnection, dnsAppConnectionId, actor); await appConnectionService.connectAppConnectionById(appConnection.app as AppConnection, dnsAppConnectionId, actor);
const caEntity = await certificateAuthorityDAL.transaction(async (tx) => { const caEntity = await certificateAuthorityDAL.transaction(async (tx) => {
const ca = await certificateAuthorityDAL.create( try {
{ const ca = await certificateAuthorityDAL.create(
projectId, {
disableDirectIssuance projectId,
}, disableDirectIssuance
tx
);
await externalCertificateAuthorityDAL.create(
{
certificateAuthorityId: ca.id,
dnsAppConnectionId,
type: CaType.ACME,
name,
configuration: {
directoryUrl,
accountEmail,
dnsProvider: dnsProviderConfig.provider,
hostedZoneId: dnsProviderConfig.hostedZoneId
}, },
status tx
}, );
tx
);
return certificateAuthorityDAL.findByIdWithAssociatedCa(ca.id, tx); await externalCertificateAuthorityDAL.create(
{
certificateAuthorityId: ca.id,
dnsAppConnectionId,
type: CaType.ACME,
name,
projectId,
configuration: {
directoryUrl,
accountEmail,
dnsProvider: dnsProviderConfig.provider,
hostedZoneId: dnsProviderConfig.hostedZoneId
},
status
},
tx
);
return await certificateAuthorityDAL.findByIdWithAssociatedCa(ca.id, tx);
} catch (error) {
// @ts-expect-error We're expecting a database error
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (error?.error?.code === "23505") {
throw new BadRequestError({
message: "Certificate authority with the same name already exists in your project"
});
}
throw error;
}
}); });
if (!caEntity.externalCa?.id) { if (!caEntity.externalCa?.id) {
@@ -440,7 +440,7 @@ export const pkiSubscriberServiceFactory = ({
const caCertObj = new x509.X509Certificate(decryptedCaCert); const caCertObj = new x509.X509Certificate(decryptedCaCert);
const notBeforeDate = new Date(); const notBeforeDate = new Date();
const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl)); const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl ?? "0"));
const caCertNotBeforeDate = new Date(caCertObj.notBefore); const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter); const caCertNotAfterDate = new Date(caCertObj.notAfter);
@@ -12,7 +12,7 @@ export type TCreatePkiSubscriberDTO = {
name: string; name: string;
commonName: string; commonName: string;
status: PkiSubscriberStatus; status: PkiSubscriberStatus;
ttl: string; ttl?: string;
subjectAlternativeNames: string[]; subjectAlternativeNames: string[];
keyUsages: CertKeyUsage[]; keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[]; extendedKeyUsages: CertExtendedKeyUsage[];
@@ -24,7 +24,7 @@ export type TCreatePkiSubscriberDTO = {
caId: string; caId: string;
name: string; name: string;
commonName: string; commonName: string;
ttl: string; ttl?: string;
subjectAlternativeNames: string[]; subjectAlternativeNames: string[];
keyUsages: CertKeyUsage[]; keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[]; extendedKeyUsages: CertExtendedKeyUsage[];
@@ -45,7 +45,7 @@ const schema = z
caId: z.string().min(1, "Issuing CA is required"), caId: z.string().min(1, "Issuing CA is required"),
commonName: z.string().trim().min(1, "Common Name is required"), commonName: z.string().trim().min(1, "Common Name is required"),
subjectAlternativeNames: z.string(), subjectAlternativeNames: z.string(),
ttl: z.string().trim(), ttl: z.string().trim().optional(),
keyUsages: z.object({ keyUsages: z.object({
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(), [CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
[CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(), [CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(),
@@ -90,6 +90,7 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
handleSubmit, handleSubmit,
reset, reset,
setValue, setValue,
watch,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
@@ -107,6 +108,9 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
} }
}); });
const selectedCaId = watch("caId");
const selectedCa = cas?.find((ca) => ca.id === selectedCaId);
useEffect(() => { useEffect(() => {
if (pkiSubscriber) { if (pkiSubscriber) {
reset({ reset({
@@ -314,97 +318,101 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
</FormControl> </FormControl>
)} )}
/> />
<Controller {selectedCa?.type !== CaType.ACME && (
control={control} <Controller
name="ttl" control={control}
render={({ field, fieldState: { error } }) => ( name="ttl"
<FormControl render={({ field, fieldState: { error } }) => (
label="TTL" <FormControl
isError={Boolean(error)} label="TTL"
errorText={error?.message} isError={Boolean(error)}
isRequired errorText={error?.message}
> isRequired
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." /> >
</FormControl> <Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
)} </FormControl>
/> )}
<Accordion type="single" collapsible className="w-full"> />
<AccordionItem value="key-usages" className="data-[state=open]:border-none"> )}
<AccordionTrigger className="h-fit flex-none pl-1 text-sm"> {selectedCa?.type !== CaType.ACME && (
<div className="order-1 ml-3">Key Usage</div> <Accordion type="single" collapsible className="w-full">
</AccordionTrigger> <AccordionItem value="key-usages" className="data-[state=open]:border-none">
<AccordionContent> <AccordionTrigger className="h-fit flex-none pl-1 text-sm">
<Controller <div className="order-1 ml-3">Key Usage</div>
control={control} </AccordionTrigger>
name="keyUsages" <AccordionContent>
render={({ field: { onChange, value }, fieldState: { error } }) => { <Controller
return ( control={control}
<FormControl name="keyUsages"
label="Key Usage" render={({ field: { onChange, value }, fieldState: { error } }) => {
errorText={error?.message} return (
isError={Boolean(error)} <FormControl
> label="Key Usage"
<div className="mb-7 mt-2 grid grid-cols-2 gap-2"> errorText={error?.message}
{KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { isError={Boolean(error)}
return ( >
<Checkbox <div className="mb-7 mt-2 grid grid-cols-2 gap-2">
id={optionValue} {KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
key={optionValue} return (
className="data-[state=checked]:bg-primary" <Checkbox
isChecked={value[optionValue]} id={optionValue}
onCheckedChange={(state) => { key={optionValue}
onChange({ className="data-[state=checked]:bg-primary"
...value, isChecked={value[optionValue]}
[optionValue]: state onCheckedChange={(state) => {
}); onChange({
}} ...value,
> [optionValue]: state
{label} });
</Checkbox> }}
); >
})} {label}
</div> </Checkbox>
</FormControl> );
); })}
}} </div>
/> </FormControl>
<Controller );
control={control} }}
name="extendedKeyUsages" />
render={({ field: { onChange, value }, fieldState: { error } }) => { <Controller
return ( control={control}
<FormControl name="extendedKeyUsages"
label="Extended Key Usage" render={({ field: { onChange, value }, fieldState: { error } }) => {
errorText={error?.message} return (
isError={Boolean(error)} <FormControl
> label="Extended Key Usage"
<div className="mb-7 mt-2 grid grid-cols-2 gap-2"> errorText={error?.message}
{EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { isError={Boolean(error)}
return ( >
<Checkbox <div className="mb-7 mt-2 grid grid-cols-2 gap-2">
id={optionValue} {EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
key={optionValue} return (
className="data-[state=checked]:bg-primary" <Checkbox
isChecked={value[optionValue]} id={optionValue}
onCheckedChange={(state) => { key={optionValue}
onChange({ className="data-[state=checked]:bg-primary"
...value, isChecked={value[optionValue]}
[optionValue]: state onCheckedChange={(state) => {
}); onChange({
}} ...value,
> [optionValue]: state
{label} });
</Checkbox> }}
); >
})} {label}
</div> </Checkbox>
</FormControl> );
); })}
}} </div>
/> </FormControl>
</AccordionContent> );
</AccordionItem> }}
</Accordion> />
</AccordionContent>
</AccordionItem>
</Accordion>
)}
<div className="mt-4 flex items-center"> <div className="mt-4 flex items-center">
<Button <Button
className="mr-4" className="mr-4"