mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-20 06:55:06 -04:00
feat(new tool): pdf signature checker (#745)
This commit is contained in:
parent
205e360400
commit
478192065e
20 changed files with 448 additions and 21 deletions
10
components.d.ts
vendored
10
components.d.ts
vendored
|
@ -29,7 +29,11 @@ declare module '@vue/runtime-core' {
|
||||||
'CButtonsSelect.demo': typeof import('./src/ui/c-buttons-select/c-buttons-select.demo.vue')['default']
|
'CButtonsSelect.demo': typeof import('./src/ui/c-buttons-select/c-buttons-select.demo.vue')['default']
|
||||||
CCard: typeof import('./src/ui/c-card/c-card.vue')['default']
|
CCard: typeof import('./src/ui/c-card/c-card.vue')['default']
|
||||||
'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default']
|
'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default']
|
||||||
|
CCollapse: typeof import('./src/ui/c-collapse/c-collapse.vue')['default']
|
||||||
|
'CCollapse.demo': typeof import('./src/ui/c-collapse/c-collapse.demo.vue')['default']
|
||||||
CDiffEditor: typeof import('./src/ui/c-diff-editor/c-diff-editor.vue')['default']
|
CDiffEditor: typeof import('./src/ui/c-diff-editor/c-diff-editor.vue')['default']
|
||||||
|
CFileUpload: typeof import('./src/ui/c-file-upload/c-file-upload.vue')['default']
|
||||||
|
'CFileUpload.demo': typeof import('./src/ui/c-file-upload/c-file-upload.demo.vue')['default']
|
||||||
ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default']
|
ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default']
|
||||||
Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default']
|
Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default']
|
||||||
CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default']
|
CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default']
|
||||||
|
@ -41,6 +45,8 @@ declare module '@vue/runtime-core' {
|
||||||
'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default']
|
'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default']
|
||||||
CModal: typeof import('./src/ui/c-modal/c-modal.vue')['default']
|
CModal: typeof import('./src/ui/c-modal/c-modal.vue')['default']
|
||||||
'CModal.demo': typeof import('./src/ui/c-modal/c-modal.demo.vue')['default']
|
'CModal.demo': typeof import('./src/ui/c-modal/c-modal.demo.vue')['default']
|
||||||
|
CModalValue: typeof import('./src/ui/c-modal-value/c-modal-value.vue')['default']
|
||||||
|
'CModalValue.demo': typeof import('./src/ui/c-modal-value/c-modal-value.demo.vue')['default']
|
||||||
CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default']
|
CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default']
|
||||||
ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default']
|
ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default']
|
||||||
ColoredCard: typeof import('./src/components/ColoredCard.vue')['default']
|
ColoredCard: typeof import('./src/components/ColoredCard.vue')['default']
|
||||||
|
@ -83,6 +89,7 @@ declare module '@vue/runtime-core' {
|
||||||
'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
|
'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
|
||||||
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
|
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
|
||||||
IconMdiArrowDown: typeof import('~icons/mdi/arrow-down')['default']
|
IconMdiArrowDown: typeof import('~icons/mdi/arrow-down')['default']
|
||||||
|
IconMdiArrowRight: typeof import('~icons/mdi/arrow-right')['default']
|
||||||
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
|
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
|
||||||
IconMdiCamera: typeof import('~icons/mdi/camera')['default']
|
IconMdiCamera: typeof import('~icons/mdi/camera')['default']
|
||||||
IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
|
IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
|
||||||
|
@ -100,6 +107,7 @@ declare module '@vue/runtime-core' {
|
||||||
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||||
IconMdiSearch: typeof import('~icons/mdi/search')['default']
|
IconMdiSearch: typeof import('~icons/mdi/search')['default']
|
||||||
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
|
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
|
||||||
|
IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default']
|
||||||
IconMdiVideo: typeof import('~icons/mdi/video')['default']
|
IconMdiVideo: typeof import('~icons/mdi/video')['default']
|
||||||
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
|
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
|
||||||
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
|
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
|
||||||
|
@ -165,6 +173,8 @@ declare module '@vue/runtime-core' {
|
||||||
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
|
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
|
||||||
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.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']
|
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']
|
||||||
|
PdfSignatureChecker: typeof import('./src/tools/pdf-signature-checker/pdf-signature-checker.vue')['default']
|
||||||
|
PdfSignatureDetails: typeof import('./src/tools/pdf-signature-checker/components/pdf-signature-details.vue')['default']
|
||||||
PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default']
|
PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default']
|
||||||
PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default']
|
PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default']
|
||||||
QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default']
|
QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default']
|
||||||
|
|
|
@ -72,6 +72,7 @@
|
||||||
"netmask": "^2.0.2",
|
"netmask": "^2.0.2",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
"oui": "^12.0.52",
|
"oui": "^12.0.52",
|
||||||
|
"pdf-signature-reader": "^1.4.2",
|
||||||
"pinia": "^2.0.34",
|
"pinia": "^2.0.34",
|
||||||
"plausible-tracker": "^0.3.8",
|
"plausible-tracker": "^0.3.8",
|
||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
|
|
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
|
@ -116,6 +116,9 @@ dependencies:
|
||||||
oui:
|
oui:
|
||||||
specifier: ^12.0.52
|
specifier: ^12.0.52
|
||||||
version: 12.0.52
|
version: 12.0.52
|
||||||
|
pdf-signature-reader:
|
||||||
|
specifier: ^1.4.2
|
||||||
|
version: 1.4.2
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^2.0.34
|
specifier: ^2.0.34
|
||||||
version: 2.0.34(typescript@5.2.2)(vue@3.3.4)
|
version: 2.0.34(typescript@5.2.2)(vue@3.3.4)
|
||||||
|
@ -3350,7 +3353,7 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@unhead/dom': 0.5.1
|
'@unhead/dom': 0.5.1
|
||||||
'@unhead/schema': 0.5.1
|
'@unhead/schema': 0.5.1
|
||||||
'@vueuse/shared': 10.5.0(vue@3.3.4)
|
'@vueuse/shared': 10.6.0(vue@3.3.4)
|
||||||
unhead: 0.5.1
|
unhead: 0.5.1
|
||||||
vue: 3.3.4
|
vue: 3.3.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
@ -3977,8 +3980,8 @@ packages:
|
||||||
- vue
|
- vue
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@vueuse/shared@10.5.0(vue@3.3.4):
|
/@vueuse/shared@10.6.0(vue@3.3.4):
|
||||||
resolution: {integrity: sha512-18iyxbbHYLst9MqU1X1QNdMHIjks6wC7XTVf0KNOv5es/Ms6gjVFCAAWTVP2JStuGqydg3DT+ExpFORUEi9yhg==}
|
resolution: {integrity: sha512-0t4MVE18sO+/4Gh0jfeOXBTjKeV4606N9kIrDOLPjFl8Rwnlodn+QC5A4LfJuysK7aOsTMjF3KnzNeueaI0xlQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
vue-demi: 0.14.6(vue@3.3.4)
|
vue-demi: 0.14.6(vue@3.3.4)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
@ -4206,7 +4209,6 @@ packages:
|
||||||
|
|
||||||
/base64-js@1.5.1:
|
/base64-js@1.5.1:
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/bcryptjs@2.4.3:
|
/bcryptjs@2.4.3:
|
||||||
resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==}
|
resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==}
|
||||||
|
@ -5758,10 +5760,6 @@ packages:
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/function-bind@1.1.1:
|
|
||||||
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/function-bind@1.1.2:
|
/function-bind@1.1.2:
|
||||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -5989,7 +5987,7 @@ packages:
|
||||||
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
|
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
|
||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.1
|
function-bind: 1.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/hasown@2.0.0:
|
/hasown@2.0.0:
|
||||||
|
@ -6143,7 +6141,6 @@ packages:
|
||||||
|
|
||||||
/ieee754@1.2.1:
|
/ieee754@1.2.1:
|
||||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/ignore-walk@4.0.1:
|
/ignore-walk@4.0.1:
|
||||||
resolution: {integrity: sha512-rzDQLaW4jQbh2YrOFlJdCtX8qgJTehFRYiUB2r1osqTeDzV/3+Jh8fz1oAPzUThf3iku8Ds4IDqawI5d8mUiQw==}
|
resolution: {integrity: sha512-rzDQLaW4jQbh2YrOFlJdCtX8qgJTehFRYiUB2r1osqTeDzV/3+Jh8fz1oAPzUThf3iku8Ds4IDqawI5d8mUiQw==}
|
||||||
|
@ -7127,7 +7124,7 @@ packages:
|
||||||
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
|
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
hosted-git-info: 2.8.9
|
hosted-git-info: 2.8.9
|
||||||
resolve: 1.22.4
|
resolve: 1.22.8
|
||||||
semver: 5.7.2
|
semver: 5.7.2
|
||||||
validate-npm-package-license: 3.0.4
|
validate-npm-package-license: 3.0.4
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -7425,6 +7422,14 @@ packages:
|
||||||
through: 2.3.8
|
through: 2.3.8
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/pdf-signature-reader@1.4.2:
|
||||||
|
resolution: {integrity: sha512-qQbmFv6nv4SQt2gmaalaREhHu3x2XyLG2+zL4Gl4D2TL2Zfii1EKxlFhJDsduP8s06t26snDoSwEAQtJOtprmQ==}
|
||||||
|
dependencies:
|
||||||
|
base64-js: 1.5.1
|
||||||
|
ieee754: 1.2.1
|
||||||
|
node-forge: 1.3.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/perfect-debounce@1.0.0:
|
/perfect-debounce@1.0.0:
|
||||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -7918,7 +7923,7 @@ packages:
|
||||||
resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==}
|
resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
is-core-module: 2.13.0
|
is-core-module: 2.13.1
|
||||||
path-parse: 1.0.7
|
path-parse: 1.0.7
|
||||||
supports-preserve-symlinks-flag: 1.0.0
|
supports-preserve-symlinks-flag: 1.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
6
src/shims.d.ts
vendored
6
src/shims.d.ts
vendored
|
@ -32,4 +32,10 @@ declare module 'unicode-emoji-json' {
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export default emoji;
|
export default emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'pdf-signature-reader' {
|
||||||
|
const verifySignature: (pdf: ArrayBuffer) => ({signatures: SignatureInfo[]});
|
||||||
|
|
||||||
|
export default verifySignature;
|
||||||
}
|
}
|
|
@ -60,9 +60,11 @@ const ibanExamples = [
|
||||||
<div>
|
<div>
|
||||||
<c-input-text v-model:value="rawIban" placeholder="Enter an IBAN to check for validity..." test-id="iban-input" />
|
<c-input-text v-model:value="rawIban" placeholder="Enter an IBAN to check for validity..." test-id="iban-input" />
|
||||||
|
|
||||||
<c-key-value-list :items="ibanInfo" my-5 data-test-id="iban-info" />
|
<c-card v-if="ibanInfo.length > 0" mt-5>
|
||||||
|
<c-key-value-list :items="ibanInfo" data-test-id="iban-info" />
|
||||||
|
</c-card>
|
||||||
|
|
||||||
<c-card title="Valid IBAN examples">
|
<c-card title="Valid IBAN examples" mt-5>
|
||||||
<div v-for="iban in ibanExamples" :key="iban">
|
<div v-for="iban in ibanExamples" :key="iban">
|
||||||
<c-text-copyable :value="iban" font-mono :displayed-value="friendlyFormatIBAN(iban)" />
|
<c-text-copyable :value="iban" font-mono :displayed-value="friendlyFormatIBAN(iban)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { tool as base64FileConverter } from './base64-file-converter';
|
import { tool as base64FileConverter } from './base64-file-converter';
|
||||||
import { tool as base64StringConverter } from './base64-string-converter';
|
import { tool as base64StringConverter } from './base64-string-converter';
|
||||||
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
import { tool as basicAuthGenerator } from './basic-auth-generator';
|
||||||
|
import { tool as pdfSignatureChecker } from './pdf-signature-checker';
|
||||||
import { tool as numeronymGenerator } from './numeronym-generator';
|
import { tool as numeronymGenerator } from './numeronym-generator';
|
||||||
import { tool as macAddressGenerator } from './mac-address-generator';
|
import { tool as macAddressGenerator } from './mac-address-generator';
|
||||||
import { tool as textToBinary } from './text-to-binary';
|
import { tool as textToBinary } from './text-to-binary';
|
||||||
|
@ -78,7 +79,7 @@ import { tool as xmlFormatter } from './xml-formatter';
|
||||||
export const toolsByCategory: ToolCategory[] = [
|
export const toolsByCategory: ToolCategory[] = [
|
||||||
{
|
{
|
||||||
name: 'Crypto',
|
name: 'Crypto',
|
||||||
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser],
|
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser, pdfSignatureChecker],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Converter',
|
name: 'Converter',
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { SignatureInfo } from '../pdf-signature-checker.types';
|
||||||
|
|
||||||
|
const props = defineProps<{ signature: SignatureInfo }>();
|
||||||
|
const { signature } = toRefs(props);
|
||||||
|
|
||||||
|
const tableHeaders = {
|
||||||
|
validityPeriod: 'Validity period',
|
||||||
|
issuedBy: 'Issued by',
|
||||||
|
issuedTo: 'Issued to',
|
||||||
|
pemCertificate: 'PEM certificate',
|
||||||
|
};
|
||||||
|
|
||||||
|
const certs = computed(() => signature.value.meta.certs.map((certificate, index) => ({
|
||||||
|
...certificate,
|
||||||
|
validityPeriod: {
|
||||||
|
notBefore: new Date(certificate.validityPeriod.notBefore).toLocaleString(),
|
||||||
|
notAfter: new Date(certificate.validityPeriod.notAfter).toLocaleString(),
|
||||||
|
},
|
||||||
|
certificateName: `Certificate ${index + 1}`,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div flex flex-col gap-2>
|
||||||
|
<c-table :data="certs" :headers="tableHeaders">
|
||||||
|
<template #validityPeriod="{ value }">
|
||||||
|
<c-key-value-list
|
||||||
|
:items="[{
|
||||||
|
label: 'Not before',
|
||||||
|
value: value.notBefore,
|
||||||
|
}, {
|
||||||
|
label: 'Not after',
|
||||||
|
value: value.notAfter,
|
||||||
|
}]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #issuedBy="{ value }">
|
||||||
|
<c-key-value-list
|
||||||
|
:items="[{
|
||||||
|
label: 'Common name',
|
||||||
|
value: value.commonName,
|
||||||
|
}, {
|
||||||
|
label: 'Organization name',
|
||||||
|
value: value.organizationName,
|
||||||
|
}, {
|
||||||
|
label: 'Country name',
|
||||||
|
value: value.countryName,
|
||||||
|
}, {
|
||||||
|
label: 'Locality name',
|
||||||
|
value: value.localityName,
|
||||||
|
}, {
|
||||||
|
label: 'Organizational unit name',
|
||||||
|
value: value.organizationalUnitName,
|
||||||
|
}, {
|
||||||
|
label: 'State or province name',
|
||||||
|
value: value.stateOrProvinceName,
|
||||||
|
}]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #issuedTo="{ value }">
|
||||||
|
<c-key-value-list
|
||||||
|
:items="[{
|
||||||
|
label: 'Common name',
|
||||||
|
value: value.commonName,
|
||||||
|
}, {
|
||||||
|
label: 'Organization name',
|
||||||
|
value: value.organizationName,
|
||||||
|
}, {
|
||||||
|
label: 'Country name',
|
||||||
|
value: value.countryName,
|
||||||
|
}, {
|
||||||
|
label: 'Locality name',
|
||||||
|
value: value.localityName,
|
||||||
|
}, {
|
||||||
|
label: 'Organizational unit name',
|
||||||
|
value: value.organizationalUnitName,
|
||||||
|
}, {
|
||||||
|
label: 'State or province name',
|
||||||
|
value: value.stateOrProvinceName,
|
||||||
|
}]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #pemCertificate="{ value }">
|
||||||
|
<c-modal-value :value="value" label="View PEM cert">
|
||||||
|
<template #value>
|
||||||
|
<div break-all text-xs>
|
||||||
|
{{ value }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</c-modal-value>
|
||||||
|
</template>
|
||||||
|
</c-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
12
src/tools/pdf-signature-checker/index.ts
Normal file
12
src/tools/pdf-signature-checker/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { defineTool } from '../tool';
|
||||||
|
import FileCertIcon from '~icons/mdi/file-certificate-outline';
|
||||||
|
|
||||||
|
export const tool = defineTool({
|
||||||
|
name: 'PDF signature checker',
|
||||||
|
path: '/pdf-signature-checker',
|
||||||
|
description: 'Verify the signatures of a PDF file. A signed PDF file contains one or more signatures that may be used to determine whether the contents of the file have been altered since the file was signed.',
|
||||||
|
keywords: ['pdf', 'signature', 'checker', 'verify', 'validate', 'sign'],
|
||||||
|
component: () => import('./pdf-signature-checker.vue'),
|
||||||
|
icon: FileCertIcon,
|
||||||
|
createdAt: new Date('2023-12-09'),
|
||||||
|
});
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Tool - Pdf signature checker', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/pdf-signature-checker');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Has correct title', async ({ page }) => {
|
||||||
|
await expect(page).toHaveTitle('PDF signature checker - IT Tools');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,39 @@
|
||||||
|
export interface SignatureInfo {
|
||||||
|
verified: boolean
|
||||||
|
authenticity: boolean
|
||||||
|
integrity: boolean
|
||||||
|
expired: boolean
|
||||||
|
meta: {
|
||||||
|
certs: {
|
||||||
|
clientCertificate?: boolean
|
||||||
|
issuedBy: {
|
||||||
|
commonName: string
|
||||||
|
organizationalUnitName?: string
|
||||||
|
organizationName: string
|
||||||
|
countryName?: string
|
||||||
|
localityName?: string
|
||||||
|
stateOrProvinceName?: string
|
||||||
|
}
|
||||||
|
issuedTo: {
|
||||||
|
commonName: string
|
||||||
|
serialNumber?: string
|
||||||
|
organizationalUnitName?: string
|
||||||
|
organizationName: string
|
||||||
|
countryName?: string
|
||||||
|
localityName?: string
|
||||||
|
stateOrProvinceName?: string
|
||||||
|
}
|
||||||
|
validityPeriod: {
|
||||||
|
notBefore: string
|
||||||
|
notAfter: string
|
||||||
|
}
|
||||||
|
pemCertificate: string
|
||||||
|
}[]
|
||||||
|
signatureMeta: {
|
||||||
|
reason: string
|
||||||
|
contactInfo: string | null
|
||||||
|
location: string
|
||||||
|
name: string | null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
src/tools/pdf-signature-checker/pdf-signature-checker.vue
Normal file
59
src/tools/pdf-signature-checker/pdf-signature-checker.vue
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import verifyPDF from 'pdf-signature-reader';
|
||||||
|
import type { SignatureInfo } from './pdf-signature-checker.types';
|
||||||
|
import { formatBytes } from '@/utils/convert';
|
||||||
|
|
||||||
|
const signatures = ref<SignatureInfo[]>([]);
|
||||||
|
const status = ref<'idle' | 'parsed' | 'error' | 'loading'>('idle');
|
||||||
|
const file = ref<File | null>(null);
|
||||||
|
|
||||||
|
async function onVerifyClicked(uploadedFile: File) {
|
||||||
|
file.value = uploadedFile;
|
||||||
|
const fileBuffer = await uploadedFile.arrayBuffer();
|
||||||
|
|
||||||
|
status.value = 'loading';
|
||||||
|
try {
|
||||||
|
const { signatures: parsedSignatures } = verifyPDF(fileBuffer);
|
||||||
|
signatures.value = parsedSignatures;
|
||||||
|
status.value = 'parsed';
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
signatures.value = [];
|
||||||
|
status.value = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="flex: 0 0 100%">
|
||||||
|
<div mx-auto max-w-600px>
|
||||||
|
<c-file-upload title="Drag and drop a PDF file here, or click to select a file" accept=".pdf" @file-upload="onVerifyClicked" />
|
||||||
|
|
||||||
|
<c-card v-if="file" mt-4 flex gap-2>
|
||||||
|
<div font-bold>
|
||||||
|
{{ file.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{ formatBytes(file.size) }}
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<div v-if="status === 'error'">
|
||||||
|
<c-alert mt-4>
|
||||||
|
No signatures found in the provided file.
|
||||||
|
</c-alert>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="status === 'parsed' && signatures.length" style="flex: 0 0 100%" mt-5 flex flex-col gap-4>
|
||||||
|
<div v-for="(signature, index) of signatures" :key="index">
|
||||||
|
<div mb-2 font-bold>
|
||||||
|
Signature {{ index + 1 }} certificates :
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pdf-signature-details :signature="signature" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
5
src/ui/c-collapse/c-collapse.demo.vue
Normal file
5
src/ui/c-collapse/c-collapse.demo.vue
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<c-collapse title="Collapse title">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquet iaculis class cubilia metus per nullam gravida ad venenatis. Id elementum elementum enim orci elementum justo facilisi habitant consequat. Justo eget ligula purus laoreet penatibus eros quisque fusce sociis. In eget amet sagittis dignissim eleifend proin lacinia potenti tellus. Interdum vulputate condimentum molestie pulvinar praesent accumsan quisque venenatis imperdiet.
|
||||||
|
</c-collapse>
|
||||||
|
</template>
|
25
src/ui/c-collapse/c-collapse.vue
Normal file
25
src/ui/c-collapse/c-collapse.vue
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const props = withDefaults(defineProps<{ title?: string }>(), { title: '' });
|
||||||
|
const { title } = toRefs(props);
|
||||||
|
|
||||||
|
const isCollapsed = ref(true);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div flex cursor-pointer items-center @click="isCollapsed = !isCollapsed">
|
||||||
|
<icon-mdi-triangle-down :class="{ 'transform-rotate--90': isCollapsed }" op-50 transition />
|
||||||
|
|
||||||
|
<slot name="title">
|
||||||
|
<span class="ml-2" font-bold>{{ title }}</span>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-show="!isCollapsed"
|
||||||
|
mt-2
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
3
src/ui/c-file-upload/c-file-upload.demo.vue
Normal file
3
src/ui/c-file-upload/c-file-upload.demo.vue
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<c-file-upload />
|
||||||
|
</template>
|
95
src/ui/c-file-upload/c-file-upload.vue
Normal file
95
src/ui/c-file-upload/c-file-upload.vue
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
multiple?: boolean
|
||||||
|
accept?: string
|
||||||
|
title?: string
|
||||||
|
}>(), {
|
||||||
|
multiple: false,
|
||||||
|
accept: undefined,
|
||||||
|
title: 'Drag and drop files here, or click to select files',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'filesUpload', files: File[]): void
|
||||||
|
(event: 'fileUpload', file: File): void
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { multiple } = toRefs(props);
|
||||||
|
|
||||||
|
const isOverDropZone = ref(false);
|
||||||
|
|
||||||
|
const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
function triggerFileInput() {
|
||||||
|
fileInput.value?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileInput(event: Event) {
|
||||||
|
const files = (event.target as HTMLInputElement).files;
|
||||||
|
|
||||||
|
handleUpload(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(event: DragEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
const files = event.dataTransfer?.files;
|
||||||
|
|
||||||
|
handleUpload(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpload(files: FileList | null | undefined) {
|
||||||
|
if (_.isNil(files) || _.isEmpty(files)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (multiple.value) {
|
||||||
|
emit('filesUpload', Array.from(files));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('fileUpload', files[0]);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col cursor-pointer items-center justify-center border-2px border-gray-300 border-opacity-50 rounded-lg border-dashed p-8 transition-colors"
|
||||||
|
:class="{
|
||||||
|
'border-primary border-opacity-100': isOverDropZone,
|
||||||
|
}"
|
||||||
|
@click="triggerFileInput"
|
||||||
|
@drop.prevent="handleDrop"
|
||||||
|
@dragover.prevent
|
||||||
|
@dragenter="isOverDropZone = true"
|
||||||
|
@dragleave="isOverDropZone = false"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
class="hidden"
|
||||||
|
:multiple="multiple"
|
||||||
|
:accept="accept"
|
||||||
|
@change="handleFileInput"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<span op-70>
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- separator -->
|
||||||
|
<div my-4 w-full flex items-center justify-center op-70>
|
||||||
|
<div class="h-1px max-w-100px flex-1 bg-gray-300 op-50" />
|
||||||
|
<div class="mx-2 text-gray-400">
|
||||||
|
or
|
||||||
|
</div>
|
||||||
|
<div class="h-1px max-w-100px flex-1 bg-gray-300 op-50" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<c-button>
|
||||||
|
Browse files
|
||||||
|
</c-button>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -9,13 +9,13 @@ const formattedItems = computed(() => items.value.filter(item => !_.isNil(item.v
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div my-5>
|
<div flex flex-col gap-2>
|
||||||
<div v-for="item in formattedItems" :key="item.label" flex gap-2 py-1 class="c-key-value-list__item">
|
<div v-for="item in formattedItems" :key="item.label" class="c-key-value-list__item">
|
||||||
<div flex-basis-180px text-right font-bold class="c-key-value-list__key">
|
<div class="c-key-value-list__key" text-13px lh-normal>
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<c-key-value-list-item :item="item" class="c-key-value-list__value" />
|
<c-key-value-list-item :item="item" class="c-key-value-list__value" font-bold lh-normal />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
21
src/ui/c-modal-value/c-modal-value.demo.vue
Normal file
21
src/ui/c-modal-value/c-modal-value.demo.vue
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<div flex gap-2>
|
||||||
|
<c-modal-value value="lorem ipsum" label="test" />
|
||||||
|
<c-modal-value>
|
||||||
|
<template #label="{ toggleModal }">
|
||||||
|
<c-button class="text-left" size="small" @click="toggleModal">
|
||||||
|
Bonjour
|
||||||
|
</c-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #value>
|
||||||
|
<pre>
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit.
|
||||||
|
Molestias, quisquam vitae saepe dolores quas debitis ab r
|
||||||
|
ecusandae suscipit ex dignissimos minus quam repellat sunt.
|
||||||
|
Molestiae culpa blanditiis totam sapiente dignissimos.
|
||||||
|
</pre>
|
||||||
|
</template>
|
||||||
|
</c-modal-value>
|
||||||
|
</div>
|
||||||
|
</template>
|
31
src/ui/c-modal-value/c-modal-value.vue
Normal file
31
src/ui/c-modal-value/c-modal-value.vue
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useCopy } from '@/composable/copy';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ value: string; label?: string; copyable?: boolean }>(), { label: undefined, copyable: true });
|
||||||
|
const { value, label } = toRefs(props);
|
||||||
|
|
||||||
|
const { copy, isJustCopied } = useCopy({ source: value });
|
||||||
|
|
||||||
|
const isModalOpen = ref(false);
|
||||||
|
const toggleModal = useToggle(isModalOpen);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<slot name="label" :value="value" :toggle-modal="toggleModal" :is-modal-open="isModalOpen">
|
||||||
|
<c-button class="text-left" @click="isModalOpen = true">
|
||||||
|
{{ label }}
|
||||||
|
</c-button>
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<c-modal v-model:open="isModalOpen">
|
||||||
|
<slot name="value" :value="value" :toggle-modal="toggleModal" :is-modal-open="isModalOpen">
|
||||||
|
{{ value }}
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<div mt-4 flex justify-center>
|
||||||
|
<c-button class="w-full" @click="copy">
|
||||||
|
{{ isJustCopied ? 'Copied!' : 'Copy' }}
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</c-modal>
|
||||||
|
</template>
|
|
@ -39,7 +39,7 @@ const headers = computed(() => {
|
||||||
<template>
|
<template>
|
||||||
<div class="relative overflow-x-auto rounded">
|
<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">
|
<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">
|
<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">
|
||||||
<tr>
|
<tr>
|
||||||
<th v-for="header in headers" :key="header.key" scope="col" class="px-6 py-3 text-xs">
|
<th v-for="header in headers" :key="header.key" scope="col" class="px-6 py-3 text-xs">
|
||||||
{{ header.label }}
|
{{ header.label }}
|
||||||
|
@ -54,7 +54,7 @@ const headers = computed(() => {
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<td v-for="header in headers" :key="header.key" class="px-6 py-4">
|
<td v-for="header in headers" :key="header.key" class="px-6 py-4">
|
||||||
<slot :name="header" :row="row" :headers="headers" :value="row[header.key]">
|
<slot :name="header.key" :row="row" :headers="headers" :value="row[header.key]">
|
||||||
{{ row[header.key] }}
|
{{ row[header.key] }}
|
||||||
</slot>
|
</slot>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -15,11 +15,13 @@ export default defineConfig({
|
||||||
theme: {
|
theme: {
|
||||||
colors: {
|
colors: {
|
||||||
primary: '#1ea54c',
|
primary: '#1ea54c',
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shortcuts: {
|
shortcuts: {
|
||||||
'pretty-scrollbar': 'scrollbar scrollbar-rounded scrollbar-thumb-color-gray-300 scrollbar-track-color-gray-100 dark:scrollbar-thumb-color-#424242 dark:scrollbar-track-color-#686868',
|
'pretty-scrollbar': 'scrollbar scrollbar-rounded scrollbar-thumb-color-gray-300 scrollbar-track-color-gray-100 dark:scrollbar-thumb-color-#424242 dark:scrollbar-track-color-#686868',
|
||||||
'divider': 'h-1px bg-current op-10',
|
'divider': 'h-1px bg-current op-10',
|
||||||
'bg-surface': 'bg-#ffffff dark:bg-#232323',
|
'bg-surface': 'bg-#ffffff dark:bg-#232323',
|
||||||
|
'bg-background': 'bg-#f1f5f9 dark:bg-#1c1c1c',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue