mirror of
https://github.com/Luzifer/ots.git
synced 2025-08-11 15:40:28 -04:00
parent
a098395daf
commit
c5124731f5
13 changed files with 330 additions and 31 deletions
|
@ -29,7 +29,7 @@
|
|||
class="row"
|
||||
@submit.prevent="createSecret"
|
||||
>
|
||||
<div class="col-12 mb-3 order-0">
|
||||
<div class="col-12 mb-3">
|
||||
<label for="createSecretData">{{ $t('label-secret-data') }}</label>
|
||||
<textarea
|
||||
id="createSecretData"
|
||||
|
@ -38,13 +38,43 @@
|
|||
rows="5"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="!$root.customize.disableFileAttachment"
|
||||
class="col-12 mb-3"
|
||||
>
|
||||
<label for="createSecretFiles">{{ $t('label-secret-files') }}</label>
|
||||
<input
|
||||
id="createSecretFiles"
|
||||
ref="createSecretFiles"
|
||||
class="form-control"
|
||||
type="file"
|
||||
multiple
|
||||
:accept="$root.customize.acceptedFileTypes"
|
||||
@change="updateFileSize"
|
||||
>
|
||||
<div class="form-text">
|
||||
{{ $t('text-max-filesize', { maxSize: bytesToHuman(maxFileSize) }) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="maxFileSizeExceeded"
|
||||
class="alert alert-danger"
|
||||
>
|
||||
{{ $t('text-max-filesize-exceeded', { curSize: bytesToHuman(fileSize), maxSize: bytesToHuman(maxFileSize) }) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12 order-2 order-md-1">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-success"
|
||||
:disabled="secret.trim().length < 1"
|
||||
:disabled="secret.trim().length < 1 || maxFileSizeExceeded || createRunning"
|
||||
>
|
||||
{{ $t('btn-create-secret') }}
|
||||
<template v-if="!createRunning">
|
||||
{{ $t('btn-create-secret') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="fa-solid fa-spinner fa-spin-pulse" />
|
||||
{{ $t('btn-create-secret-processing') }}
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
|
@ -80,6 +110,8 @@
|
|||
/* global maxSecretExpire */
|
||||
|
||||
import appCrypto from '../crypto.js'
|
||||
import { bytesToHuman } from '../helpers'
|
||||
import OTSMeta from '../ots-meta'
|
||||
|
||||
const defaultExpiryChoices = [
|
||||
90 * 86400, // 90 days
|
||||
|
@ -94,6 +126,17 @@ const defaultExpiryChoices = [
|
|||
5 * 60, // 5 minutes
|
||||
]
|
||||
|
||||
/*
|
||||
* We define an internal max file-size which cannot get exceeded even
|
||||
* though the server might accept more: at around 70 MiB the base64
|
||||
* encoding broke and nothing works anymore. This might be fixed by
|
||||
* changing how the base64 implementation works (maybe use a WASM
|
||||
* object?) or switching to a browser-native implementation in case
|
||||
* that will appear somewhen in the future but for now we just "fix"
|
||||
* the issue by disallowing bigger files.
|
||||
*/
|
||||
const internalMaxFileSize = 64 * 1024 * 1024 // 64 MiB
|
||||
|
||||
const passwordCharset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
const passwordLength = 20
|
||||
|
||||
|
@ -122,6 +165,14 @@ export default {
|
|||
|
||||
return choices
|
||||
},
|
||||
|
||||
maxFileSize() {
|
||||
return this.$root.customize.maxAttachmentSizeTotal === 0 ? internalMaxFileSize : Math.min(internalMaxFileSize, this.$root.customize.maxAttachmentSizeTotal)
|
||||
},
|
||||
|
||||
maxFileSizeExceeded() {
|
||||
return this.fileSize > this.maxFileSize
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
|
@ -131,6 +182,8 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
canWrite: null,
|
||||
createRunning: false,
|
||||
fileSize: 0,
|
||||
secret: '',
|
||||
securePassword: null,
|
||||
selectedExpiry: null,
|
||||
|
@ -138,6 +191,8 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
bytesToHuman,
|
||||
|
||||
checkWriteAccess() {
|
||||
fetch('api/isWritable', {
|
||||
credentials: 'same-origin',
|
||||
|
@ -157,14 +212,28 @@ export default {
|
|||
|
||||
// createSecret executes the secret creation after encrypting the secret
|
||||
createSecret() {
|
||||
if (this.secret.trim().length < 1) {
|
||||
if (this.secret.trim().length < 1 || this.maxFileSizeExceeded) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Encoding large files takes a while, prevent duplicate click on "create"
|
||||
this.createRunning = true
|
||||
|
||||
this.securePassword = [...window.crypto.getRandomValues(new Uint8Array(passwordLength))]
|
||||
.map(n => passwordCharset[n % passwordCharset.length])
|
||||
.join('')
|
||||
appCrypto.enc(this.secret, this.securePassword)
|
||||
|
||||
const meta = new OTSMeta()
|
||||
meta.secret = this.secret
|
||||
|
||||
if (this.$refs.createSecretFiles) {
|
||||
for (const f of [...this.$refs.createSecretFiles.files]) {
|
||||
meta.files.push(f)
|
||||
}
|
||||
}
|
||||
|
||||
meta.serialize()
|
||||
.then(secret => appCrypto.enc(secret, this.securePassword))
|
||||
.then(secret => {
|
||||
let reqURL = 'api/create'
|
||||
if (this.selectedExpiry !== null) {
|
||||
|
@ -205,6 +274,15 @@ export default {
|
|||
|
||||
return false
|
||||
},
|
||||
|
||||
updateFileSize() {
|
||||
let cumSize = 0
|
||||
for (const f of [...this.$refs.createSecretFiles.files]) {
|
||||
cumSize += f.size
|
||||
}
|
||||
|
||||
this.fileSize = cumSize
|
||||
},
|
||||
},
|
||||
|
||||
name: 'AppCreate',
|
||||
|
|
|
@ -10,9 +10,16 @@
|
|||
<p v-html="$t('text-pre-reveal-hint')" />
|
||||
<button
|
||||
class="btn btn-success"
|
||||
:disabled="secretLoading"
|
||||
@click="requestSecret"
|
||||
>
|
||||
{{ $t('btn-reveal-secret') }}
|
||||
<template v-if="!secretLoading">
|
||||
{{ $t('btn-reveal-secret') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="fa-solid fa-spinner fa-spin-pulse" />
|
||||
{{ $t('btn-reveal-secret-processing') }}
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
@ -41,6 +48,22 @@
|
|||
</div>
|
||||
</div>
|
||||
<p v-html="$t('text-hint-burned')" />
|
||||
<template v-if="files.length > 0">
|
||||
<p v-html="$t('text-attached-files')" />
|
||||
<ul>
|
||||
<li
|
||||
v-for="file in files"
|
||||
:key="file.name"
|
||||
class="font-monospace"
|
||||
>
|
||||
<a
|
||||
:href="file.url"
|
||||
:download="file.name"
|
||||
>{{ file.name }}</a>
|
||||
({{ bytesToHuman(file.size) }})
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -49,21 +72,28 @@
|
|||
import appClipboardButton from './clipboard-button.vue'
|
||||
import appCrypto from '../crypto.js'
|
||||
import appQrButton from './qr-button.vue'
|
||||
import { bytesToHuman } from '../helpers'
|
||||
import OTSMeta from '../ots-meta'
|
||||
|
||||
export default {
|
||||
components: { appClipboardButton, appQrButton },
|
||||
|
||||
data() {
|
||||
return {
|
||||
files: [],
|
||||
popover: null,
|
||||
secret: null,
|
||||
secretContentBlobURL: null,
|
||||
secretLoading: false,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
bytesToHuman,
|
||||
|
||||
// requestSecret requests the encrypted secret from the backend
|
||||
requestSecret() {
|
||||
this.secretLoading = true
|
||||
window.history.replaceState({}, '', window.location.href.split('#')[0])
|
||||
fetch(`api/get/${this.secretId}`)
|
||||
.then(resp => {
|
||||
|
@ -89,11 +119,19 @@ export default {
|
|||
|
||||
appCrypto.dec(secret, this.securePassword)
|
||||
.then(secret => {
|
||||
this.secret = secret
|
||||
})
|
||||
.catch(() => {
|
||||
this.$emit('error', this.$t('alert-something-went-wrong'))
|
||||
const meta = new OTSMeta(secret)
|
||||
this.secret = meta.secret
|
||||
|
||||
meta.files.forEach(file => {
|
||||
file.arrayBuffer()
|
||||
.then(ab => {
|
||||
const blobURL = window.URL.createObjectURL(new Blob([ab], { type: file.type }))
|
||||
this.files.push({ name: file.name, size: ab.byteLength, url: blobURL })
|
||||
})
|
||||
})
|
||||
this.secretLoading = false
|
||||
})
|
||||
.catch(() => this.$emit('error', this.$t('alert-something-went-wrong')))
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue