Skip to content

Commit a70fa41

Browse files
committed
feat: Add generated certificate mode
1 parent 9de45b9 commit a70fa41

File tree

8 files changed

+350
-89
lines changed

8 files changed

+350
-89
lines changed

landscape-common/src/cert/order.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ fn default_renew_before_days() -> u32 {
1313
30
1414
}
1515

16+
fn default_generated_validity_days() -> u32 {
17+
365
18+
}
19+
1620
#[derive(Debug, Clone, Serialize, Deserialize)]
1721
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1822
#[serde(rename_all = "snake_case")]
@@ -94,6 +98,7 @@ pub enum KeyType {
9498
#[serde(tag = "t", rename_all = "snake_case")]
9599
pub enum CertType {
96100
Acme(AcmeCertConfig),
101+
Generated(GeneratedCertConfig),
97102
Manual,
98103
}
99104

@@ -120,6 +125,19 @@ pub struct AcmeCertConfig {
120125
pub renew_before_days: u32,
121126
}
122127

128+
#[derive(Debug, Clone, Serialize, Deserialize)]
129+
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
130+
pub struct GeneratedCertConfig {
131+
#[serde(default = "default_generated_validity_days")]
132+
pub validity_days: u32,
133+
}
134+
135+
impl Default for GeneratedCertConfig {
136+
fn default() -> Self {
137+
Self { validity_days: default_generated_validity_days() }
138+
}
139+
}
140+
123141
#[derive(Debug, Clone, Serialize, Deserialize)]
124142
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
125143
pub struct CertConfig {

landscape-webui/src/components/cert/order/CertInfoModal.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ const status_key = computed(() => {
6565
const cert_type_key = computed(() => {
6666
const ct = props.cert?.cert_type;
6767
if (!ct) return "-";
68-
return ct.t === "acme" ? t("cert.type_acme") : t("cert.type_manual");
68+
if (ct.t === "acme") return t("cert.type_acme");
69+
if (ct.t === "generated") return t("cert.type_generated");
70+
return t("cert.type_manual");
6971
});
7072
7173
async function fetch_parsed_info() {

landscape-webui/src/components/cert/order/CertOrderCard.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ function status_label(status?: string) {
3939
function cert_type_label(ct?: CertConfig["cert_type"]) {
4040
if (!ct) return "-";
4141
if (ct.t === "acme") return t("cert.type_acme");
42+
if (ct.t === "generated") return t("cert.type_generated");
4243
if (ct.t === "manual") return t("cert.type_manual");
4344
return "-";
4445
}

landscape-webui/src/components/cert/order/CertOrderEditModal.vue

Lines changed: 105 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,53 @@ const cert_type_kind = computed(() => {
3535
});
3636
3737
const is_acme = computed(() => cert_type_kind.value === "acme");
38+
const is_generated = computed(() => cert_type_kind.value === "generated");
39+
const needs_domains = computed(() => is_acme.value || is_generated.value);
40+
41+
function buildDefaultAcmeCertType(): CertType {
42+
return {
43+
t: "acme",
44+
account_id: accounts.value[0]?.id ?? "",
45+
challenge_type: {
46+
dns: { dns_provider: { cloudflare: { api_token: "" } } },
47+
},
48+
key_type: "ecdsa_p256",
49+
auto_renew: true,
50+
renew_before_days: 30,
51+
} as CertType;
52+
}
53+
54+
function buildDefaultGeneratedCertType(): CertType {
55+
return {
56+
t: "generated",
57+
validity_days: 365,
58+
} as CertType;
59+
}
60+
61+
function reset_cert_material() {
62+
if (!rule.value) return;
63+
rule.value.private_key = undefined;
64+
rule.value.certificate = undefined;
65+
rule.value.certificate_chain = undefined;
66+
rule.value.issued_at = undefined;
67+
rule.value.expires_at = undefined;
68+
rule.value.status_message = undefined;
69+
rule.value.status = "pending";
70+
}
3871
3972
function on_cert_type_change(val: string) {
4073
if (!rule.value) return;
74+
const domains = [...(rule.value.domains ?? [])];
75+
reset_cert_material();
76+
4177
if (val === "acme") {
42-
rule.value.cert_type = {
43-
t: "acme",
44-
account_id: accounts.value[0]?.id ?? "",
45-
challenge_type: {
46-
dns: { dns_provider: { cloudflare: { api_token: "" } } },
47-
} as unknown as CertType extends { t: "acme" } ? CertType : never,
48-
key_type: "ecdsa_p256",
49-
auto_renew: true,
50-
renew_before_days: 30,
51-
} as CertType;
78+
rule.value.cert_type = buildDefaultAcmeCertType();
79+
rule.value.domains = domains;
80+
} else if (val === "generated") {
81+
rule.value.cert_type = buildDefaultGeneratedCertType();
82+
rule.value.domains = domains;
5283
} else {
5384
rule.value.cert_type = { t: "manual" } as CertType;
54-
// Clear ACME-specific data
5585
rule.value.domains = [];
5686
}
5787
}
@@ -99,6 +129,7 @@ const dns_provider_options = [
99129
100130
const cert_type_options = [
101131
{ label: t("cert.type_acme"), value: "acme" },
132+
{ label: t("cert.type_generated"), value: "generated" },
102133
{ label: t("cert.type_manual"), value: "manual" },
103134
];
104135
@@ -198,16 +229,7 @@ async function enter() {
198229
status: "pending",
199230
for_api: false,
200231
for_gateway: false,
201-
cert_type: {
202-
t: "acme",
203-
account_id: accounts.value[0]?.id ?? "",
204-
challenge_type: {
205-
dns: { dns_provider: { cloudflare: { api_token: "" } } },
206-
},
207-
key_type: "ecdsa_p256",
208-
auto_renew: true,
209-
renew_before_days: 30,
210-
} as CertType,
232+
cert_type: buildDefaultAcmeCertType(),
211233
};
212234
}
213235
// Keep UI consistent with currently supported challenge/provider combinations.
@@ -289,11 +311,26 @@ const rules = {
289311
domains: {
290312
trigger: ["change"],
291313
validator(_: unknown, value: string[]) {
292-
if (is_acme.value && (!value || value.length === 0))
314+
if (needs_domains.value && (!value || value.length === 0))
293315
return new Error(t("cert.cert_domains_required"));
294316
return true;
295317
},
296318
},
319+
"cert_type.validity_days": {
320+
trigger: ["blur", "change"],
321+
validator() {
322+
if (
323+
is_generated.value &&
324+
(!rule.value?.cert_type ||
325+
rule.value.cert_type.t !== "generated" ||
326+
!rule.value.cert_type.validity_days ||
327+
rule.value.cert_type.validity_days < 1)
328+
) {
329+
return new Error(t("cert.generated_validity_days_invalid"));
330+
}
331+
return true;
332+
},
333+
},
297334
};
298335
299336
async function save() {
@@ -355,6 +392,33 @@ async function save() {
355392
</n-switch>
356393
</n-form-item>
357394

395+
<n-form-item
396+
v-if="needs_domains"
397+
:label="t('cert.cert_domains')"
398+
path="domains"
399+
>
400+
<n-dynamic-input
401+
v-model:value="rule.domains"
402+
placeholder="example.com"
403+
#="{ index }"
404+
>
405+
<n-form-item
406+
:path="`domains[${index}]`"
407+
:rule="domain_rule"
408+
ignore-path-change
409+
:show-label="false"
410+
:show-feedback="false"
411+
style="margin-bottom: 0; flex: 1"
412+
>
413+
<n-input
414+
v-model:value="rule.domains[index]"
415+
placeholder="example.com"
416+
@keydown.enter.prevent
417+
/>
418+
</n-form-item>
419+
</n-dynamic-input>
420+
</n-form-item>
421+
358422
<!-- ===== ACME mode ===== -->
359423
<template v-if="is_acme && rule.cert_type && rule.cert_type.t === 'acme'">
360424
<n-form-item :label="t('cert.acme_account')">
@@ -366,29 +430,6 @@ async function save() {
366430
/>
367431
</n-form-item>
368432

369-
<n-form-item :label="t('cert.cert_domains')" path="domains">
370-
<n-dynamic-input
371-
v-model:value="rule.domains"
372-
placeholder="example.com"
373-
#="{ index }"
374-
>
375-
<n-form-item
376-
:path="`domains[${index}]`"
377-
:rule="domain_rule"
378-
ignore-path-change
379-
:show-label="false"
380-
:show-feedback="false"
381-
style="margin-bottom: 0; flex: 1"
382-
>
383-
<n-input
384-
v-model:value="rule.domains[index]"
385-
placeholder="example.com"
386-
@keydown.enter.prevent
387-
/>
388-
</n-form-item>
389-
</n-dynamic-input>
390-
</n-form-item>
391-
392433
<n-form-item :label="t('cert.acme_key_type')">
393434
<n-select
394435
:value="rule.cert_type.key_type"
@@ -551,8 +592,26 @@ async function save() {
551592
</n-form-item>
552593
</template>
553594

595+
<!-- ===== Generated mode ===== -->
596+
<template
597+
v-if="
598+
is_generated && rule.cert_type && rule.cert_type.t === 'generated'
599+
"
600+
>
601+
<n-form-item
602+
:label="t('cert.generated_validity_days')"
603+
path="cert_type.validity_days"
604+
>
605+
<n-input-number
606+
v-model:value="rule.cert_type.validity_days"
607+
:min="1"
608+
:max="36500"
609+
/>
610+
</n-form-item>
611+
</template>
612+
554613
<!-- ===== Manual mode ===== -->
555-
<template v-if="!is_acme">
614+
<template v-if="!is_acme && !is_generated">
556615
<n-form-item :label="t('cert.upload_cert')">
557616
<n-input
558617
v-model:value="rule.certificate"

landscape-webui/src/i18n/en/cert.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,15 @@ export default {
3838
// Cert type
3939
cert_type: "Type",
4040
type_acme: "ACME",
41+
type_generated: "Generated",
4142
type_manual: "Manual",
4243

4344
// Manual upload
4445
upload_cert: "Certificate (PEM)",
4546
upload_key: "Private Key (PEM)",
4647
upload_chain: "Certificate Chain (PEM)",
48+
generated_validity_days: "Validity (days)",
49+
generated_validity_days_invalid: "Validity must be greater than 0 days",
4750

4851
// ACME fields
4952
acme_account: "ACME Account",

landscape-webui/src/i18n/zh/cert.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,15 @@ export default {
3838
// Cert type
3939
cert_type: "类型",
4040
type_acme: "ACME",
41+
type_generated: "生成证书",
4142
type_manual: "手动上传",
4243

4344
// Manual upload
4445
upload_cert: "证书 (PEM)",
4546
upload_key: "私钥 (PEM)",
4647
upload_chain: "证书链 (PEM)",
48+
generated_validity_days: "有效期(天)",
49+
generated_validity_days_invalid: "有效期必须大于 0 天",
4750

4851
// ACME fields
4952
acme_account: "ACME 账户",

landscape-webui/src/views/cert/CertOrders.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,16 @@ function status_type(status?: string) {
108108
function cert_type_label(ct?: CertConfig["cert_type"]) {
109109
if (!ct) return "-";
110110
if (ct.t === "acme") return t("cert.type_acme");
111+
if (ct.t === "generated") return t("cert.type_generated");
111112
if (ct.t === "manual") return t("cert.type_manual");
112113
return "-";
113114
}
114115
115116
function cert_type_tag_type(ct?: CertConfig["cert_type"]) {
116117
if (!ct) return "default";
117-
return ct.t === "acme" ? "info" : "success";
118+
if (ct.t === "acme") return "info";
119+
if (ct.t === "generated") return "warning";
120+
return "success";
118121
}
119122
120123
function bool_label(v?: boolean) {

0 commit comments

Comments
 (0)