Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
295 changes: 217 additions & 78 deletions src/views/ReadCertificate/CertificateContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,71 +3,121 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<table v-if="Object.keys(certificate).length">
<tr>
<th colspan="2">
{{ t('libresign', 'Owner of certificate') }}
</th>
</tr>
<tr v-for="(value, customName) in orderList(certificate.subject)" :key="customName">
<td>{{ getLabelFromId(customName) }}</td>
<td>{{ Array.isArray(value) ? value.join(', ') : value }}</td>
</tr>
<tr v-if="index !== 0">
<th colspan="2">
{{ t('libresign', 'Issuer of certificate') }}
</th>
</tr>
<tr v-for="(value, customName) in orderList(certificate.issuer)" :key="value">
<td>{{ getLabelFromId(customName) }}</td>
<td>{{ value }}</td>
</tr>
<tr v-if="certificate.extracerts">
<th colspan="2">
{{ t('libresign', 'Certificate chain:') }}
</th>
</tr>
<tr v-for="(extra, key) in certificate.extracerts"
:key="`extracerts-${key}`"
class="certificate-chain">
<td>
{{ key }}
</td>
<td>
<CertificateContent :certificate="extra" :index="index + '_' + key" />
</td>
</tr>
<tr>
<td>{{ t('libresign', 'Certificate valid from:') }}</td>
<td>{{ certificate.valid_from }}</td>
</tr>
<tr>
<td>{{ t('libresign', 'Certificate valid to:') }}</td>
<td>{{ certificate.valid_to }}</td>
</tr>
<tr>
<th colspan="2">
{{ t('libresign', 'Extra information') }}
</th>
</tr>
<tr>
<td>Name</td>
<td>{{ certificate.name }}</td>
</tr>
<tr v-for="(value, name) in certificate.extensions" :key="name">
<td>{{ name }}</td>
<td>{{ value }}</td>
</tr>
</table>
<div v-if="Object.keys(certificate).length" class="certificate-content">
<NcSettingsSection :name="t('libresign', 'Owner of certificate')">
<div class="certificate-fields">
<div v-for="(value, customName) in orderList(certificate.subject)"
:key="customName"
class="certificate-field">
<span class="field-label">{{ getLabelFromId(customName) }}</span>
<span class="field-value">{{ Array.isArray(value) ? value.join(', ') : value }}</span>
</div>
</div>
</NcSettingsSection>

<NcSettingsSection v-if="index !== 0"
:name="t('libresign', 'Issuer of certificate')">
<div class="certificate-fields">
<div v-for="(value, customName) in orderList(certificate.issuer)"
:key="value"
class="certificate-field">
<span class="field-label">{{ getLabelFromId(customName) }}</span>
<span class="field-value">{{ value }}</span>
</div>
</div>
</NcSettingsSection>

<NcSettingsSection v-if="certificate.extracerts && index === '0'"
:name="t('libresign', 'Certificate chain')">
<div class="certificate-chain-container">
<div v-for="(extra, key) in certificate.extracerts"
:key="`extracerts-${key}`"
class="chain-certificate">
<h4 class="chain-certificate-title">
{{ getChainCertificateLabel(key, extra) }}
</h4>
<CertificateContent :certificate="extra" :index="index + '_' + key" />
</div>
</div>
</NcSettingsSection>

<NcSettingsSection :name="t('libresign', 'Certificate Information')">
<div class="certificate-fields">
<div v-if="index === '0'" class="certificate-field">
<span class="field-label">{{ t('libresign', 'Valid from') }}</span>
<span class="field-value">{{ certificate.valid_from }}</span>
</div>
<div v-if="index === '0'" class="certificate-field">
<span class="field-label">{{ t('libresign', 'Valid to') }}</span>
<span class="field-value">{{ certificate.valid_to }}</span>
</div>
<div v-if="certificate.version !== undefined" class="certificate-field">
<span class="field-label">{{ t('libresign', 'Version') }}</span>
<span class="field-value">{{ certificate.version + 1 }}</span>
</div>
<div v-if="certificate.hash" class="certificate-field">
<span class="field-label">{{ t('libresign', 'Fingerprint') }}</span>
<span class="field-value">{{ certificate.hash }}</span>
</div>
<div v-if="certificate.signatureTypeLN" class="certificate-field">
<span class="field-label">{{ t('libresign', 'Signature algorithm') }}</span>
<span class="field-value">{{ certificate.signatureTypeLN }}</span>
</div>
<div v-if="certificate.serialNumber" class="certificate-field">
<span class="field-label">{{ t('libresign', 'Serial number') }}</span>
<span class="field-value">{{ certificate.serialNumber }}</span>
</div>
</div>
</NcSettingsSection>

<NcSettingsSection v-if="index === '0'"
:name="t('libresign', 'Technical Details')">
<div class="certificate-fields">
<div class="certificate-field">
<span class="field-label">Name</span>
<span class="field-value">{{ certificate.name }}</span>
</div>
<div v-for="(value, name) in certificate.extensions"
:key="name"
class="certificate-field">
<span class="field-label">{{ camelCaseToTitleCase(name) }}</span>
<span class="field-value">{{ value }}</span>
</div>
</div>
</NcSettingsSection>

