1668: JWT parser value editor. Add the ability to edit parsed values and regenerate the token. Add an option to view parsed token as json. signature validator and generator added.
This commit is contained in:
parent
0de73e8971
commit
0f95d9bfa8
5
components.d.ts
vendored
5
components.d.ts
vendored
@ -130,18 +130,17 @@ declare module '@vue/runtime-core' {
|
|||||||
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.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']
|
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
|
||||||
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
|
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
|
||||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
|
||||||
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
||||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
NDivider: typeof import('naive-ui')['NDivider']
|
|
||||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||||
NH1: typeof import('naive-ui')['NH1']
|
NH1: typeof import('naive-ui')['NH1']
|
||||||
NH3: typeof import('naive-ui')['NH3']
|
NH3: typeof import('naive-ui')['NH3']
|
||||||
NIcon: typeof import('naive-ui')['NIcon']
|
NIcon: typeof import('naive-ui')['NIcon']
|
||||||
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
NLayout: typeof import('naive-ui')['NLayout']
|
NLayout: typeof import('naive-ui')['NLayout']
|
||||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||||
NMenu: typeof import('naive-ui')['NMenu']
|
NMenu: typeof import('naive-ui')['NMenu']
|
||||||
NSpace: typeof import('naive-ui')['NSpace']
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
NTable: typeof import('naive-ui')['NTable']
|
NTable: typeof import('naive-ui')['NTable']
|
||||||
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
|
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']
|
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { HmacMD5, HmacSHA1, HmacSHA224, HmacSHA256, HmacSHA384, HmacSHA512, enc } from 'crypto-js';
|
||||||
|
import type { lib } from 'crypto-js';
|
||||||
import { decodeJwt } from './jwt-parser.service';
|
import { decodeJwt } from './jwt-parser.service';
|
||||||
import { useValidation } from '@/composable/validation';
|
import { useValidation } from '@/composable/validation';
|
||||||
import { isNotThrowing } from '@/utils/boolean';
|
import { isNotThrowing } from '@/utils/boolean';
|
||||||
@ -12,10 +14,356 @@ const decodedJWT = computed(() =>
|
|||||||
withDefaultOnError(() => decodeJwt({ jwt: rawJwt.value }), { header: [], payload: [] }),
|
withDefaultOnError(() => decodeJwt({ jwt: rawJwt.value }), { header: [], payload: [] }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const showAsJson = ref(false);
|
||||||
|
const signatureSecret = ref('');
|
||||||
|
|
||||||
|
// Create editable versions of header and payload
|
||||||
|
const editableHeader = ref<Record<string, any>>({});
|
||||||
|
const editablePayload = ref<Record<string, any>>({});
|
||||||
|
|
||||||
|
// Editable JSON string
|
||||||
|
const editableJson = ref('');
|
||||||
|
|
||||||
|
// Track the original key being edited (stable reference)
|
||||||
|
const editingKeys = ref<Record<string, string>>({});
|
||||||
|
|
||||||
|
// Map of supported HMAC algorithms
|
||||||
|
const hmacAlgorithms: Record<string, (message: string, key: string) => lib.WordArray> = {
|
||||||
|
HS256: HmacSHA256,
|
||||||
|
HS384: HmacSHA384,
|
||||||
|
HS512: HmacSHA512,
|
||||||
|
HS224: HmacSHA224,
|
||||||
|
HS1: HmacSHA1,
|
||||||
|
MD5: HmacMD5,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recommended minimum secret lengths for each algorithm (in bytes)
|
||||||
|
const minimumSecretLengths: Record<string, number> = {
|
||||||
|
HS256: 32, // 256 bits
|
||||||
|
HS384: 48, // 384 bits
|
||||||
|
HS512: 64, // 512 bits
|
||||||
|
HS224: 28, // 224 bits
|
||||||
|
HS1: 20, // 160 bits
|
||||||
|
MD5: 16, // 128 bits
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the algorithm from the header
|
||||||
|
const algorithm = computed(() => {
|
||||||
|
return editableHeader.value.alg || 'HS256';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if the algorithm is supported
|
||||||
|
const isAlgorithmSupported = computed(() => {
|
||||||
|
return algorithm.value in hmacAlgorithms;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate secret length in bytes
|
||||||
|
const secretLength = computed(() => {
|
||||||
|
if (!signatureSecret.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return new TextEncoder().encode(signatureSecret.value).byteLength;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get minimum required length for current algorithm
|
||||||
|
const minimumSecretLength = computed(() => {
|
||||||
|
return minimumSecretLengths[algorithm.value] || 32;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if secret meets minimum length requirement
|
||||||
|
const isSecretLengthValid = computed(() => {
|
||||||
|
if (!signatureSecret.value || !isAlgorithmSupported.value) {
|
||||||
|
return true; // Don't show error if no secret or unsupported algorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
return secretLength.value >= minimumSecretLength.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the required secret length message
|
||||||
|
const secretLengthMessage = computed(() => {
|
||||||
|
if (!signatureSecret.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAlgorithmSupported.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${secretLength.value}/${minimumSecretLength.value} bytes`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validation for signature secret
|
||||||
|
const secretValidation = useValidation({
|
||||||
|
source: signatureSecret,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
validator: () => {
|
||||||
|
if (!signatureSecret.value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return isSecretLengthValid.value;
|
||||||
|
},
|
||||||
|
message: computed(() => {
|
||||||
|
if (!isAlgorithmSupported.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return `Secret must be at least ${minimumSecretLength.value} bytes for ${algorithm.value}`;
|
||||||
|
}).value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
watch: [algorithm],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract and display signature
|
||||||
|
const currentSignature = computed(() => {
|
||||||
|
const parts = rawJwt.value.split('.');
|
||||||
|
return parts[2] || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate the expected signature based on secret and algorithm
|
||||||
|
const expectedSignature = computed(() => {
|
||||||
|
if (!signatureSecret.value || !isSecretLengthValid.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAlgorithmSupported.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parts = rawJwt.value.split('.');
|
||||||
|
const headerPayload = `${parts[0]}.${parts[1]}`;
|
||||||
|
|
||||||
|
// Sign using the algorithm specified in the header
|
||||||
|
const hmacFunction = hmacAlgorithms[algorithm.value];
|
||||||
|
const signature = hmacFunction(headerPayload, signatureSecret.value);
|
||||||
|
|
||||||
|
// Convert to base64url (JWT standard)
|
||||||
|
return signature.toString(enc.Base64).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Failed to calculate signature:', error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if the signature is valid
|
||||||
|
const isSignatureValid = computed(() => {
|
||||||
|
if (!signatureSecret.value || !currentSignature.value || !isSecretLengthValid.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAlgorithmSupported.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentSignature.value === expectedSignature.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to update the signature with the expected one
|
||||||
|
function updateSignature() {
|
||||||
|
if (!expectedSignature.value || !isSecretLengthValid.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = rawJwt.value.split('.');
|
||||||
|
rawJwt.value = `${parts[0]}.${parts[1]}.${expectedSignature.value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync decoded values to editable state when JWT changes
|
||||||
|
watch(
|
||||||
|
decodedJWT,
|
||||||
|
(decoded) => {
|
||||||
|
// Only update if we're not actively editing keys
|
||||||
|
const hasEditingKeys = Object.keys(editingKeys.value).length > 0;
|
||||||
|
|
||||||
|
if (!hasEditingKeys) {
|
||||||
|
editableHeader.value = decoded.header.reduce(
|
||||||
|
(acc, { claim, value }) => {
|
||||||
|
acc[claim] = value;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, any>,
|
||||||
|
);
|
||||||
|
|
||||||
|
editablePayload.value = decoded.payload.reduce(
|
||||||
|
(acc, { claim, value }) => {
|
||||||
|
acc[claim] = value;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, any>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update JSON representation
|
||||||
|
updateJsonFromObjects();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Computed table rows with stable keys
|
||||||
|
const tableRows = computed(() => {
|
||||||
|
return {
|
||||||
|
header: Object.keys(editableHeader.value).map((key, index) => {
|
||||||
|
const original = decodedJWT.value.header.find(item => item.claim === key);
|
||||||
|
const stableId = `header-${index}`;
|
||||||
|
return {
|
||||||
|
stableId,
|
||||||
|
claim: key,
|
||||||
|
value: editableHeader.value[key],
|
||||||
|
claimDescription: original?.claimDescription,
|
||||||
|
friendlyValue: original?.friendlyValue,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
payload: Object.keys(editablePayload.value).map((key, index) => {
|
||||||
|
const original = decodedJWT.value.payload.find(item => item.claim === key);
|
||||||
|
const stableId = `payload-${index}`;
|
||||||
|
return {
|
||||||
|
stableId,
|
||||||
|
claim: key,
|
||||||
|
value: editablePayload.value[key],
|
||||||
|
claimDescription: original?.claimDescription,
|
||||||
|
friendlyValue: original?.friendlyValue,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update JSON string from objects
|
||||||
|
function updateJsonFromObjects() {
|
||||||
|
editableJson.value = JSON.stringify(
|
||||||
|
{
|
||||||
|
header: editableHeader.value,
|
||||||
|
payload: editablePayload.value,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update objects from JSON string
|
||||||
|
function updateObjectsFromJson() {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(editableJson.value);
|
||||||
|
|
||||||
|
if (parsed.header && typeof parsed.header === 'object') {
|
||||||
|
editableHeader.value = parsed.header;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.payload && typeof parsed.payload === 'object') {
|
||||||
|
editablePayload.value = parsed.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateJWT();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
// Invalid JSON, don't update
|
||||||
|
console.error('Invalid JSON:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation for JSON
|
||||||
|
const jsonValidation = useValidation({
|
||||||
|
source: editableJson,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
validator: (value) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
return parsed.header && parsed.payload;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message: 'Invalid JSON format. Must contain "header" and "payload" properties.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to re-encode JWT when values are edited (without updating signature)
|
||||||
|
function updateJWT() {
|
||||||
|
try {
|
||||||
|
const headerBase64 = toBase64Url(JSON.stringify(editableHeader.value))
|
||||||
|
.replace(/=/g, '')
|
||||||
|
.replace(/\+/g, '-')
|
||||||
|
.replace(/\//g, '_');
|
||||||
|
const payloadBase64 = toBase64Url(JSON.stringify(editablePayload.value))
|
||||||
|
.replace(/=/g, '')
|
||||||
|
.replace(/\+/g, '-')
|
||||||
|
.replace(/\//g, '_');
|
||||||
|
|
||||||
|
// Keep the existing signature
|
||||||
|
const signature = currentSignature.value;
|
||||||
|
|
||||||
|
rawJwt.value = `${headerBase64}.${payloadBase64}.${signature}`;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Failed to encode JWT:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBase64Url(input: string): string {
|
||||||
|
const bytes = new TextEncoder().encode(input);
|
||||||
|
let binary = '';
|
||||||
|
bytes.forEach(b => (binary += String.fromCharCode(b)));
|
||||||
|
return btoa(binary).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start editing a key
|
||||||
|
function startEditingKey(stableId: string, currentKey: string) {
|
||||||
|
editingKeys.value[stableId] = currentKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update key while editing
|
||||||
|
function updateEditingKey(section: 'header' | 'payload', stableId: string, newValue: string) {
|
||||||
|
const originalKey = editingKeys.value[stableId];
|
||||||
|
if (!originalKey || !newValue.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = section === 'header' ? editableHeader.value : editablePayload.value;
|
||||||
|
|
||||||
|
// Create a new object with the updated key
|
||||||
|
const newData: Record<string, any> = {};
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
if (key === originalKey) {
|
||||||
|
newData[newValue] = data[key];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
newData[key] = data[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (section === 'header') {
|
||||||
|
editableHeader.value = newData;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
editablePayload.value = newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the original key reference
|
||||||
|
editingKeys.value[stableId] = newValue;
|
||||||
|
|
||||||
|
updateJWT();
|
||||||
|
updateJsonFromObjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop editing a key
|
||||||
|
function stopEditingKey(stableId: string) {
|
||||||
|
delete editingKeys.value[stableId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle table value changes
|
||||||
|
function handleTableValueChange() {
|
||||||
|
updateJWT();
|
||||||
|
updateJsonFromObjects();
|
||||||
|
}
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
{ key: 'header', title: 'Header' },
|
{ key: 'header' as const, title: 'Header', editableData: editableHeader },
|
||||||
{ key: 'payload', title: 'Payload' },
|
{ key: 'payload' as const, title: 'Payload', editableData: editablePayload },
|
||||||
] as const;
|
];
|
||||||
|
|
||||||
const validation = useValidation({
|
const validation = useValidation({
|
||||||
source: rawJwt,
|
source: rawJwt,
|
||||||
@ -30,28 +378,131 @@ const validation = useValidation({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<c-card>
|
<c-card>
|
||||||
<c-input-text v-model:value="rawJwt" label="JWT to decode" :validation="validation" placeholder="Put your token here..." rows="5" multiline raw-text autofocus mb-3 />
|
<c-input-text
|
||||||
|
v-model:value="rawJwt"
|
||||||
|
label="JWT to decode"
|
||||||
|
:validation="validation"
|
||||||
|
placeholder="Put your token here..."
|
||||||
|
rows="5"
|
||||||
|
multiline
|
||||||
|
raw-text
|
||||||
|
autofocus
|
||||||
|
mb-3
|
||||||
|
/>
|
||||||
|
|
||||||
<n-table v-if="validation.isValid">
|
<c-input-text
|
||||||
|
v-model:value="signatureSecret"
|
||||||
|
:label="signatureSecret ? `Signature secret (optional) - ${secretLengthMessage}` : 'Signature secret (optional)'"
|
||||||
|
:validation="secretValidation"
|
||||||
|
placeholder="Enter secret to verify/sign JWT..."
|
||||||
|
raw-text
|
||||||
|
clearable
|
||||||
|
mb-3
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="validation.isValid" class="mb-3">
|
||||||
|
<div class="mb-2 flex items-center gap-2">
|
||||||
|
<span class="font-bold">Algorithm:</span>
|
||||||
|
<span class="text-sm font-mono">{{ algorithm }}</span>
|
||||||
|
<span v-if="!isAlgorithmSupported" class="text-sm text-orange-600 dark:text-orange-400">
|
||||||
|
(Unsupported - only HMAC algorithms are supported)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2 flex items-center gap-2">
|
||||||
|
<span class="font-bold">Signature:</span>
|
||||||
|
<span class="break-all text-sm font-mono">{{ currentSignature || '(no signature)' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-bold">Signature Status:</span>
|
||||||
|
<!-- Secret too short -->
|
||||||
|
<span v-if="signatureSecret && !isSecretLengthValid" class="text-red-600 dark:text-red-400"> ✗ Secret too short </span>
|
||||||
|
<!-- No signature but has valid secret -->
|
||||||
|
<div v-else-if="!currentSignature && signatureSecret && isSecretLengthValid" class="flex items-center gap-2">
|
||||||
|
<span class="text-yellow-600 dark:text-yellow-400"> ⚠ Unverified (no signature) </span>
|
||||||
|
<c-button size="small" @click="updateSignature">
|
||||||
|
Add Signature
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
<!-- Has signature but no secret -->
|
||||||
|
<span v-else-if="!signatureSecret && currentSignature" class="text-yellow-600 dark:text-yellow-400">
|
||||||
|
⚠ Unverified (enter secret to verify)
|
||||||
|
</span>
|
||||||
|
<!-- Valid signature -->
|
||||||
|
<div v-else-if="isSignatureValid === true" class="flex items-center gap-2">
|
||||||
|
<span class="text-green-600 dark:text-green-400"> ✓ Valid </span>
|
||||||
|
</div>
|
||||||
|
<!-- Invalid signature -->
|
||||||
|
<div v-else-if="isSignatureValid === false" class="flex items-center gap-2">
|
||||||
|
<span class="text-red-600 dark:text-red-400"> ✗ Invalid </span>
|
||||||
|
<c-button size="small" @click="updateSignature">
|
||||||
|
Update Signature
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
<!-- No signature and no secret -->
|
||||||
|
<span v-else-if="!currentSignature" class="op-50"> No signature </span>
|
||||||
|
<!-- Other cases -->
|
||||||
|
<span v-else class="op-50">
|
||||||
|
{{ isAlgorithmSupported ? 'Enter secret to verify' : 'Algorithm not supported' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<hr style="height: 1px; border: 0; border-top: 1px solid #353535">
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div v-if="validation.isValid" class="mb-3 flex items-center gap-2">
|
||||||
|
<span>Display as JSON:</span>
|
||||||
|
<n-switch v-model:value="showAsJson" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<c-input-text
|
||||||
|
v-if="validation.isValid && showAsJson"
|
||||||
|
v-model:value="editableJson"
|
||||||
|
:validation="jsonValidation"
|
||||||
|
label="Decoded JWT (JSON)"
|
||||||
|
placeholder="Edit JSON here..."
|
||||||
|
rows="15"
|
||||||
|
multiline
|
||||||
|
raw-text
|
||||||
|
@update:value="updateObjectsFromJson"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<n-table v-if="validation.isValid && !showAsJson">
|
||||||
<tbody>
|
<tbody>
|
||||||
<template v-for="section of sections" :key="section.key">
|
<template v-for="section of sections" :key="section.key">
|
||||||
<th colspan="2" class="table-header">
|
<th colspan="2" class="table-header">
|
||||||
{{ section.title }}
|
{{ section.title }}
|
||||||
</th>
|
</th>
|
||||||
<tr v-for="{ claim, claimDescription, friendlyValue, value } in decodedJWT[section.key]" :key="claim + value">
|
<tr v-for="row in tableRows[section.key]" :key="row.stableId">
|
||||||
<td class="claims" style="vertical-align: top;">
|
<td class="claims" style="vertical-align: top; width: 30%">
|
||||||
<span font-bold>
|
<div>
|
||||||
{{ claim }}
|
<n-input
|
||||||
</span>
|
:value="row.claim"
|
||||||
<span v-if="claimDescription" ml-2 op-70>
|
size="small"
|
||||||
({{ claimDescription }})
|
style="font-weight: bold"
|
||||||
</span>
|
@focus="() => startEditingKey(row.stableId, row.claim)"
|
||||||
|
@input="(value) => updateEditingKey(section.key, row.stableId, value)"
|
||||||
|
@blur="() => stopEditingKey(row.stableId)"
|
||||||
|
/>
|
||||||
|
<div v-if="row.claimDescription" class="ml-2 mt-1 text-sm op-70">
|
||||||
|
({{ row.claimDescription }})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style="word-wrap: break-word;word-break: break-all;">
|
<td style="word-wrap: break-word; word-break: break-all; vertical-align: top">
|
||||||
<span>{{ value }}</span>
|
<div>
|
||||||
<span v-if="friendlyValue" ml-2 op-70>
|
<n-input
|
||||||
({{ friendlyValue }})
|
v-model:value="section.editableData.value[row.claim]"
|
||||||
</span>
|
size="small"
|
||||||
|
@input="handleTableValueChange"
|
||||||
|
/>
|
||||||
|
<div v-if="row.friendlyValue" class="ml-2 mt-1 text-sm op-70">
|
||||||
|
({{ row.friendlyValue }})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user