Compare commits

..

4 commits

Author SHA1 Message Date
0e60183372
fix: Selection not working in country autocomplete
Some checks failed
Build / build (pull_request) Successful in 39s
Check formatting / check-formatting (pull_request) Failing after 16s
2025-07-12 15:16:04 +02:00
ed948d642a
chore: Update Vuetify 2025-07-12 15:15:47 +02:00
c923c2dbf4
feat: Install Vue devtools 2025-07-12 14:19:07 +02:00
b0650a5bf4
chore: Minor optimizations
- Optimize code, update a validation message and fix code inspection warning
2025-07-12 12:45:18 +02:00
6 changed files with 982 additions and 57 deletions

View file

@ -19,7 +19,7 @@
"openapi-fetch": "^0.14.0", "openapi-fetch": "^0.14.0",
"roboto-fontface": "*", "roboto-fontface": "*",
"vue": "^3.5.13", "vue": "^3.5.13",
"vuetify": "^3.8.1" "vuetify": "^3.9.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.24.0", "@eslint/js": "^9.24.0",
@ -42,6 +42,7 @@
"unplugin-vue-components": "^28.5.0", "unplugin-vue-components": "^28.5.0",
"unplugin-vue-router": "^0.12.0", "unplugin-vue-router": "^0.12.0",
"vite": "^6.2.6", "vite": "^6.2.6",
"vite-plugin-vue-devtools": "^7.7.7",
"vite-plugin-vuetify": "^2.1.1", "vite-plugin-vuetify": "^2.1.1",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"vue-tsc": "^2.2.8" "vue-tsc": "^2.2.8"