<NcSettingsSection v-if="shouldShowPurposes"
:name="t('libresign', 'Certificate purposes')">
<div class="purposes-grid">
<NcNoteCard v-for="(purpose, purposeIndex) in certificate.purposes"
:key="purposeIndex"
:type="purpose[0] ? 'success' : 'error'"
:heading="formatPurposeName(purpose[2])">
<div class="purpose-status">
<span v-if="purpose[0]">{{ t('libresign', 'Allowed') }}</span>
<span v-else>{{ t('libresign', 'Not allowed') }}</span>
<NcChip v-if="purpose[1]" no-close>CA</NcChip>
</div>
</NcNoteCard>
</div>
</NcSettingsSection>
</div>
</template>

<script>

// import CertificateContent from './CertificateContent.vue'
import { selectCustonOption } from '../../helpers/certification.js'
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import NcChip from '@nextcloud/vue/dist/Components/NcChip.js'

export default {
name: 'CertificateContent',
components: {
NcSettingsSection,
NcNoteCard,
NcChip,
},
props: {
certificate: {
type: Object,
Expand All @@ -79,6 +129,16 @@ export default {
default: '0',
},
},
data() {
return {}
},
computed: {
shouldShowPurposes() {
return this.certificate.purposes &&
Object.keys(this.certificate.purposes).length &&
this.index === '0'
},
},
methods: {
orderList(data) {
const sorted = {};
Expand All @@ -95,41 +155,120 @@ export default {
return sorted
},
getLabelFromId(id) {
try {
const item = selectCustonOption(id).unwrap()
return item.label
} catch (error) {
return id
const option = selectCustonOption(id)
if (option.isSome()) {
return this.camelCaseToTitleCase(option.unwrap().label)
}
return this.camelCaseToTitleCase(id)
},
camelCaseToTitleCase(text) {
if (text.includes(' ')) {
return text.replace(/^./, str => str.toUpperCase())
}

return text
// Handle acronyms (consecutive uppercase letters)
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
// Add space before uppercase letters that follow lowercase
.replace(/([a-z])([A-Z])/g, '$1 $2')
// Capitalize first letter
.replace(/^./, str => str.toUpperCase())
.trim()
},
formatPurposeName(purpose) {
const purposeNames = {
'sslclient': this.t('libresign', 'SSL Client'),
'sslserver': this.t('libresign', 'SSL Server'),
'nssslserver': this.t('libresign', 'Netscape SSL Server'),
'smimesign': this.t('libresign', 'S/MIME Signing'),
'smimeencrypt': this.t('libresign', 'S/MIME Encryption'),
'crlsign': this.t('libresign', 'CRL Signing'),
'any': this.t('libresign', 'Any Purpose'),
'ocsphelper': this.t('libresign', 'OCSP Helper'),
'timestampsign': this.t('libresign', 'Timestamp Signing'),
'codesign': this.t('libresign', 'Code Signing'),
}
return purposeNames[purpose] || purpose
},
getChainCertificateLabel(index, certificate) {
if (index === 0) {
return this.t('libresign', 'Intermediate Certificate')
}
if (certificate.subject && certificate.issuer &&
JSON.stringify(certificate.subject) === JSON.stringify(certificate.issuer)) {
return this.t('libresign', 'Root Certificate (CA)')
}
return this.t('libresign', 'Certificate {number}', { number: index + 1 })
},
},
}
</script>

<style lang="scss" scoped>
table {
width: 100%;
white-space: unset;
}
$border: 1px solid var(--color-border-dark);
$desktop: 768px;

td {
padding: 5px;
border-bottom: 1px solid var(--color-border);
.certificate-field {
display: flex;
flex-direction: column;
padding: 6px 0;
border-bottom: $border;
gap: 4px;

&:last-child { border-bottom: none; }

@media (min-width: $desktop) {
flex-direction: row;
gap: 16px;
}
}

td:nth-child(2) {
word-break: break-all;
.field {
&-label {
color: var(--color-text-maxcontrast);
align-self: flex-start;

@media (min-width: $desktop) {
min-width: 140px;
max-width: 140px;
text-align: right;
padding-right: 16px;
border-right: $border;
word-wrap: break-word;
}
}

&-value {
word-break: break-all;
align-self: flex-start;
}
}

th {
font-weight: bold;
.chain-certificate {
background: var(--color-background-hover);
border-radius: var(--border-radius);
margin-bottom: 16px;

&-title {
background: var(--color-background-dark);
padding: 12px 16px;
font-weight: 600;
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
}

tr:last-child td {
border-bottom: none;
.purposes {
&-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 12px;

:deep(.notecard__heading) { font-size: unset; }
}
}

td:first-child, th:first-child {
opacity: .5;
word-break: normal;
.purpose-status {
display: flex;
gap: 8px;
}
</style>
Loading