Compare commits
3 commits
b6db17e7d8
...
46f31de837
Author | SHA1 | Date | |
---|---|---|---|
46f31de837 | |||
b997a5c273 | |||
dca9845fe9 |
12 changed files with 545 additions and 84 deletions
|
@ -15,10 +15,11 @@
|
|||
"dependencies": {
|
||||
"@mdi/font": "7.4.47",
|
||||
"core-js": "^3.41.0",
|
||||
"iso-3166-1": "^2.1.1",
|
||||
"openapi-fetch": "^0.14.0",
|
||||
"roboto-fontface": "*",
|
||||
"vue": "^3.5.13",
|
||||
"vuetify": "^3.8.1",
|
||||
"openapi-fetch": "^0.14.0"
|
||||
"vuetify": "^3.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.24.0",
|
||||
|
|
8
frontend/pnpm-lock.yaml
generated
8
frontend/pnpm-lock.yaml
generated
|
@ -14,6 +14,9 @@ importers:
|
|||
core-js:
|
||||
specifier: ^3.41.0
|
||||
version: 3.41.0
|
||||
iso-3166-1:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
openapi-fetch:
|
||||
specifier: ^0.14.0
|
||||
version: 0.14.0
|
||||
|
@ -1289,6 +1292,9 @@ packages:
|
|||
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
iso-3166-1@2.1.1:
|
||||
resolution: {integrity: sha512-RZxXf8cw5Y8LyHZIwIRvKw8sWTIHh2/txBT+ehO0QroesVfnz3JNFFX4i/OC/Yuv2bDIVYrHna5PMvjtpefq5w==}
|
||||
|
||||
js-levenshtein@1.1.6:
|
||||
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -3361,6 +3367,8 @@ snapshots:
|
|||
|
||||
isexe@3.1.1: {}
|
||||
|
||||
iso-3166-1@2.1.1: {}
|
||||
|
||||
js-levenshtein@1.1.6: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<template>
|
||||
<v-app>
|
||||
<v-app-bar>
|
||||
<v-app-bar-nav-icon icon="mdi-home" to="/"/>
|
||||
<v-app-bar-nav-icon
|
||||
icon="mdi-home"
|
||||
to="/"
|
||||
/>
|
||||
</v-app-bar>
|
||||
<v-main>
|
||||
<router-view />
|
||||
|
|
306
frontend/src/components/CertificateEditor.vue
Normal file
306
frontend/src/components/CertificateEditor.vue
Normal file
|
@ -0,0 +1,306 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, useId, useTemplateRef } from "vue";
|
||||
import iso3166 from "iso-3166-1";
|
||||
import CountryListItem from "./CountryListItem.vue";
|
||||
|
||||
// Define interface for a country object
|
||||
interface Country {
|
||||
country: string;
|
||||
alpha2: string;
|
||||
alpha3: string;
|
||||
numeric: string;
|
||||
}
|
||||
|
||||
const formModel = ref(false);
|
||||
const formRef = useTemplateRef("form");
|
||||
|
||||
// Domain fields
|
||||
const domainInputId = useId();
|
||||
const domainValue = ref("");
|
||||
const domains = ref<string[]>([]);
|
||||
|
||||
// Subject fields
|
||||
const commonName = ref("");
|
||||
const emailAddress = ref("");
|
||||
const organization = ref("");
|
||||
const organizationalUnit = ref("");
|
||||
const country = ref("");
|
||||
const countryName = ref("");
|
||||
const state = ref("");
|
||||
const locality = ref("");
|
||||
|
||||
// Get user's locale from browser
|
||||
const userLocale = ref<string>(navigator.language || navigator.languages?.[0] || 'en-US');
|
||||
const userCountryCode = ref<string>(userLocale.value.split('-')[1]?.toUpperCase() || '');
|
||||
const userCountry = ref<Country | undefined>(undefined);
|
||||
|
||||
// Countries data for autocomplete
|
||||
const allCountries = iso3166.all() as Country[];
|
||||
const countries = ref<(Country | { divider: boolean; header: string })[]>([]);
|
||||
|
||||
// Prepare countries list with user's country at the top
|
||||
function prepareCountriesList() {
|
||||
// Find user's country based on browser locale
|
||||
userCountry.value = allCountries.find(c => c.alpha2 === userCountryCode.value);
|
||||
|
||||
// Create the countries list
|
||||
countries.value = [];
|
||||
|
||||
// Add user's country at the top if found
|
||||
if (userCountry.value) {
|
||||
countries.value.push(userCountry.value);
|
||||
countries.value.push({ divider: true, header: 'All Countries' });
|
||||
|
||||
// Add all countries except the user's country to avoid duplicates
|
||||
countries.value.push(...allCountries.filter(c => c.alpha2 !== userCountryCode.value));
|
||||
} else {
|
||||
// Add all countries if user's country not found
|
||||
countries.value.push(...allCountries);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize countries list
|
||||
prepareCountriesList();
|
||||
|
||||
// Certificate settings
|
||||
const keyLength = ref(4096);
|
||||
const validityDays = ref(365);
|
||||
const noExpiry = ref(false);
|
||||
|
||||
// Validation rules
|
||||
const domainRules = [
|
||||
(value: string) => !!value || "A value must be provided to add the domain.",
|
||||
(value: string) => new RegExp("^[a-z0-9][a-z0-9.]+$").test(value) || "Invalid domain characters provided. (To use some characters, convert the domain to xn-domain format.)",
|
||||
]
|
||||
|
||||
const emailRules = [
|
||||
(value: string) => !value || /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value) || "Invalid email format.",
|
||||
];
|
||||
|
||||
const keyLengthRules = [
|
||||
(value: number) => value >= 3000 || "Key length must be at least 3000 bits.",
|
||||
(value: number) => value <= 6000 || "Key length must not exceed 6000 bits.",
|
||||
];
|
||||
|
||||
const validityDaysRules = [
|
||||
(value: number) => !value || value > 0 || "Validity period (in days) must be a positive integer.",
|
||||
];
|
||||
|
||||
|
||||
function addDomain() {
|
||||
if (formRef.value?.errors.some(({id}) => domainInputId === id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
domains.value.push(domainValue.value);
|
||||
domainValue.value = "";
|
||||
|
||||
// Set common name to primary domain if not already set
|
||||
if (!commonName.value) {
|
||||
commonName.value = domainValue.value;
|
||||
}
|
||||
}
|
||||
|
||||
const isSubmitting = ref(false);
|
||||
async function submitNewCertificate() {
|
||||
if (!formRef.value?.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
// Extract country code if country is an object
|
||||
const countryCode = typeof country.value === 'object' && country.value !== null
|
||||
? (country.value as Country).alpha2
|
||||
: country.value;
|
||||
|
||||
// TODO: Implement certificate request submission
|
||||
console.log({
|
||||
type: "STANDALONE_CERTIFICATE",
|
||||
requestedKeyLength: keyLength.value,
|
||||
requestedValidityDays: noExpiry.value ? null : validityDays.value,
|
||||
subject: {
|
||||
commonName: commonName.value,
|
||||
emailAddress: emailAddress.value,
|
||||
organization: organization.value,
|
||||
organizationalUnit: organizationalUnit.value,
|
||||
country: countryCode,
|
||||
state: state.value,
|
||||
locality: locality.value,
|
||||
},
|
||||
extension: {
|
||||
alternativeDnsNames: domains.value,
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="formModel"
|
||||
:disabled="isSubmitting"
|
||||
@submit.prevent="submitNewCertificate"
|
||||
>
|
||||
<h2>New certificate</h2>
|
||||
|
||||
<fieldset class="mb-4">
|
||||
<legend>Subject details</legend>
|
||||
<v-text-field
|
||||
v-model="emailAddress"
|
||||
label="Email Address"
|
||||
:rules="emailRules"
|
||||
hint="Optional"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="organization"
|
||||
label="Organization"
|
||||
hint="Optional"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="organizationalUnit"
|
||||
label="Organizational Unit"
|
||||
hint="Optional"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<v-autocomplete
|
||||
v-model="country"
|
||||
v-model:search="countryName"
|
||||
:items="countries"
|
||||
item-title="country"
|
||||
item-value="alpha2"
|
||||
label="Country"
|
||||
hint="Search and select your country"
|
||||
class="mb-2"
|
||||
>
|
||||
<template #item="slotProps">
|
||||
<CountryListItem :item="slotProps.item" :props="slotProps.props" />
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
|
||||
<v-text-field
|
||||
v-model="state"
|
||||
label="State/Province"
|
||||
hint="Optional"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="locality"
|
||||
label="Locality/City"
|
||||
hint="Optional"
|
||||
class="mb-2"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-4">
|
||||
<legend>Domain Names</legend>
|
||||
<v-text-field
|
||||
:id="domainInputId"
|
||||
v-model="domainValue"
|
||||
label="Domain(s)"
|
||||
:rules="domainRules"
|
||||
@keydown.enter.prevent="addDomain"
|
||||
hint="Press Enter to add domain"
|
||||
append-icon="mdi-plus"
|
||||
@click:append="addDomain"
|
||||
/>
|
||||
<ul v-if="domains.length" class="domain-list">
|
||||
<li
|
||||
v-for="domain in domains"
|
||||
:key="domain"
|
||||
>
|
||||
<span>{{ domain }}</span>
|
||||
<v-tooltip
|
||||
v-if="commonName === domain"
|
||||
text="This is the primary domain for this certificate. It will be used for the certificate common name."
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-icon
|
||||
v-bind="props"
|
||||
icon="mdi-star"
|
||||
color="amber"
|
||||
class="ml-2"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-4">
|
||||
<legend>Certificate Settings</legend>
|
||||
<v-text-field
|
||||
v-model.number="keyLength"
|
||||
type="number"
|
||||
:rules="keyLengthRules"
|
||||
label="Key Length (bits)"
|
||||
hint="Value between 3000 and 6000 bits"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model.number="validityDays"
|
||||
type="number"
|
||||
:rules="validityDaysRules"
|
||||
label="Validity Period (days)"
|
||||
hint="Any positive integer value"
|
||||
:disabled="noExpiry"
|
||||
/>
|
||||
<div class="d-flex align-center">
|
||||
<v-checkbox
|
||||
v-model="noExpiry"
|
||||
label="Certificate without expiry"
|
||||
class="mr-4"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<v-btn
|
||||
class="w-100 w-md-auto ml-md-auto"
|
||||
type="submit"
|
||||
color="primary"
|
||||
:loading="isSubmitting"
|
||||
>
|
||||
Send certificate request
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
fieldset {
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.domain-list {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.domain-list li {
|
||||
padding: 8px;
|
||||
margin-bottom: 4px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
45
frontend/src/components/CountryListItem.vue
Normal file
45
frontend/src/components/CountryListItem.vue
Normal file
|
@ -0,0 +1,45 @@
|
|||
<script setup lang="ts">
|
||||
// Define props with proper TypeScript typing
|
||||
import { computed } from "vue";
|
||||
|
||||
interface Country {
|
||||
country: string;
|
||||
alpha2: string;
|
||||
alpha3: string;
|
||||
numeric: string;
|
||||
}
|
||||
|
||||
interface Divider {
|
||||
divider: boolean;
|
||||
header: string;
|
||||
}
|
||||
|
||||
// Define a type that can be either a Country or a Divider
|
||||
type CountryListItemType = Country | Divider;
|
||||
|
||||
// Define props with proper TypeScript typing
|
||||
const props = defineProps<{
|
||||
item: { raw: CountryListItemType };
|
||||
props?: Record<string, unknown>;
|
||||
}>();
|
||||
|
||||
// Helper functions to check the type of the item
|
||||
const isDivider = computed(() => {
|
||||
return 'divider' in props.item.raw && props.item.raw.divider;
|
||||
});
|
||||
|
||||
const headerText = computed(() => {
|
||||
return 'header' in props.item.raw ? props.item.raw.header : "";
|
||||
});
|
||||
|
||||
const countryItem = computed(() => {
|
||||
return !isDivider.value && !headerText.value && 'country' in props.item.raw ? props.item.raw: undefined;
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-divider v-if="isDivider" class="my-2" />
|
||||
<v-list-subheader v-if="headerText">{{ headerText }}</v-list-subheader>
|
||||
<v-list-item v-if="countryItem" v-bind="props" :title="countryItem.country" :value="countryItem.alpha2" />
|
||||
</template>
|
|
@ -5,8 +5,12 @@
|
|||
max-width="900"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
|
||||
<h1 class="text-h2 font-weight-bold">home-cert-manager</h1>
|
||||
<div class="text-body-2 font-weight-light mb-n1">
|
||||
Welcome to
|
||||
</div>
|
||||
<h1 class="text-h2 font-weight-bold">
|
||||
home-cert-manager
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="py-4" />
|
||||
|
@ -26,7 +30,9 @@
|
|||
</template>
|
||||
|
||||
<template #title>
|
||||
<h2 class="text-h5 font-weight-bold">Get started</h2>
|
||||
<h2 class="text-h5 font-weight-bold">
|
||||
Get started
|
||||
</h2>
|
||||
</template>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
<v-form v-model="valid">
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="subject.country"
|
||||
:rules="subject.countryRules"
|
||||
|
@ -13,7 +16,10 @@
|
|||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="subject.state"
|
||||
:counter="10"
|
||||
|
@ -23,7 +29,10 @@
|
|||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="subject.city"
|
||||
:rules="subject.cityRules"
|
||||
|
@ -33,7 +42,10 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="subject.organization"
|
||||
:rules="subject.organizationRules"
|
||||
|
@ -41,7 +53,10 @@
|
|||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="subject.orgUnit"
|
||||
:rules="subject.orgUnitRules"
|
||||
|
@ -49,7 +64,10 @@
|
|||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="subject.commonName"
|
||||
:counter="10"
|
||||
|
@ -60,26 +78,48 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field type="number" v-model="validity.duration" :rules="validity.durationRules" label="Duration for validity in Days"/>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="validity.duration"
|
||||
type="number"
|
||||
:rules="validity.durationRules"
|
||||
label="Duration for validity in Days"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field label="Alternative Names" v-model="ui.altName">
|
||||
<template v-slot:append>
|
||||
<v-btn icon="mdi-plus" color="green" :disabled="!ui.altName" @click="addAltName"/>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="ui.altName"
|
||||
label="Alternative Names"
|
||||
>
|
||||
<template #append>
|
||||
<v-btn
|
||||
icon="mdi-plus"
|
||||
color="green"
|
||||
:disabled="!ui.altName"
|
||||
@click="addAltName"
|
||||
/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="8">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<v-combobox
|
||||
v-model="subject.altNames"
|
||||
:items="subject.altNames"
|
||||
label="Alternative Names"
|
||||
multiple
|
||||
>
|
||||
<template v-slot:selection="data">
|
||||
<template #selection="data">
|
||||
<v-chip
|
||||
:key="JSON.stringify(data.item)"
|
||||
v-bind="data"
|
||||
|
@ -92,8 +132,14 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="12">
|
||||
<v-textarea v-model="trustingAuthority" label="Trusting Authority"/>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="12"
|
||||
>
|
||||
<v-textarea
|
||||
v-model="trustingAuthority"
|
||||
label="Trusting Authority"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
@ -101,7 +147,12 @@
|
|||
</v-card-item>
|
||||
<v-card-actions>
|
||||
<v-col class="text-right">
|
||||
<v-btn :disabled="!valid" @click="requestCertificate" text="Request certificate" prepend-icon="mdi-certificate"/>
|
||||
<v-btn
|
||||
:disabled="!valid"
|
||||
text="Request certificate"
|
||||
prepend-icon="mdi-certificate"
|
||||
@click="requestCertificate"
|
||||
/>
|
||||
</v-col>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
@ -109,7 +160,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import type { Validated } from "@/types/util";
|
||||
import type { Certificate, Subject, Validity } from "@/types/certificate";
|
||||
import type { Certificate, Subject } from "@/types/certificate";
|
||||
|
||||
const requiredValidation = (fieldName: string) => {
|
||||
return (val: string) => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { fetchClient, type Schemas } from "@/plugins/client.ts";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const certificates = ref<Schemas["Certificate"][]>([]);
|
||||
|
||||
|
@ -13,35 +14,98 @@ onMounted(() => {
|
|||
getCertificates();
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function navigateToNew() {
|
||||
router.push("/certificates/new");
|
||||
}
|
||||
|
||||
function navigateToImport() {
|
||||
router.push("/certificates/import");
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<v-container>
|
||||
<article>
|
||||
<h2>Currently known certificates</h2>
|
||||
<section id="no-certs-found" v-if="certificates.length === 0">
|
||||
<section
|
||||
v-if="certificates.length === 0"
|
||||
id="no-certs-found"
|
||||
>
|
||||
<v-row>
|
||||
<v-col cols="12" class="text-center">
|
||||
<h3>There seems to be nothing in here.</h3>
|
||||
<v-col
|
||||
cols="12"
|
||||
class="text-center pt-4"
|
||||
>
|
||||
<p class="font-weight-bold">
|
||||
There seems to be nothing in here.
|
||||
</p>
|
||||
<p>Why don't you start with one of the following options.</p>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-btn primary>Request new certificate</v-btn>
|
||||
<v-row class="mt-4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="navigateToNew"
|
||||
>
|
||||
Request new certificate
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-btn>Import an existing certificate</v-btn>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="navigateToImport"
|
||||
>
|
||||
Import an existing certificate
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</section>
|
||||
<section aria-label="Bekannte Zertifikate">
|
||||
<ul v-if="certificates.length > 0">
|
||||
<li v-for="cert in certificates" :key="cert.fingerprint">
|
||||
<section
|
||||
v-if="certificates.length > 0"
|
||||
id="known-certificates"
|
||||
>
|
||||
<ul>
|
||||
<li
|
||||
v-for="cert in certificates"
|
||||
:key="cert.fingerprint"
|
||||
>
|
||||
<p>{{ cert.fingerprint }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-btn @click="navigateToNew">
|
||||
Request a new certificate
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="navigateToImport"
|
||||
>
|
||||
Import an existing certificate
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</section>
|
||||
</main>
|
||||
</article>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import CertificateEditor from "@/components/CertificateEditor.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>New certificate</h2>
|
||||
<v-container>
|
||||
<CertificateEditor />
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
|
4
frontend/src/types/certificate.d.ts
vendored
4
frontend/src/types/certificate.d.ts
vendored
|
@ -13,9 +13,7 @@ export interface Validity {
|
|||
to: Date;
|
||||
}
|
||||
|
||||
export interface Extensions {
|
||||
|
||||
}
|
||||
export type Extensions = object;
|
||||
|
||||
export interface Certificate {
|
||||
issuer: Subject;
|
||||
|
|
27
frontend/typed-router.d.ts
vendored
27
frontend/typed-router.d.ts
vendored
|
@ -1,27 +0,0 @@
|
|||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
|
||||
// It's recommended to commit this file.
|
||||
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
|
||||
|
||||
declare module 'vue-router/auto-routes' {
|
||||
import type {
|
||||
RouteRecordInfo,
|
||||
ParamValue,
|
||||
ParamValueOneOrMore,
|
||||
ParamValueZeroOrMore,
|
||||
ParamValueZeroOrOne,
|
||||
} from 'vue-router'
|
||||
|
||||
/**
|
||||
* Route name map generated by unplugin-vue-router
|
||||
*/
|
||||
export interface RouteNamedMap {
|
||||
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
||||
'/cert-request': RouteRecordInfo<'/cert-request', '/cert-request', Record<never, never>, Record<never, never>>,
|
||||
'/certificates/': RouteRecordInfo<'/certificates/', '/certificates', Record<never, never>, Record<never, never>>,
|
||||
'/certificates/[fingerprint]': RouteRecordInfo<'/certificates/[fingerprint]', '/certificates/:fingerprint', { fingerprint: ParamValue<true> }, { fingerprint: ParamValue<false> }>,
|
||||
'/certificates/new': RouteRecordInfo<'/certificates/new', '/certificates/new', Record<never, never>, Record<never, never>>,
|
||||
}
|
||||
}
|
|
@ -9,7 +9,9 @@ import { fileURLToPath, URL } from 'node:url'
|
|||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
VueRouter(),
|
||||
VueRouter({
|
||||
dts: './src/generated/typed-routes.d.ts'
|
||||
}),
|
||||
Vue({
|
||||
template: { transformAssetUrls },
|
||||
}),
|
||||
|
@ -19,7 +21,9 @@ export default defineConfig({
|
|||
configFile: 'src/styles/settings.scss',
|
||||
},
|
||||
}),
|
||||
Components(),
|
||||
Components({
|
||||
dts: './src/generated/components.d.ts',
|
||||
}),
|
||||
ViteFonts({
|
||||
google: {
|
||||
families: [ {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue