mirror of
https://github.com/CorentinTh/it-tools.git
synced 2026-08-27 04:05:27 +00:00
Merge f6753316dc into d505845f91
This commit is contained in:
commit
99ee47bddd
14 changed files with 883 additions and 7 deletions
25
components.d.ts
vendored
25
components.d.ts
vendored
|
|
@ -91,6 +91,7 @@ declare module '@vue/runtime-core' {
|
|||
IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default']
|
||||
'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default']
|
||||
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
|
||||
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
|
||||
IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
|
||||
IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
|
||||
IconMdiClose: typeof import('~icons/mdi/close')['default']
|
||||
|
|
@ -98,6 +99,7 @@ declare module '@vue/runtime-core' {
|
|||
IconMdiEye: typeof import('~icons/mdi/eye')['default']
|
||||
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
|
||||
IconMdiHeart: typeof import('~icons/mdi/heart')['default']
|
||||
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||
IconMdiSearch: typeof import('~icons/mdi/search')['default']
|
||||
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
|
||||
IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default']
|
||||
|
|
@ -129,20 +131,38 @@ declare module '@vue/runtime-core' {
|
|||
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
|
||||
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
|
||||
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NCode: typeof import('naive-ui')['NCode']
|
||||
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
||||
NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDatePicker: typeof import('naive-ui')['NDatePicker']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NGi: typeof import('naive-ui')['NGi']
|
||||
NGrid: typeof import('naive-ui')['NGrid']
|
||||
NH1: typeof import('naive-ui')['NH1']
|
||||
NH2: typeof import('naive-ui')['NH2']
|
||||
NH3: typeof import('naive-ui')['NH3']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||
NMenu: typeof import('naive-ui')['NMenu']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTable: typeof import('naive-ui')['NTable']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
|
||||
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
|
||||
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']
|
||||
|
|
@ -186,6 +206,7 @@ declare module '@vue/runtime-core' {
|
|||
UserAgentResultCards: typeof import('./src/tools/user-agent-parser/user-agent-result-cards.vue')['default']
|
||||
UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default']
|
||||
WifiQrCodeGenerator: typeof import('./src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue')['default']
|
||||
X509CertificateGenerator: typeof import('./src/tools/x509-certificate-generator/x509-certificate-generator.vue')['default']
|
||||
XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default']
|
||||
XmlToJson: typeof import('./src/tools/xml-to-json/xml-to-json.vue')['default']
|
||||
YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default']
|
||||
|
|
|
|||
|
|
@ -97,6 +97,35 @@ tools:
|
|||
description: >-
|
||||
Ermittle die Stärke deines Passworts mit diesem Client-seitigen
|
||||
Passwortstärken-Analysator und Tool zur Schätzung der Knackzeit.
|
||||
x509-certificate-generator:
|
||||
title: X509-Zertifikat-Generator
|
||||
description: Erstellen Sie selbstsignierte X509-Zertifikate mit anpassbaren Parametern. Erstellen Sie Root-CA-Zertifikate für Entwicklung und Tests.
|
||||
|
||||
commonName: Allgemeiner Name (CN)
|
||||
country: Land (C)
|
||||
stateOrProvince: Bundesland/Provinz (ST)
|
||||
locality: Ort (L)
|
||||
organization: Organisation (O)
|
||||
organizationalUnit: Organisationseinheit (OU)
|
||||
email: E-Mail
|
||||
validFrom: Gültig ab
|
||||
validTo: Gültig bis
|
||||
regenerateKeys: Schlüssel neu generieren
|
||||
certificate: Zertifikat
|
||||
privateKey: Privater Schlüssel
|
||||
download: Herunterladen
|
||||
commonNamePlaceholder: example.com
|
||||
countryPlaceholder: DE
|
||||
stateOrProvincePlaceholder: Bayern
|
||||
localityPlaceholder: München
|
||||
organizationPlaceholder: Meine Firma
|
||||
organizationalUnitPlaceholder: IT-Abteilung
|
||||
emailPlaceholder: 'admin [at] example.com'
|
||||
requiredField: '{field} ist erforderlich'
|
||||
countryMustBe2Letters: Land muss ein 2-Buchstaben-ISO-Code sein
|
||||
invalidEmailFormat: Ungültiges E-Mail-Format
|
||||
endDateMustBeAfterStart: Enddatum muss nach dem Startdatum liegen
|
||||
fillRequiredFields: Bitte füllen Sie die Pflichtfelder korrekt aus, um das Zertifikat zu generieren.
|
||||
chronometer:
|
||||
title: Chronometer
|
||||
description: >-
|
||||
|
|
|
|||
|
|
@ -75,6 +75,36 @@ tools:
|
|||
title: Password strength analyser
|
||||
description: Discover the strength of your password with this client-side-only password strength analyser and crack time estimation tool.
|
||||
|
||||
x509-certificate-generator:
|
||||
title: X509 Certificate Generator
|
||||
description: Generate self-signed X509 certificates with customizable parameters. Create root CA certificates for development and testing.
|
||||
|
||||
commonName: Common Name (CN)
|
||||
country: Country (C)
|
||||
stateOrProvince: State/Province (ST)
|
||||
locality: Locality (L)
|
||||
organization: Organization (O)
|
||||
organizationalUnit: Organizational Unit (OU)
|
||||
email: Email
|
||||
validFrom: Valid From
|
||||
validTo: Valid To
|
||||
regenerateKeys: Regenerate Keys
|
||||
certificate: Certificate
|
||||
privateKey: Private Key
|
||||
download: Download
|
||||
commonNamePlaceholder: example.com
|
||||
countryPlaceholder: US
|
||||
stateOrProvincePlaceholder: California
|
||||
localityPlaceholder: San Francisco
|
||||
organizationPlaceholder: My Company
|
||||
organizationalUnitPlaceholder: IT Department
|
||||
emailPlaceholder: 'admin [at] example.com'
|
||||
requiredField: '{field} is required'
|
||||
countryMustBe2Letters: Country must be a 2-letter ISO code
|
||||
invalidEmailFormat: Invalid email format
|
||||
endDateMustBeAfterStart: End date must be after start date
|
||||
fillRequiredFields: Please fill in the required fields correctly to generate the certificate.
|
||||
|
||||
chronometer:
|
||||
title: Chronometer
|
||||
description: Monitor the duration of a thing. Basically a chronometer with simple chronometer features.
|
||||
|
|
|
|||
|
|
@ -75,6 +75,36 @@ tools:
|
|||
title: Analyseverktøy for passordstyrke
|
||||
description: Oppdag styrken av passordet ditt med dette kun-klient-maskin passordstyrke analyse verktøyet og se den estimerte knekketiden.
|
||||
|
||||
x509-certificate-generator:
|
||||
title: X509-sertifikatgenerator
|
||||
description: Generer selvsignerte X509-sertifikater med tilpassbare parametere. Opprett rot-CA-sertifikater for utvikling og testing.
|
||||
|
||||
commonName: Vanlig navn (CN)
|
||||
country: Land (C)
|
||||
stateOrProvince: Stat/Provins (ST)
|
||||
locality: Sted (L)
|
||||
organization: Organisasjon (O)
|
||||
organizationalUnit: Organisasjonsenhet (OU)
|
||||
email: E-post
|
||||
validFrom: Gyldig fra
|
||||
validTo: Gyldig til
|
||||
regenerateKeys: Regenerer nøkler
|
||||
certificate: Sertifikat
|
||||
privateKey: Privat nøkkel
|
||||
download: Last ned
|
||||
commonNamePlaceholder: example.com
|
||||
countryPlaceholder: NO
|
||||
stateOrProvincePlaceholder: California
|
||||
localityPlaceholder: San Francisco
|
||||
organizationPlaceholder: Mitt Selskap
|
||||
organizationalUnitPlaceholder: IT-avdeling
|
||||
emailPlaceholder: 'admin [at] example.com'
|
||||
requiredField: '{field} er påkrevd'
|
||||
countryMustBe2Letters: Land må være en 2-bokstavers ISO-kode
|
||||
invalidEmailFormat: Ugyldig e-postformat
|
||||
endDateMustBeAfterStart: Sluttdato må være etter startdato
|
||||
fillRequiredFields: Vennligst fyll ut de obligatoriske feltene korrekt for å generere sertifikatet.
|
||||
|
||||
chronometer:
|
||||
title: Kronometer
|
||||
description: Overvåk varigheten av noe. I bunn og grunn et kronometer med enkle funksjoner.
|
||||
|
|
|
|||
|
|
@ -75,6 +75,36 @@ tools:
|
|||
title: Bộ phân tích độ mạnh mật khẩu
|
||||
description: Khám phá độ mạnh của mật khẩu của bạn với công cụ phân tích độ mạnh mật khẩu chỉ chạy trên phía máy khách và ước tính thời gian phá mật khẩu.
|
||||
|
||||
x509-certificate-generator:
|
||||
title: Trình tạo chứng chỉ X509
|
||||
description: Tạo chứng chỉ X509 tự ký với các thông số có thể tùy chỉnh. Tạo chứng chỉ root CA cho phát triển và kiểm thử.
|
||||
|
||||
commonName: Tên chung (CN)
|
||||
country: Quốc gia (C)
|
||||
stateOrProvince: Tỉnh/Bang (ST)
|
||||
locality: Địa phương (L)
|
||||
organization: Tổ chức (O)
|
||||
organizationalUnit: Đơn vị tổ chức (OU)
|
||||
email: Email
|
||||
validFrom: Có hiệu lực từ
|
||||
validTo: Có hiệu lực đến
|
||||
regenerateKeys: Tạo lại khóa
|
||||
certificate: Chứng chỉ
|
||||
privateKey: Khóa riêng
|
||||
download: Tải xuống
|
||||
commonNamePlaceholder: example.com
|
||||
countryPlaceholder: VN
|
||||
stateOrProvincePlaceholder: California
|
||||
localityPlaceholder: San Francisco
|
||||
organizationPlaceholder: Công ty của tôi
|
||||
organizationalUnitPlaceholder: Bộ phận IT
|
||||
emailPlaceholder: 'admin [at] example.com'
|
||||
requiredField: '{field} là bắt buộc'
|
||||
countryMustBe2Letters: Quốc gia phải là mã ISO 2 chữ cái
|
||||
invalidEmailFormat: Định dạng email không hợp lệ
|
||||
endDateMustBeAfterStart: Ngày kết thúc phải sau ngày bắt đầu
|
||||
fillRequiredFields: Vui lòng điền đúng các trường bắt buộc để tạo chứng chỉ.
|
||||
|
||||
chronometer:
|
||||
title: Đồng hồ bấm giờ
|
||||
description: Giám sát thời gian của một sự việc. Cơ bản là một đồng hồ bấm giờ với các tính năng đơn giản.
|
||||
|
|
|
|||
|
|
@ -73,7 +73,37 @@ tools:
|
|||
|
||||
password-strength-analyser:
|
||||
title: 密码强度分析仪
|
||||
description: 使用此密码强度分析器和破解时间估计工具来发现密码的强度。
|
||||
description: 使用此密码强度分析<EFBFBD><EFBFBD><EFBFBD>和破解时间估计工具来发现密码的强度。
|
||||
|
||||
x509-certificate-generator:
|
||||
title: X509证书生成器
|
||||
description: 生成具有可自定义参数的自签名X509证书。为开发和测试创建根CA证书。
|
||||
|
||||
commonName: 通用名称 (CN)
|
||||
country: 国家 (C)
|
||||
stateOrProvince: 省/州 (ST)
|
||||
locality: 地区 (L)
|
||||
organization: 组织 (O)
|
||||
organizationalUnit: 组织单位 (OU)
|
||||
email: 邮箱
|
||||
validFrom: 有效期从
|
||||
validTo: 有效期到
|
||||
regenerateKeys: 重新生成密钥
|
||||
certificate: 证书
|
||||
privateKey: 私钥
|
||||
download: 下载
|
||||
commonNamePlaceholder: example.com
|
||||
countryPlaceholder: US
|
||||
stateOrProvincePlaceholder: 加利福尼亚
|
||||
localityPlaceholder: 旧金山
|
||||
organizationPlaceholder: 我的公司
|
||||
organizationalUnitPlaceholder: IT部门
|
||||
emailPlaceholder: 'admin [at] example.com'
|
||||
requiredField: '{field}是必填项'
|
||||
countryMustBe2Letters: 国家必须是2字母ISO代码
|
||||
invalidEmailFormat: 无效的邮箱格式
|
||||
endDateMustBeAfterStart: 结束日期必须晚于开始日期
|
||||
fillRequiredFields: 请正确填写必填字段以生成证书。
|
||||
|
||||
chronometer:
|
||||
title: 计时器
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ function activateOption(option: PaletteOption) {
|
|||
<c-input-text ref="inputRef" v-model:value="searchPrompt" raw-text placeholder="Type to search a tool or a command..." autofocus clearable />
|
||||
|
||||
<div v-for="(options, category) in filteredSearchResult" :key="category">
|
||||
<div ml-3 mt-3 text-sm font-bold text-primary op-60>
|
||||
<div ml-3 mt-3 text-sm text-primary font-bold op-60>
|
||||
{{ category }}
|
||||
</div>
|
||||
<command-palette-option v-for="option in options" :key="option.name" :option="option" :selected="selectedOptionIndex === getOptionIndex(option)" @activated="activateOption" />
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ import { tool as temperatureConverter } from './temperature-converter';
|
|||
import { tool as textStatistics } from './text-statistics';
|
||||
import { tool as tokenGenerator } from './token-generator';
|
||||
import type { ToolCategory } from './tools.types';
|
||||
import { tool as x509CertificateGenerator } from './x509-certificate-generator';
|
||||
import { tool as urlEncoder } from './url-encoder';
|
||||
import { tool as urlParser } from './url-parser';
|
||||
import { tool as uuidGenerator } from './uuid-generator';
|
||||
|
|
@ -91,7 +92,7 @@ import { tool as yamlViewer } from './yaml-viewer';
|
|||
export const toolsByCategory: ToolCategory[] = [
|
||||
{
|
||||
name: 'Crypto',
|
||||
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser, pdfSignatureChecker],
|
||||
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, x509CertificateGenerator, passwordStrengthAnalyser, pdfSignatureChecker],
|
||||
},
|
||||
{
|
||||
name: 'Converter',
|
||||
|
|
|
|||
59
src/tools/x509-certificate-generator/https-server-example.ts
Normal file
59
src/tools/x509-certificate-generator/https-server-example.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/* eslint-disable no-console */
|
||||
/**
|
||||
* HTTPS Server Example
|
||||
*
|
||||
* This is an example demonstrating how to use the generated X509 certificate
|
||||
* in a Node.js HTTPS server.
|
||||
*
|
||||
* Usage:
|
||||
* 1. Generate a certificate using the X509 Certificate Generator tool
|
||||
* 2. Save the certificate as 'certificate.crt' and private key as 'privateKey.key'
|
||||
* 3. Create a 'cert' folder in your project root
|
||||
* 4. Place the files in the cert folder
|
||||
* 5. Run this file with: ts-node https-server-example.ts
|
||||
* 6. Access the server at: https://localhost:8443
|
||||
*
|
||||
* Note: Since this is a self-signed certificate, your browser will show a security warning.
|
||||
* You can proceed safely for development/testing purposes.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import https from 'node:https';
|
||||
import path from 'node:path';
|
||||
|
||||
const PORT = 8443;
|
||||
|
||||
// Certificate and private key file paths (modify as needed)
|
||||
const crtPath = path.join(__dirname, 'cert/certificate.crt');
|
||||
const keyPath = path.join(__dirname, 'cert/privateKey.key');
|
||||
|
||||
const options: https.ServerOptions = {
|
||||
key: fs.readFileSync(keyPath),
|
||||
cert: fs.readFileSync(crtPath),
|
||||
};
|
||||
|
||||
const server = https.createServer(options, (req, res) => {
|
||||
const { method, url } = req;
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(
|
||||
JSON.stringify(
|
||||
{
|
||||
code: 0,
|
||||
message: 'HTTPS server is running',
|
||||
method,
|
||||
url,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`HTTPS server: https://localhost:${PORT}`);
|
||||
console.log(`Certificate: ${crtPath}`);
|
||||
console.log(`Private Key: ${keyPath}`);
|
||||
console.log('\nNote: Your browser will show a security warning because this is a self-signed certificate.');
|
||||
console.log('This is normal for development/testing purposes.');
|
||||
});
|
||||
12
src/tools/x509-certificate-generator/index.ts
Normal file
12
src/tools/x509-certificate-generator/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Certificate } from '@vicons/tabler';
|
||||
import { defineTool } from '../tool';
|
||||
import { translate } from '@/plugins/i18n.plugin';
|
||||
|
||||
export const tool = defineTool({
|
||||
name: translate('tools.x509-certificate-generator.title'),
|
||||
path: '/x509-certificate-generator',
|
||||
description: translate('tools.x509-certificate-generator.description'),
|
||||
keywords: ['x509', 'certificate', 'ssl', 'tls', 'pem', 'generator', 'root', 'ca', 'self-signed'],
|
||||
component: () => import('./x509-certificate-generator.vue'),
|
||||
icon: Certificate,
|
||||
});
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
import { md, pki } from 'node-forge';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export interface X509CertificateOptions {
|
||||
commonName: string
|
||||
country: string
|
||||
stateOrProvince?: string
|
||||
locality?: string
|
||||
organization?: string
|
||||
organizationalUnit?: string
|
||||
email?: string
|
||||
date: [string, string]
|
||||
}
|
||||
|
||||
export interface X509CertificateResult {
|
||||
certificate: string
|
||||
privateKey: string
|
||||
}
|
||||
|
||||
function getUuid(): string {
|
||||
return uuidv4().replace(/-/g, '');
|
||||
}
|
||||
|
||||
export function generateX509(options: X509CertificateOptions): X509CertificateResult {
|
||||
const {
|
||||
commonName,
|
||||
country,
|
||||
stateOrProvince,
|
||||
locality,
|
||||
organization,
|
||||
organizationalUnit,
|
||||
email,
|
||||
date: [startDate, endDate],
|
||||
} = options;
|
||||
|
||||
// Generate RSA key pair, 2048 bits
|
||||
const keys = pki.rsa.generateKeyPair(2048);
|
||||
|
||||
// Create a new X.509 certificate
|
||||
const cert = pki.createCertificate();
|
||||
cert.publicKey = keys.publicKey;
|
||||
|
||||
// Set certificate serial number using UUID
|
||||
cert.serialNumber = getUuid();
|
||||
|
||||
// Set certificate validity period
|
||||
cert.validity.notBefore = new Date(startDate);
|
||||
cert.validity.notAfter = new Date(endDate);
|
||||
|
||||
// Define certificate subject attributes
|
||||
const attrs: pki.CertificateField[] = [
|
||||
{
|
||||
name: 'countryName',
|
||||
value: country,
|
||||
},
|
||||
{
|
||||
shortName: 'ST',
|
||||
value: stateOrProvince || '',
|
||||
},
|
||||
{
|
||||
name: 'localityName',
|
||||
value: locality || '',
|
||||
},
|
||||
{
|
||||
name: 'organizationName',
|
||||
value: organization || '',
|
||||
},
|
||||
{
|
||||
shortName: 'OU',
|
||||
value: organizationalUnit || '',
|
||||
},
|
||||
];
|
||||
|
||||
// Handle multiple domains in commonName (comma-separated)
|
||||
const domainList = commonName.split(',').map(item => item.trim());
|
||||
domainList.forEach((domain) => {
|
||||
attrs.push({
|
||||
name: 'commonName',
|
||||
value: domain,
|
||||
});
|
||||
});
|
||||
|
||||
// Set certificate subject and issuer
|
||||
cert.setSubject(attrs);
|
||||
cert.setIssuer(attrs);
|
||||
|
||||
// Build Subject Alternative Names (SAN)
|
||||
const altNames: Array<{ type: number; value: string }> = [];
|
||||
if (email) {
|
||||
altNames.push({ type: 1, value: email }); // 1: Email
|
||||
}
|
||||
|
||||
domainList.forEach((domain) => {
|
||||
// Check if it's an IP address (contains only digits and dots, and has 4 parts)
|
||||
if (/\d+/.test(domain.replace(/\./g, '')) && domain.split('.').length === 4) {
|
||||
altNames.push({ type: 7, value: domain }); // 7: IP Address
|
||||
}
|
||||
else {
|
||||
altNames.push({ type: 2, value: domain }); // 2: DNS
|
||||
}
|
||||
});
|
||||
|
||||
// Set certificate extensions
|
||||
cert.setExtensions([
|
||||
{
|
||||
name: 'basicConstraints',
|
||||
cA: true,
|
||||
},
|
||||
{
|
||||
name: 'subjectAltName',
|
||||
altNames,
|
||||
},
|
||||
]);
|
||||
|
||||
// Self-sign the certificate with SHA256
|
||||
cert.sign(keys.privateKey, md.sha256.create());
|
||||
|
||||
// Convert to PEM format
|
||||
const pem = {
|
||||
privateKey: pki.privateKeyToPem(keys.privateKey),
|
||||
certificate: pki.certificateToPem(cert),
|
||||
};
|
||||
|
||||
return pem;
|
||||
}
|
||||
|
|
@ -0,0 +1,509 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { X509CertificateOptions } from './x509-certificate-generator.service';
|
||||
import { generateX509 } from './x509-certificate-generator.service';
|
||||
import { computedRefreshable } from '@/composable/computedRefreshable';
|
||||
import { useValidation } from '@/composable/validation';
|
||||
import { withDefaultOnError } from '@/utils/defaults';
|
||||
import TextareaCopyable from '@/components/TextareaCopyable.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Country codes list (ISO 3166-1 alpha-2)
|
||||
const countryCodes = [
|
||||
{ label: 'AF', value: 'AF' },
|
||||
{ label: 'AL', value: 'AL' },
|
||||
{ label: 'AG', value: 'AG' },
|
||||
{ label: 'AQ', value: 'AQ' },
|
||||
{ label: 'AN', value: 'AN' },
|
||||
{ label: 'AO', value: 'AO' },
|
||||
{ label: 'AV', value: 'AV' },
|
||||
{ label: 'AY', value: 'AY' },
|
||||
{ label: 'AC', value: 'AC' },
|
||||
{ label: 'AR', value: 'AR' },
|
||||
{ label: 'AM', value: 'AM' },
|
||||
{ label: 'AA', value: 'AA' },
|
||||
{ label: 'AS', value: 'AS' },
|
||||
{ label: 'AU', value: 'AU' },
|
||||
{ label: 'AJ', value: 'AJ' },
|
||||
{ label: 'BF', value: 'BF' },
|
||||
{ label: 'BA', value: 'BA' },
|
||||
{ label: 'BG', value: 'BG' },
|
||||
{ label: 'BB', value: 'BB' },
|
||||
{ label: 'BO', value: 'BO' },
|
||||
{ label: 'BE', value: 'BE' },
|
||||
{ label: 'BH', value: 'BH' },
|
||||
{ label: 'BN', value: 'BN' },
|
||||
{ label: 'BD', value: 'BD' },
|
||||
{ label: 'BT', value: 'BT' },
|
||||
{ label: 'BL', value: 'BL' },
|
||||
{ label: 'BK', value: 'BK' },
|
||||
{ label: 'BC', value: 'BC' },
|
||||
{ label: 'BV', value: 'BV' },
|
||||
{ label: 'BR', value: 'BR' },
|
||||
{ label: 'IO', value: 'IO' },
|
||||
{ label: 'VI', value: 'VI' },
|
||||
{ label: 'BX', value: 'BX' },
|
||||
{ label: 'BU', value: 'BU' },
|
||||
{ label: 'UV', value: 'UV' },
|
||||
{ label: 'BM', value: 'BM' },
|
||||
{ label: 'BY', value: 'BY' },
|
||||
{ label: 'CV', value: 'CV' },
|
||||
{ label: 'CB', value: 'CB' },
|
||||
{ label: 'CM', value: 'CM' },
|
||||
{ label: 'CA', value: 'CA' },
|
||||
{ label: 'CJ', value: 'CJ' },
|
||||
{ label: 'CT', value: 'CT' },
|
||||
{ label: 'CD', value: 'CD' },
|
||||
{ label: 'CI', value: 'CI' },
|
||||
{ label: 'CH', value: 'CH' },
|
||||
{ label: 'KT', value: 'KT' },
|
||||
{ label: 'CK', value: 'CK' },
|
||||
{ label: 'CO', value: 'CO' },
|
||||
{ label: 'CN', value: 'CN' },
|
||||
{ label: 'CG', value: 'CG' },
|
||||
{ label: 'CF', value: 'CF' },
|
||||
{ label: 'CW', value: 'CW' },
|
||||
{ label: 'CS', value: 'CS' },
|
||||
{ label: 'IV', value: 'IV' },
|
||||
{ label: 'HR', value: 'HR' },
|
||||
{ label: 'CU', value: 'CU' },
|
||||
{ label: 'UC', value: 'UC' },
|
||||
{ label: 'CY', value: 'CY' },
|
||||
{ label: 'EZ', value: 'EZ' },
|
||||
{ label: 'DA', value: 'DA' },
|
||||
{ label: 'DJ', value: 'DJ' },
|
||||
{ label: 'DO', value: 'DO' },
|
||||
{ label: 'DR', value: 'DR' },
|
||||
{ label: 'EC', value: 'EC' },
|
||||
{ label: 'EG', value: 'EG' },
|
||||
{ label: 'ES', value: 'ES' },
|
||||
{ label: 'EK', value: 'EK' },
|
||||
{ label: 'ER', value: 'ER' },
|
||||
{ label: 'EN', value: 'EN' },
|
||||
{ label: 'ET', value: 'ET' },
|
||||
{ label: 'FK', value: 'FK' },
|
||||
{ label: 'FO', value: 'FO' },
|
||||
{ label: 'FJ', value: 'FJ' },
|
||||
{ label: 'FI', value: 'FI' },
|
||||
{ label: 'FR', value: 'FR' },
|
||||
{ label: 'FX', value: 'FX' },
|
||||
{ label: 'FG', value: 'FG' },
|
||||
{ label: 'FP', value: 'FP' },
|
||||
{ label: 'FS', value: 'FS' },
|
||||
{ label: 'GB', value: 'GB' },
|
||||
{ label: 'GA', value: 'GA' },
|
||||
{ label: 'GZ', value: 'GZ' },
|
||||
{ label: 'GG', value: 'GG' },
|
||||
{ label: 'GM', value: 'GM' },
|
||||
{ label: 'GH', value: 'GH' },
|
||||
{ label: 'GI', value: 'GI' },
|
||||
{ label: 'GR', value: 'GR' },
|
||||
{ label: 'GL', value: 'GL' },
|
||||
{ label: 'GJ', value: 'GJ' },
|
||||
{ label: 'GP', value: 'GP' },
|
||||
{ label: 'GQ', value: 'GQ' },
|
||||
{ label: 'GT', value: 'GT' },
|
||||
{ label: 'GZ', value: 'GZ' },
|
||||
{ label: 'GK', value: 'GK' },
|
||||
{ label: 'GV', value: 'GV' },
|
||||
{ label: 'PU', value: 'PU' },
|
||||
{ label: 'GY', value: 'GY' },
|
||||
{ label: 'HA', value: 'HA' },
|
||||
{ label: 'HM', value: 'HM' },
|
||||
{ label: 'VT', value: 'VT' },
|
||||
{ label: 'HO', value: 'HO' },
|
||||
{ label: 'HK', value: 'HK' },
|
||||
{ label: 'HU', value: 'HU' },
|
||||
{ label: 'IC', value: 'IC' },
|
||||
{ label: 'IN', value: 'IN' },
|
||||
{ label: 'ID', value: 'ID' },
|
||||
{ label: 'IR', value: 'IR' },
|
||||
{ label: 'IZ', value: 'IZ' },
|
||||
{ label: 'EI', value: 'EI' },
|
||||
{ label: 'IM', value: 'IM' },
|
||||
{ label: 'IS', value: 'IS' },
|
||||
{ label: 'IT', value: 'IT' },
|
||||
{ label: 'JM', value: 'JM' },
|
||||
{ label: 'JA', value: 'JA' },
|
||||
{ label: 'JE', value: 'JE' },
|
||||
{ label: 'JO', value: 'JO' },
|
||||
{ label: 'KZ', value: 'KZ' },
|
||||
{ label: 'KE', value: 'KE' },
|
||||
{ label: 'KR', value: 'KR' },
|
||||
{ label: 'KN', value: 'KN' },
|
||||
{ label: 'KS', value: 'KS' },
|
||||
{ label: 'KV', value: 'KV' },
|
||||
{ label: 'KU', value: 'KU' },
|
||||
{ label: 'KG', value: 'KG' },
|
||||
{ label: 'LA', value: 'LA' },
|
||||
{ label: 'LG', value: 'LG' },
|
||||
{ label: 'LE', value: 'LE' },
|
||||
{ label: 'LT', value: 'LT' },
|
||||
{ label: 'LI', value: 'LI' },
|
||||
{ label: 'LY', value: 'LY' },
|
||||
{ label: 'LS', value: 'LS' },
|
||||
{ label: 'LH', value: 'LH' },
|
||||
{ label: 'LU', value: 'LU' },
|
||||
{ label: 'MC', value: 'MC' },
|
||||
{ label: 'MK', value: 'MK' },
|
||||
{ label: 'MA', value: 'MA' },
|
||||
{ label: 'MI', value: 'MI' },
|
||||
{ label: 'MY', value: 'MY' },
|
||||
{ label: 'MV', value: 'MV' },
|
||||
{ label: 'ML', value: 'ML' },
|
||||
{ label: 'MT', value: 'MT' },
|
||||
{ label: 'RM', value: 'RM' },
|
||||
{ label: 'MB', value: 'MB' },
|
||||
{ label: 'MR', value: 'MR' },
|
||||
{ label: 'MP', value: 'MP' },
|
||||
{ label: 'MF', value: 'MF' },
|
||||
{ label: 'MX', value: 'MX' },
|
||||
{ label: 'FM', value: 'FM' },
|
||||
{ label: 'MD', value: 'MD' },
|
||||
{ label: 'MN', value: 'MN' },
|
||||
{ label: 'MG', value: 'MG' },
|
||||
{ label: 'MJ', value: 'MJ' },
|
||||
{ label: 'MH', value: 'MH' },
|
||||
{ label: 'MO', value: 'MO' },
|
||||
{ label: 'MZ', value: 'MZ' },
|
||||
{ label: 'WA', value: 'WA' },
|
||||
{ label: 'NR', value: 'NR' },
|
||||
{ label: 'NP', value: 'NP' },
|
||||
{ label: 'NL', value: 'NL' },
|
||||
{ label: 'NT', value: 'NT' },
|
||||
{ label: 'NC', value: 'NC' },
|
||||
{ label: 'NZ', value: 'NZ' },
|
||||
{ label: 'NU', value: 'NU' },
|
||||
{ label: 'NG', value: 'NG' },
|
||||
{ label: 'NI', value: 'NI' },
|
||||
{ label: 'NE', value: 'NE' },
|
||||
{ label: 'NF', value: 'NF' },
|
||||
{ label: 'CQ', value: 'CQ' },
|
||||
{ label: 'NO', value: 'NO' },
|
||||
{ label: 'MU', value: 'MU' },
|
||||
{ label: 'PK', value: 'PK' },
|
||||
{ label: 'PS', value: 'PS' },
|
||||
{ label: 'PM', value: 'PM' },
|
||||
{ label: 'PP', value: 'PP' },
|
||||
{ label: 'PA', value: 'PA' },
|
||||
{ label: 'PE', value: 'PE' },
|
||||
{ label: 'RP', value: 'RP' },
|
||||
{ label: 'PC', value: 'PC' },
|
||||
{ label: 'PL', value: 'PL' },
|
||||
{ label: 'PO', value: 'PO' },
|
||||
{ label: 'RQ', value: 'RQ' },
|
||||
{ label: 'QA', value: 'QA' },
|
||||
{ label: 'RE', value: 'RE' },
|
||||
{ label: 'RO', value: 'RO' },
|
||||
{ label: 'TW', value: 'TW' },
|
||||
{ label: 'RS', value: 'RS' },
|
||||
{ label: 'RW', value: 'RW' },
|
||||
{ label: 'TB', value: 'TB' },
|
||||
{ label: 'SH', value: 'SH' },
|
||||
{ label: 'SC', value: 'SC' },
|
||||
{ label: 'ST', value: 'ST' },
|
||||
{ label: 'RN', value: 'RN' },
|
||||
{ label: 'VC', value: 'VC' },
|
||||
{ label: 'WS', value: 'WS' },
|
||||
{ label: 'SM', value: 'SM' },
|
||||
{ label: 'TP', value: 'TP' },
|
||||
{ label: 'SA', value: 'SA' },
|
||||
{ label: 'SG', value: 'SG' },
|
||||
{ label: 'RI', value: 'RI' },
|
||||
{ label: 'SE', value: 'SE' },
|
||||
{ label: 'SL', value: 'SL' },
|
||||
{ label: 'SN', value: 'SN' },
|
||||
{ label: 'NN', value: 'NN' },
|
||||
{ label: 'LO', value: 'LO' },
|
||||
{ label: 'SI', value: 'SI' },
|
||||
{ label: 'BP', value: 'BP' },
|
||||
{ label: 'SO', value: 'SO' },
|
||||
{ label: 'SF', value: 'SF' },
|
||||
{ label: 'SX', value: 'SX' },
|
||||
{ label: 'OD', value: 'OD' },
|
||||
{ label: 'SP', value: 'SP' },
|
||||
{ label: 'CE', value: 'CE' },
|
||||
{ label: 'SU', value: 'SU' },
|
||||
{ label: 'NS', value: 'NS' },
|
||||
{ label: 'SB', value: 'SB' },
|
||||
{ label: 'WZ', value: 'WZ' },
|
||||
{ label: 'SW', value: 'SW' },
|
||||
{ label: 'SZ', value: 'SZ' },
|
||||
{ label: 'SY', value: 'SY' },
|
||||
{ label: 'TI', value: 'TI' },
|
||||
{ label: 'TZ', value: 'TZ' },
|
||||
{ label: 'TH', value: 'TH' },
|
||||
{ label: 'TT', value: 'TT' },
|
||||
{ label: 'TO', value: 'TO' },
|
||||
{ label: 'TL', value: 'TL' },
|
||||
{ label: 'TN', value: 'TN' },
|
||||
{ label: 'TD', value: 'TD' },
|
||||
{ label: 'TS', value: 'TS' },
|
||||
{ label: 'TU', value: 'TU' },
|
||||
{ label: 'TX', value: 'TX' },
|
||||
{ label: 'TK', value: 'TK' },
|
||||
{ label: 'TV', value: 'TV' },
|
||||
{ label: 'UG', value: 'UG' },
|
||||
{ label: 'UP', value: 'UP' },
|
||||
{ label: 'AE', value: 'AE' },
|
||||
{ label: 'UK', value: 'UK' },
|
||||
{ label: 'US', value: 'US' },
|
||||
{ label: 'UM', value: 'UM' },
|
||||
{ label: 'UY', value: 'UY' },
|
||||
{ label: 'UZ', value: 'UZ' },
|
||||
{ label: 'NH', value: 'NH' },
|
||||
{ label: 'VE', value: 'VE' },
|
||||
{ label: 'VM', value: 'VM' },
|
||||
{ label: 'VQ', value: 'VQ' },
|
||||
{ label: 'WF', value: 'WF' },
|
||||
{ label: 'WE', value: 'WE' },
|
||||
{ label: 'WI', value: 'WI' },
|
||||
{ label: 'UN', value: 'UN' },
|
||||
{ label: 'EU', value: 'EU' },
|
||||
{ label: 'ASEAN', value: 'ASEAN' },
|
||||
{ label: 'YM', value: 'YM' },
|
||||
{ label: 'ZA', value: 'ZA' },
|
||||
{ label: 'ZI', value: 'ZI' },
|
||||
{ label: 'RH', value: 'RH' },
|
||||
{ label: 'YU', value: 'YU' },
|
||||
{ label: 'UR', value: 'UR' },
|
||||
{ label: 'VO', value: 'VO' },
|
||||
{ label: 'TC', value: 'TC' },
|
||||
{ label: 'UA', value: 'UA' },
|
||||
];
|
||||
|
||||
// Form fields
|
||||
const commonName = ref('example.com');
|
||||
const country = ref('US');
|
||||
const stateOrProvince = ref('');
|
||||
const locality = ref('');
|
||||
const organization = ref('');
|
||||
const organizationalUnit = ref('');
|
||||
const email = ref('');
|
||||
|
||||
// Date range (1 year from today by default)
|
||||
const today = new Date();
|
||||
const oneYearFromNow = new Date(today);
|
||||
oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1);
|
||||
|
||||
const startDate = ref(today.toISOString().split('T')[0]);
|
||||
const endDate = ref(oneYearFromNow.toISOString().split('T')[0]);
|
||||
|
||||
// Validation
|
||||
const commonNameValidation = useValidation({
|
||||
source: commonName,
|
||||
rules: [
|
||||
{
|
||||
validator: value => value.length > 0,
|
||||
message: t('tools.x509-certificate-generator.requiredField', { field: t('tools.x509-certificate-generator.commonName') }),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const countryValidation = useValidation({
|
||||
source: country,
|
||||
rules: [
|
||||
{
|
||||
validator: value => value.length > 0,
|
||||
message: t('tools.x509-certificate-generator.requiredField', { field: t('tools.x509-certificate-generator.country') }),
|
||||
},
|
||||
{
|
||||
validator: value => value.length === 2,
|
||||
message: t('tools.x509-certificate-generator.countryMustBe2Letters'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const emailValidation = useValidation({
|
||||
source: email,
|
||||
rules: [
|
||||
{
|
||||
validator: value => !value || /^[^@\s]{1,64}@[^@\s]{1,255}\.[^@\s]{2,6}$/.test(value),
|
||||
message: t('tools.x509-certificate-generator.invalidEmailFormat'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const dateValidation = useValidation({
|
||||
source: endDate,
|
||||
rules: [
|
||||
{
|
||||
validator: value => new Date(value) > new Date(startDate.value),
|
||||
message: t('tools.x509-certificate-generator.endDateMustBeAfterStart'),
|
||||
},
|
||||
],
|
||||
watch: [startDate],
|
||||
});
|
||||
|
||||
// Generate certificate with refresh functionality
|
||||
const emptyResult = { certificate: '', privateKey: '' };
|
||||
const refreshCounter = ref(0);
|
||||
|
||||
function generateCertificate() {
|
||||
// Include refreshCounter as a dependency to force regeneration
|
||||
const _forceDependency = refreshCounter.value;
|
||||
|
||||
const isValid = commonNameValidation.isValid
|
||||
&& countryValidation.isValid
|
||||
&& emailValidation.isValid
|
||||
&& dateValidation.isValid;
|
||||
|
||||
if (!isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return withDefaultOnError(() => {
|
||||
const options: X509CertificateOptions = {
|
||||
commonName: commonName.value,
|
||||
country: country.value,
|
||||
stateOrProvince: stateOrProvince.value || undefined,
|
||||
locality: locality.value || undefined,
|
||||
organization: organization.value || undefined,
|
||||
organizationalUnit: organizationalUnit.value || undefined,
|
||||
email: email.value || undefined,
|
||||
date: [startDate.value, endDate.value],
|
||||
};
|
||||
|
||||
return generateX509(options);
|
||||
}, null);
|
||||
}
|
||||
|
||||
const certificateResult = computed(generateCertificate);
|
||||
const [refreshableResult, refresh] = computedRefreshable(() => certificateResult.value ?? emptyResult);
|
||||
|
||||
// Override refresh to increment counter
|
||||
function regenerateKeys() {
|
||||
refreshCounter.value++;
|
||||
refresh();
|
||||
}
|
||||
|
||||
// Download functionality
|
||||
function downloadCertificate() {
|
||||
if (!refreshableResult.value.certificate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([refreshableResult.value.certificate], { type: 'application/x-pem-file' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${commonName.value.replace(/[^a-z0-9]/gi, '_')}.crt`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function downloadPrivateKey() {
|
||||
if (!refreshableResult.value.privateKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([refreshableResult.value.privateKey], { type: 'application/x-pem-file' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${commonName.value.replace(/[^a-z0-9]/gi, '_')}.key`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="flex: 0 0 100%">
|
||||
<c-card>
|
||||
<div style="max-width: 600px" mx-auto>
|
||||
<n-form label-placement="left" label-width="180" label-align="right">
|
||||
<!-- Required fields -->
|
||||
<n-form-item :label="`${t('tools.x509-certificate-generator.commonName')}:*`" v-bind="commonNameValidation.attrs as any">
|
||||
<n-input v-model:value="commonName" :placeholder="t('tools.x509-certificate-generator.commonNamePlaceholder')" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item :label="`${t('tools.x509-certificate-generator.country')}:*`" v-bind="countryValidation.attrs as any">
|
||||
<n-select
|
||||
v-model:value="country"
|
||||
:options="countryCodes"
|
||||
filterable
|
||||
tag
|
||||
:placeholder="t('tools.x509-certificate-generator.countryPlaceholder')"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<!-- Optional fields -->
|
||||
<n-form-item :label="`${t('tools.x509-certificate-generator.stateOrProvince')}:`">
|
||||
<n-input v-model:value="stateOrProvince" :placeholder="t('tools.x509-certificate-generator.stateOrProvincePlaceholder')" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item :label="`${t('tools.x509-certificate-generator.locality')}:`">
|
||||
<n-input v-model:value="locality" :placeholder="t('tools.x509-certificate-generator.localityPlaceholder')" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item :label="`${t('tools.x509-certificate-generator.organization')}:`">
|
||||
<n-input v-model:value="organization" :placeholder="t('tools.x509-certificate-generator.organizationPlaceholder')" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item :label="`${t('tools.x509-certificate-generator.organizationalUnit')}:`">
|
||||
<n-input v-model:value="organizationalUnit" :placeholder="t('tools.x509-certificate-generator.organizationalUnitPlaceholder')" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item :label="`${t('tools.x509-certificate-generator.email')}:`" v-bind="emailValidation.attrs as any">
|
||||
<n-input v-model:value="email" :placeholder="t('tools.x509-certificate-generator.emailPlaceholder')" />
|
||||
</n-form-item>
|
||||
|
||||
<!-- Date range -->
|
||||
<n-form-item :label="`${t('tools.x509-certificate-generator.validFrom')}:`">
|
||||
<n-date-picker
|
||||
v-model:formatted-value="startDate"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
:placeholder="t('tools.x509-certificate-generator.validFrom')"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item :label="`${t('tools.x509-certificate-generator.validTo')}:`" v-bind="dateValidation.attrs as any">
|
||||
<n-date-picker
|
||||
v-model:formatted-value="endDate"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
:placeholder="t('tools.x509-certificate-generator.validTo')"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<!-- Refresh button -->
|
||||
<div mt-4 flex justify-center>
|
||||
<c-button @click="regenerateKeys">
|
||||
{{ t('tools.x509-certificate-generator.regenerateKeys') }}
|
||||
</c-button>
|
||||
</div>
|
||||
</div>
|
||||
</c-card>
|
||||
</div>
|
||||
|
||||
<!-- Display certificate and private key side by side -->
|
||||
<div v-if="certificateResult" flex gap-3>
|
||||
<div flex-1>
|
||||
<div mb-2 flex items-center justify-between>
|
||||
<h3>{{ t('tools.x509-certificate-generator.certificate') }}</h3>
|
||||
<c-button @click="downloadCertificate">
|
||||
{{ t('tools.x509-certificate-generator.download') }}
|
||||
</c-button>
|
||||
</div>
|
||||
<TextareaCopyable :value="refreshableResult.certificate" />
|
||||
</div>
|
||||
|
||||
<div flex-1>
|
||||
<div mb-2 flex items-center justify-between>
|
||||
<h3>{{ t('tools.x509-certificate-generator.privateKey') }}</h3>
|
||||
<c-button @click="downloadPrivateKey">
|
||||
{{ t('tools.x509-certificate-generator.download') }}
|
||||
</c-button>
|
||||
</div>
|
||||
<TextareaCopyable :value="refreshableResult.privateKey" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<c-alert v-else type="warning" mt-4>
|
||||
{{ t('tools.x509-certificate-generator.fillRequiredFields') }}
|
||||
</c-alert>
|
||||
</template>
|
||||
|
|
@ -151,7 +151,7 @@ function onSearchInput() {
|
|||
>
|
||||
<div flex-1 truncate>
|
||||
<slot name="displayed-value">
|
||||
<input v-if="searchable && isOpen" ref="searchInputRef" v-model="searchQuery" type="text" placeholder="Search..." class="search-input" w-full lh-normal color-current @input="onSearchInput">
|
||||
<input v-if="searchable && isOpen" ref="searchInputRef" v-model="searchQuery" type="text" placeholder="Search..." class="search-input" w-full color-current lh-normal @input="onSearchInput">
|
||||
<span v-else-if="selectedOption" lh-normal>
|
||||
{{ selectedOption.label }}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const headers = computed(() => {
|
|||
<template>
|
||||
<div class="relative overflow-x-auto rounded">
|
||||
<table class="w-full border-collapse text-left text-sm text-gray-500 dark:text-gray-400" role="table" :aria-label="description">
|
||||
<thead v-if="!hideHeaders" class="bg-#ffffff uppercase text-gray-700 dark:bg-#333333 dark:text-gray-400" border-b="1px solid dark:transparent #efeff5">
|
||||
<thead v-if="!hideHeaders" class="bg-#ffffff text-gray-700 uppercase dark:bg-#333333 dark:text-gray-400" border-b="1px solid dark:transparent #efeff5">
|
||||
<tr>
|
||||
<th v-for="header in headers" :key="header.key" scope="col" class="px-6 py-3 text-xs">
|
||||
{{ header.label }}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue