2019-05-10 21:12:00 +00:00
|
|
|
<template>
|
|
|
|
<div id="app">
|
2019-07-14 16:00:47 +00:00
|
|
|
<b-navbar
|
|
|
|
toggleable="lg"
|
|
|
|
type="dark"
|
|
|
|
variant="primary"
|
|
|
|
>
|
|
|
|
<b-navbar-brand
|
|
|
|
href="#"
|
|
|
|
@click="newSecret"
|
|
|
|
>
|
2023-06-10 16:21:38 +00:00
|
|
|
<i
|
|
|
|
v-if="!customize.appIcon"
|
|
|
|
class="fas fa-user-secret mr-1"
|
|
|
|
/>
|
|
|
|
<img
|
|
|
|
v-else
|
|
|
|
class="mr-1"
|
|
|
|
:src="customize.appIcon"
|
|
|
|
>
|
|
|
|
<span v-if="!customize.disableAppTitle">{{ customize.appTitle || 'OTS - One Time Secrets' }}</span>
|
2019-05-10 21:12:00 +00:00
|
|
|
</b-navbar-brand>
|
|
|
|
|
2019-07-14 16:00:47 +00:00
|
|
|
<b-navbar-toggle target="nav-collapse" />
|
2019-05-10 21:12:00 +00:00
|
|
|
|
2019-07-14 16:00:47 +00:00
|
|
|
<b-collapse
|
|
|
|
id="nav-collapse"
|
|
|
|
is-nav
|
|
|
|
>
|
2019-05-10 21:12:00 +00:00
|
|
|
<b-navbar-nav class="ml-auto">
|
2019-07-14 19:21:18 +00:00
|
|
|
<b-nav-item @click="explanationShown = !explanationShown">
|
|
|
|
<i class="fas fa-question" /> {{ $t('btn-show-explanation') }}
|
|
|
|
</b-nav-item>
|
2019-07-14 16:00:47 +00:00
|
|
|
<b-nav-item @click="newSecret">
|
|
|
|
<i class="fas fa-plus" /> {{ $t('btn-new-secret') }}
|
|
|
|
</b-nav-item>
|
2023-06-10 16:21:38 +00:00
|
|
|
<b-nav-form
|
|
|
|
v-if="!customize.disableThemeSwitcher"
|
|
|
|
class="ml-2"
|
|
|
|
>
|
2021-09-16 17:24:36 +00:00
|
|
|
<b-form-checkbox
|
|
|
|
v-model="darkTheme"
|
|
|
|
switch
|
|
|
|
>
|
|
|
|
<i class="fas fa-moon" />​
|
|
|
|
</b-form-checkbox>
|
|
|
|
</b-nav-form>
|
2019-05-10 21:12:00 +00:00
|
|
|
</b-navbar-nav>
|
|
|
|
</b-collapse>
|
|
|
|
</b-navbar>
|
|
|
|
|
|
|
|
<b-container class="mt-4">
|
|
|
|
<b-row class="justify-content-center">
|
|
|
|
<b-col md="8">
|
2019-07-14 16:00:47 +00:00
|
|
|
<b-alert
|
|
|
|
v-model="showError"
|
|
|
|
variant="danger"
|
|
|
|
dismissible
|
|
|
|
v-html="error"
|
|
|
|
/>
|
2019-05-10 21:12:00 +00:00
|
|
|
</b-col>
|
|
|
|
</b-row>
|
|
|
|
|
|
|
|
<b-row>
|
|
|
|
<b-col>
|
2019-07-14 19:21:18 +00:00
|
|
|
<!-- Explanation -->
|
|
|
|
<b-card
|
|
|
|
v-if="explanationShown"
|
|
|
|
class="mb-3"
|
|
|
|
border-variant="primary"
|
|
|
|
header-bg-variant="primary"
|
|
|
|
header-text-variant="white"
|
|
|
|
>
|
|
|
|
<span
|
|
|
|
slot="header"
|
|
|
|
v-html="$t('title-explanation')"
|
|
|
|
/>
|
|
|
|
<ul>
|
|
|
|
<li v-for="explanation in $t('items-explanation')">
|
|
|
|
{{ explanation }}
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
</b-card>
|
|
|
|
|
|
|
|
<!-- Creation dialog -->
|
2019-05-10 21:12:00 +00:00
|
|
|
<b-card
|
2023-06-15 16:49:10 +00:00
|
|
|
v-if="mode == 'create' && !secretId && canWrite"
|
2019-05-10 21:12:00 +00:00
|
|
|
border-variant="primary"
|
|
|
|
header-bg-variant="primary"
|
|
|
|
header-text-variant="white"
|
2019-07-14 16:00:47 +00:00
|
|
|
>
|
|
|
|
<span
|
|
|
|
slot="header"
|
|
|
|
v-html="$t('title-new-secret')"
|
|
|
|
/>
|
2019-05-10 21:12:00 +00:00
|
|
|
<b-form-group :label="$t('label-secret-data')">
|
|
|
|
<b-form-textarea
|
|
|
|
id="secret"
|
2019-07-14 16:00:47 +00:00
|
|
|
v-model="secret"
|
2019-05-10 21:12:00 +00:00
|
|
|
max-rows="25"
|
|
|
|
rows="5"
|
2019-07-14 16:00:47 +00:00
|
|
|
/>
|
2019-05-10 21:12:00 +00:00
|
|
|
</b-form-group>
|
2019-07-14 16:00:47 +00:00
|
|
|
<b-button
|
2023-06-09 20:21:40 +00:00
|
|
|
:disabled="secret.trim().length < 1"
|
2019-07-14 16:00:47 +00:00
|
|
|
variant="success"
|
|
|
|
@click="createSecret"
|
|
|
|
>
|
|
|
|
{{ $t('btn-create-secret') }}
|
|
|
|
</b-button>
|
2019-05-10 21:12:00 +00:00
|
|
|
</b-card>
|
|
|
|
|
2023-06-15 16:49:10 +00:00
|
|
|
<!-- Creation disabled -->
|
|
|
|
<b-card
|
|
|
|
v-if="mode == 'create' && !secretId && canWrite === false"
|
|
|
|
border-variant="info"
|
|
|
|
header-bg-variant="info"
|
|
|
|
header-text-variant="white"
|
|
|
|
>
|
|
|
|
<span
|
|
|
|
slot="header"
|
|
|
|
v-html="$t('title-secret-create-disabled')"
|
|
|
|
/>
|
|
|
|
<p v-html="$t('text-secret-create-disabled')" />
|
|
|
|
</b-card>
|
|
|
|
|
2019-07-14 19:21:18 +00:00
|
|
|
<!-- Secret created, show secret URL -->
|
2019-05-10 21:12:00 +00:00
|
|
|
<b-card
|
2019-07-14 16:00:47 +00:00
|
|
|
v-if="mode == 'create' && secretId"
|
2019-05-10 21:12:00 +00:00
|
|
|
border-variant="success"
|
|
|
|
header-bg-variant="success"
|
|
|
|
header-text-variant="white"
|
2019-07-14 16:00:47 +00:00
|
|
|
>
|
|
|
|
<span
|
|
|
|
slot="header"
|
|
|
|
v-html="$t('title-secret-created')"
|
|
|
|
/>
|
|
|
|
<p v-html="$t('text-pre-url')" />
|
2019-05-10 21:12:00 +00:00
|
|
|
<b-form-group>
|
2023-06-10 18:24:17 +00:00
|
|
|
<b-input-group>
|
|
|
|
<b-form-input
|
|
|
|
ref="secretUrl"
|
|
|
|
:value="secretUrl"
|
|
|
|
readonly
|
|
|
|
@focus="$refs.secretUrl.select()"
|
|
|
|
/>
|
2023-06-14 20:49:21 +00:00
|
|
|
<b-input-group-append>
|
2023-06-10 18:24:17 +00:00
|
|
|
<b-button
|
2023-06-14 20:49:21 +00:00
|
|
|
v-if="hasClipboard"
|
|
|
|
:disabled="!secretUrl"
|
|
|
|
:variant="copyToClipboardSuccess ? 'success' : 'primary'"
|
|
|
|
@click="copySecretUrl"
|
|
|
|
>
|
|
|
|
<i class="fas fa-clipboard" />
|
|
|
|
</b-button>
|
|
|
|
<b-button
|
|
|
|
v-if="!customize.disableQRSupport"
|
2023-06-10 18:24:17 +00:00
|
|
|
id="secret-url-qrcode"
|
|
|
|
:disabled="!secretQRDataURL"
|
2023-06-14 20:49:21 +00:00
|
|
|
variant="secondary"
|
2023-06-10 18:24:17 +00:00
|
|
|
>
|
|
|
|
<i class="fas fa-qrcode" />
|
|
|
|
</b-button>
|
|
|
|
</b-input-group-append>
|
|
|
|
</b-input-group>
|
2019-05-10 21:12:00 +00:00
|
|
|
</b-form-group>
|
2019-07-14 16:00:47 +00:00
|
|
|
<p v-html="$t('text-burn-hint')" />
|
2023-06-10 18:24:17 +00:00
|
|
|
|
|
|
|
<b-popover
|
|
|
|
v-id="!customize.disableQRSupport"
|
|
|
|
target="secret-url-qrcode"
|
|
|
|
triggers="focus"
|
|
|
|
placement="leftbottom"
|
|
|
|
>
|
|
|
|
<b-img
|
|
|
|
height="200"
|
|
|
|
:src="secretQRDataURL"
|
|
|
|
width="200"
|
|
|
|
/>
|
|
|
|
</b-popover>
|
2019-05-10 21:12:00 +00:00
|
|
|
</b-card>
|
|
|
|
|
2023-06-10 18:24:17 +00:00
|
|
|
|
2019-07-14 19:21:18 +00:00
|
|
|
<!-- Display secret -->
|
2019-05-10 21:12:00 +00:00
|
|
|
<b-card
|
2019-07-14 16:00:47 +00:00
|
|
|
v-if="mode == 'view'"
|
2019-05-10 21:12:00 +00:00
|
|
|
border-variant="primary"
|
|
|
|
header-bg-variant="primary"
|
|
|
|
header-text-variant="white"
|
2019-07-14 16:00:47 +00:00
|
|
|
>
|
|
|
|
<span
|
|
|
|
slot="header"
|
|
|
|
v-html="$t('title-reading-secret')"
|
|
|
|
/>
|
2019-05-10 21:12:00 +00:00
|
|
|
<template v-if="!secret">
|
2019-07-14 16:00:47 +00:00
|
|
|
<p v-html="$t('text-pre-reveal-hint')" />
|
|
|
|
<b-button
|
|
|
|
variant="success"
|
|
|
|
@click="requestSecret"
|
|
|
|
>
|
|
|
|
{{ $t('btn-reveal-secret') }}
|
|
|
|
</b-button>
|
2019-05-10 21:12:00 +00:00
|
|
|
</template>
|
|
|
|
<template v-else>
|
|
|
|
<b-form-group>
|
|
|
|
<b-form-textarea
|
|
|
|
max-rows="25"
|
|
|
|
readonly
|
|
|
|
rows="5"
|
|
|
|
:value="secret"
|
2019-07-14 16:00:47 +00:00
|
|
|
/>
|
2019-05-10 21:12:00 +00:00
|
|
|
</b-form-group>
|
2019-07-14 16:00:47 +00:00
|
|
|
<p v-html="$t('text-hint-burned')" />
|
2019-05-10 21:12:00 +00:00
|
|
|
</template>
|
|
|
|
</b-card>
|
|
|
|
</b-col>
|
|
|
|
</b-row>
|
|
|
|
|
2023-06-10 16:21:38 +00:00
|
|
|
<b-row
|
|
|
|
v-if="!customize.disablePoweredBy"
|
|
|
|
class="mt-5"
|
|
|
|
>
|
2019-05-10 21:12:00 +00:00
|
|
|
<b-col class="footer">
|
2023-06-09 20:31:24 +00:00
|
|
|
{{ $t('text-powered-by') }} <a href="https://github.com/Luzifer/ots"><i class="fab fa-github" /> OTS</a> {{ $root.version }}
|
2019-05-10 21:12:00 +00:00
|
|
|
</b-col>
|
|
|
|
</b-row>
|
|
|
|
</b-container>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
2023-04-14 11:06:14 +00:00
|
|
|
import crypto from './crypto.js'
|
2023-06-10 18:24:17 +00:00
|
|
|
import qrcode from 'qrcode'
|
2019-05-10 21:12:00 +00:00
|
|
|
|
2022-08-25 22:41:55 +00:00
|
|
|
const passwordCharset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
|
|
const passwordLength = 20
|
|
|
|
|
2019-05-10 21:12:00 +00:00
|
|
|
export default {
|
2019-07-14 16:00:47 +00:00
|
|
|
computed: {
|
2023-06-14 20:49:21 +00:00
|
|
|
hasClipboard() {
|
|
|
|
return Boolean(navigator.clipboard && navigator.clipboard.writeText)
|
|
|
|
},
|
|
|
|
|
2019-07-14 16:00:47 +00:00
|
|
|
secretUrl() {
|
2021-09-06 20:31:25 +00:00
|
|
|
return [
|
|
|
|
window.location.href,
|
|
|
|
encodeURIComponent([
|
|
|
|
this.secretId,
|
|
|
|
this.securePassword,
|
|
|
|
].join('|')),
|
|
|
|
].join('#')
|
2019-07-14 16:00:47 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
|
2023-06-10 18:50:53 +00:00
|
|
|
data() {
|
|
|
|
return {
|
2023-06-15 16:49:10 +00:00
|
|
|
canWrite: null,
|
2023-06-14 20:49:21 +00:00
|
|
|
copyToClipboardSuccess: false,
|
2023-06-10 18:50:53 +00:00
|
|
|
customize: {},
|
|
|
|
darkTheme: false,
|
|
|
|
error: '',
|
|
|
|
explanationShown: false,
|
|
|
|
mode: 'create',
|
|
|
|
secret: '',
|
|
|
|
secretId: '',
|
|
|
|
secretQRDataURL: '',
|
|
|
|
securePassword: '',
|
|
|
|
showError: false,
|
|
|
|
}
|
2019-07-14 16:00:47 +00:00
|
|
|
},
|
|
|
|
|
2019-05-10 21:12:00 +00:00
|
|
|
methods: {
|
2023-06-15 16:49:10 +00:00
|
|
|
checkWriteAccess() {
|
|
|
|
fetch('api/isWritable', {
|
|
|
|
credentials: 'same-origin',
|
|
|
|
method: 'GET',
|
|
|
|
redirect: 'error',
|
|
|
|
})
|
|
|
|
.then(resp => {
|
|
|
|
if (resp.status !== 204) {
|
|
|
|
throw new Error(`unexpected status: ${resp.status}`)
|
|
|
|
}
|
|
|
|
this.canWrite = true
|
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
this.canWrite = false
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2023-06-14 20:49:21 +00:00
|
|
|
copySecretUrl() {
|
|
|
|
navigator.clipboard.writeText(this.secretUrl)
|
|
|
|
.then(() => {
|
|
|
|
this.copyToClipboardSuccess = true
|
|
|
|
window.setTimeout(() => {
|
|
|
|
this.copyToClipboardSuccess = false
|
|
|
|
}, 500)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-05-10 21:12:00 +00:00
|
|
|
// createSecret executes the secret creation after encrypting the secret
|
|
|
|
createSecret() {
|
2023-06-09 20:21:40 +00:00
|
|
|
if (this.secret.trim().length < 1) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-08-25 22:41:55 +00:00
|
|
|
this.securePassword = [...window.crypto.getRandomValues(new Uint8Array(passwordLength))]
|
|
|
|
.map(n => passwordCharset[n % passwordCharset.length])
|
|
|
|
.join('')
|
2023-04-14 11:06:14 +00:00
|
|
|
crypto.enc(this.secret, this.securePassword)
|
2023-06-10 12:50:21 +00:00
|
|
|
.then(secret => fetch('api/create', {
|
|
|
|
body: JSON.stringify({ secret }),
|
|
|
|
headers: {
|
|
|
|
'content-type': 'application/json',
|
|
|
|
},
|
|
|
|
method: 'POST',
|
|
|
|
})
|
2023-04-14 11:06:14 +00:00
|
|
|
.then(resp => {
|
2023-06-14 13:05:21 +00:00
|
|
|
if (resp.status !== 201) {
|
|
|
|
// Server says "no"
|
|
|
|
this.error = this.$t('alert-something-went-wrong')
|
|
|
|
this.showError = true
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
resp.json()
|
|
|
|
.then(data => {
|
|
|
|
this.secretId = data.secret_id
|
|
|
|
this.secret = ''
|
2019-05-10 21:12:00 +00:00
|
|
|
|
2023-06-14 13:05:21 +00:00
|
|
|
// Give the interface a moment to transistion and focus
|
|
|
|
window.setTimeout(() => this.$refs.secretUrl.focus(), 100)
|
|
|
|
})
|
2023-04-14 11:06:14 +00:00
|
|
|
})
|
|
|
|
.catch(err => {
|
2023-06-14 13:05:21 +00:00
|
|
|
// Network error
|
2023-06-14 08:57:14 +00:00
|
|
|
this.error = this.$t('alert-something-went-wrong')
|
|
|
|
this.showError = true
|
2023-04-14 11:06:14 +00:00
|
|
|
}))
|
2019-05-10 21:12:00 +00:00
|
|
|
|
|
|
|
return false
|
|
|
|
},
|
|
|
|
|
|
|
|
// hashLoad reacts on a changed window hash an starts the diplaying of the secret
|
|
|
|
hashLoad() {
|
2019-05-12 22:12:11 +00:00
|
|
|
const hash = decodeURIComponent(window.location.hash)
|
2019-05-10 22:31:26 +00:00
|
|
|
if (hash.length === 0) {
|
|
|
|
return
|
|
|
|
}
|
2019-05-10 21:12:00 +00:00
|
|
|
|
|
|
|
const parts = hash.substring(1).split('|')
|
2019-05-10 22:31:26 +00:00
|
|
|
if (parts.length === 2) {
|
2019-05-10 21:12:00 +00:00
|
|
|
this.securePassword = parts[1]
|
|
|
|
}
|
|
|
|
this.secretId = parts[0]
|
|
|
|
this.mode = 'view'
|
|
|
|
},
|
|
|
|
|
|
|
|
// newSecret removes the window hash and therefore returns to "new secret" mode
|
|
|
|
newSecret() {
|
|
|
|
location.href = location.href.split('#')[0]
|
|
|
|
},
|
|
|
|
|
|
|
|
// requestSecret requests the encrypted secret from the backend
|
|
|
|
requestSecret() {
|
2023-06-10 12:50:21 +00:00
|
|
|
fetch(`api/get/${this.secretId}`)
|
2019-05-10 21:12:00 +00:00
|
|
|
.then(resp => {
|
2023-06-14 13:05:21 +00:00
|
|
|
if (resp.status === 404) {
|
|
|
|
// Secret has already been consumed
|
|
|
|
this.error = this.$t('alert-secret-not-found')
|
|
|
|
this.showError = true
|
2023-04-14 11:06:14 +00:00
|
|
|
return
|
2019-05-10 21:12:00 +00:00
|
|
|
}
|
2023-04-14 11:06:14 +00:00
|
|
|
|
2023-06-14 13:05:21 +00:00
|
|
|
if (resp.status !== 200) {
|
|
|
|
// Some other non-200: Something(tm) was wrong
|
2019-05-10 22:31:26 +00:00
|
|
|
this.error = this.$t('alert-something-went-wrong')
|
|
|
|
this.showError = true
|
2023-06-14 13:05:21 +00:00
|
|
|
return
|
2019-05-10 21:12:00 +00:00
|
|
|
}
|
2023-06-14 13:05:21 +00:00
|
|
|
|
|
|
|
resp.json()
|
|
|
|
.then(data => {
|
|
|
|
const secret = data.secret
|
|
|
|
if (!this.securePassword) {
|
|
|
|
this.secret = secret
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
crypto.dec(secret, this.securePassword)
|
|
|
|
.then(secret => {
|
|
|
|
this.secret = secret
|
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
this.error = this.$t('alert-something-went-wrong')
|
|
|
|
this.showError = true
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.catch(err => {
|
|
|
|
// Network error
|
|
|
|
this.error = this.$t('alert-something-went-wrong')
|
|
|
|
this.showError = true
|
2019-05-10 21:12:00 +00:00
|
|
|
})
|
|
|
|
},
|
2023-06-10 18:50:53 +00:00
|
|
|
},
|
2019-05-10 21:12:00 +00:00
|
|
|
|
2023-06-10 18:50:53 +00:00
|
|
|
// Trigger initialization functions
|
|
|
|
mounted() {
|
2023-06-15 16:49:10 +00:00
|
|
|
this.checkWriteAccess()
|
2023-06-10 18:50:53 +00:00
|
|
|
this.customize = window.OTSCustomize
|
|
|
|
this.darkTheme = window.getTheme() === 'dark'
|
|
|
|
window.onhashchange = this.hashLoad
|
|
|
|
this.hashLoad()
|
2019-05-10 21:12:00 +00:00
|
|
|
},
|
2023-06-10 18:24:17 +00:00
|
|
|
|
2023-06-10 18:50:53 +00:00
|
|
|
name: 'App',
|
|
|
|
|
2023-06-10 18:24:17 +00:00
|
|
|
watch: {
|
2023-06-10 18:50:53 +00:00
|
|
|
darkTheme(to) {
|
|
|
|
window.setTheme(to ? 'dark' : 'light')
|
|
|
|
},
|
|
|
|
|
2023-06-10 18:24:17 +00:00
|
|
|
secretUrl(to) {
|
|
|
|
if (this.customize.disableQRSupport) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-06-10 18:50:53 +00:00
|
|
|
qrcode.toDataURL(to, { width: 200 })
|
2023-06-10 18:24:17 +00:00
|
|
|
.then(url => {
|
|
|
|
this.secretQRDataURL = url
|
|
|
|
})
|
|
|
|
},
|
|
|
|
},
|
2019-05-10 21:12:00 +00:00
|
|
|
}
|
|
|
|
</script>
|