950
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, useId, useTemplateRef } from "vue"; import { computed, ref, useId, useTemplateRef } from "vue";
import CountryListItem from "./CountryListItem.vue"; import CountryListItem from "./CountryListItem.vue";
import { Country, getAvailableCountries } from "../utils/countries"; import { type Country, getAvailableCountries, userCountry } from "../utils/countries.ts";
const formModel = ref(false); const formModel = ref(false);
const formRef = useTemplateRef("form"); const formRef = useTemplateRef("form");
@ -16,16 +16,13 @@ const commonName = ref("");
const emailAddress = ref(""); const emailAddress = ref("");
const organization = ref(""); const organization = ref("");
const organizationalUnit = ref(""); const organizationalUnit = ref("");
const country = ref(""); const country = ref(userCountry?.alpha2);
const countryName = ref(""); const countryName = ref("");
const state = ref(""); const state = ref("");
const locality = ref(""); const locality = ref("");
// Countries data for autocomplete // Countries data for autocomplete
const countries = ref<(Country | { divider: boolean; header: string })[]>([]); const countries = ref(getAvailableCountries());
// Initialize countries list
countries.value = getAvailableCountries();
// Certificate settings // Certificate settings
const keyLength = ref(4096); const keyLength = ref(4096);
@ -35,25 +32,25 @@ const noExpiry = ref(false);
// Validation rules // Validation rules
const domainRules = [ const domainRules = [
(value: string) => !!value || "A value must be provided to add the domain.", (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.)", (value: string) => /^[a-z0-9][a-z0-9.\-_]+$|^xn--[a-z0-9.\-_]+$/i.test(value) || "Invalid domain characters provided. (To use some special characters, convert the domain to xn-domain format.)"
] ];
const emailRules = [ const emailRules = [
(value: string) => !value || /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value) || "Invalid email format.", (value: string) => !value || /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value) || "Invalid email format."
]; ];
const keyLengthRules = [ const keyLengthRules = [
(value: number) => value >= 3000 || "Key length must be at least 3000 bits.", (value: number) => value >= 3000 || "Key length must be at least 3000 bits.",
(value: number) => value <= 6000 || "Key length must not exceed 6000 bits.", (value: number) => value <= 6000 || "Key length must not exceed 6000 bits."
]; ];
const validityDaysRules = [ const validityDaysRules = [
(value: number) => !value || value > 0 || "Validity period (in days) must be a positive integer.", (value: number) => !value || value > 0 || "Validity period (in days) must be a positive integer."
]; ];
function addDomain() { function addDomain() {
if (formRef.value?.errors.some(({id}) => domainInputId === id)) { if (formRef.value?.errors.some(({ id }) => domainInputId === id)) {
return; return;
} }
@ -67,6 +64,7 @@ function addDomain() {
} }
const isSubmitting = ref(false); const isSubmitting = ref(false);
async function submitNewCertificate() { async function submitNewCertificate() {
if (!formRef.value?.validate()) { if (!formRef.value?.validate()) {
return; return;
@ -75,7 +73,7 @@ async function submitNewCertificate() {
isSubmitting.value = true; isSubmitting.value = true;
try { try {
// Extract country code if country is an object // Extract country code if country is an object
const countryCode = typeof country.value === 'object' && country.value !== null const countryCode = typeof country.value === "object" && country.value !== null
? (country.value as Country).alpha2 ? (country.value as Country).alpha2
: country.value; : country.value;
@ -91,10 +89,10 @@ async function submitNewCertificate() {
organizationalUnit: organizationalUnit.value, organizationalUnit: organizationalUnit.value,
country: countryCode, country: countryCode,
state: state.value, state: state.value,
locality: locality.value, locality: locality.value
}, },
extension: { extension: {
alternativeDnsNames: domains.value, alternativeDnsNames: domains.value
} }
}); });
} finally { } finally {
@ -147,8 +145,11 @@ async function submitNewCertificate() {
hint="Search and select your country" hint="Search and select your country"
class="mb-2" class="mb-2"
> >
<template #item="slotProps"> <template #item="{ props, item }">
<CountryListItem :item="slotProps.item" :props="slotProps.props" /> <CountryListItem
:item="item"
:item-props="props"
/>
</template> </template>
</v-autocomplete> </v-autocomplete>

View file

@ -1,45 +1,44 @@
<script setup lang="ts"> <script setup lang="ts">
// Define props with proper TypeScript typing // Define props with proper TypeScript typing
import { computed } from "vue"; import { computed } from "vue";
import type { CountryListItemType } from "@/utils/countries.ts";
interface Country { type ListItem<T> = {
country: string; raw: T;
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 // Define props with proper TypeScript typing
const props = defineProps<{ const props = defineProps<{
item: { raw: CountryListItemType }; item: ListItem<CountryListItemType>;
props?: Record<string, unknown>; itemProps?: Record<string, unknown>;
}>(); }>();
// Helper functions to check the type of the item // Helper functions to check the type of the item
const isDivider = computed(() => { const isDivider = computed(() => {
return 'divider' in props.item.raw && props.item.raw.divider; return "divider" in props.item.raw && props.item.raw.divider;
}); });
const headerText = computed(() => { const headerText = computed(() => {
return 'header' in props.item.raw ? props.item.raw.header : ""; return "header" in props.item.raw ? props.item.raw.header : "";
}); });
const countryItem = computed(() => { const countryItem = computed(() => {
return !isDivider.value && !headerText.value && 'country' in props.item.raw ? props.item.raw: undefined; return !isDivider.value && !headerText.value && "country" in props.item.raw ? props.item.raw : undefined;
}) });
</script> </script>
<template> <template>
<v-divider v-if="isDivider" class="my-2" /> <v-divider
<v-list-subheader v-if="headerText">{{ headerText }}</v-list-subheader> v-if="isDivider"
<v-list-item v-if="countryItem" v-bind="props" :title="countryItem.country" :value="countryItem.alpha2" /> class="my-2"
/>
<v-list-subheader v-if="headerText">
{{ headerText }}
</v-list-subheader>
<v-list-item
v-if="countryItem"
v-bind="itemProps"
:title="countryItem.country"
/>
</template> </template>

View file

@ -1,6 +1,6 @@
{ {
"extends": "@vue/tsconfig/tsconfig.dom.json", "extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "include": ["env.d.ts", "src/**/*", "src/**/*.ts", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,

View file

@ -2,6 +2,7 @@ import Components from 'unplugin-vue-components/vite'
import Vue from '@vitejs/plugin-vue' import Vue from '@vitejs/plugin-vue'
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify' import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
import ViteFonts from 'unplugin-fonts/vite' import ViteFonts from 'unplugin-fonts/vite'
import VueDevTools from 'vite-plugin-vue-devtools';
import VueRouter from 'unplugin-vue-router/vite' import VueRouter from 'unplugin-vue-router/vite'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
@ -32,6 +33,9 @@ export default defineConfig({
}], }],
}, },
}), }),
VueDevTools({
launchEditor: process.env.DEVTOOLS_LAUNCH_EDITOR ?? 'idea',
}),
], ],
define: { 'process.env': {} }, define: { 'process.env': {} },
resolve: { resolve: {