feat: allow to password protect shares (#1252)
This changes allows to password protect shares. It works by: * Allowing to optionally pass a password when creating a share * If set, the password + salt that is configured via a new flag will be hashed via bcrypt and the hash stored together with the rest of the share * Additionally, a random 96 byte long token gets generated and stored as part of the share * When the backend retrieves an unauthenticated request for a share that has authentication configured, it will return a http 401 * The frontend detects this and will show a login prompt * The actual download links are protected via an url arg that contains the previously generated token. This allows us to avoid buffering the download in the browser and allows pasting the link without breaking it
This commit is contained in:
@@ -77,8 +77,13 @@ export function download (format, ...files) {
|
||||
if (format !== null) {
|
||||
url += `algo=${format}&`
|
||||
}
|
||||
if (store.state.jwt !== ''){
|
||||
url += `auth=${store.state.jwt}&`
|
||||
}
|
||||
if (store.state.token !== ''){
|
||||
url += `token=${store.state.token}`
|
||||
}
|
||||
|
||||
url += `auth=${store.state.jwt}`
|
||||
window.open(url)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ export async function list() {
|
||||
return fetchJSON('/api/shares')
|
||||
}
|
||||
|
||||
export async function getHash(hash) {
|
||||
return fetchJSON(`/api/public/share/${hash}`)
|
||||
export async function getHash(hash, password = "") {
|
||||
return fetchJSON(`/api/public/share/${hash}`, {
|
||||
headers: {'X-SHARE-PASSWORD': password},
|
||||
})
|
||||
}
|
||||
|
||||
export async function get(url) {
|
||||
@@ -23,14 +25,18 @@ export async function remove(hash) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(url, expires = '', unit = 'hours') {
|
||||
export async function create(url, password = '', expires = '', unit = 'hours') {
|
||||
url = removePrefix(url)
|
||||
url = `/api/share${url}`
|
||||
if (expires !== '') {
|
||||
url += `?expires=${expires}&unit=${unit}`
|
||||
}
|
||||
|
||||
let body = '{}';
|
||||
if (password != '' || expires !== '' || unit !== 'hours') {
|
||||
body = JSON.stringify({password: password, expires: expires, unit: unit})
|
||||
}
|
||||
return fetchJSON(url, {
|
||||
method: 'POST'
|
||||
method: 'POST',
|
||||
body: body,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<template>
|
||||
<div class="card floating" id="share">
|
||||
<div class="card floating share__promt__card" id="share">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('buttons.share') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<ul>
|
||||
<li v-if="!hasPermanent">
|
||||
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
|
||||
</li>
|
||||
|
||||
<li v-for="link in links" :key="link.hash">
|
||||
<a :href="buildLink(link.hash)" target="_blank">
|
||||
@@ -27,6 +24,13 @@
|
||||
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
|
||||
</li>
|
||||
|
||||
<li v-if="!hasPermanent">
|
||||
<div>
|
||||
<input type="password" :placeholder="$t('prompts.optionalPassword')" v-model="passwordPermalink">
|
||||
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<input v-focus
|
||||
type="number"
|
||||
@@ -40,6 +44,7 @@
|
||||
<option value="hours">{{ $t('time.hours') }}</option>
|
||||
<option value="days">{{ $t('time.days') }}</option>
|
||||
</select>
|
||||
<input type="password" :placeholder="$t('prompts.optionalPassword')" v-model="password">
|
||||
<button class="action"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
@@ -72,7 +77,9 @@ export default {
|
||||
unit: 'hours',
|
||||
hasPermanent: false,
|
||||
links: [],
|
||||
clip: null
|
||||
clip: null,
|
||||
password: '',
|
||||
passwordPermalink: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -121,7 +128,7 @@ export default {
|
||||
if (!this.time) return
|
||||
|
||||
try {
|
||||
const res = await api.create(this.url, this.time, this.unit)
|
||||
const res = await api.create(this.url, this.password, this.time, this.unit)
|
||||
this.links.push(res)
|
||||
this.sort()
|
||||
} catch (e) {
|
||||
@@ -130,7 +137,7 @@ export default {
|
||||
},
|
||||
getPermalink: async function () {
|
||||
try {
|
||||
const res = await api.create(this.url)
|
||||
const res = await api.create(this.url, this.passwordPermalink)
|
||||
this.links.push(res)
|
||||
this.sort()
|
||||
this.hasPermanent = true
|
||||
|
||||
@@ -62,4 +62,17 @@
|
||||
|
||||
.share__box__items #listing.list .item .modified {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.share__wrong__password {
|
||||
background: var(--red);
|
||||
color: #fff;
|
||||
padding: .5em;
|
||||
text-align: center;
|
||||
animation: .2s opac forwards;
|
||||
}
|
||||
|
||||
.share__promt__card {
|
||||
max-width: max-content !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"selectMultiple": "Select multiple",
|
||||
"share": "Share",
|
||||
"shell": "Toggle shell",
|
||||
"submit": "Submit",
|
||||
"switchView": "Switch view",
|
||||
"toggleSidebar": "Toggle sidebar",
|
||||
"update": "Update",
|
||||
@@ -142,7 +143,8 @@
|
||||
"show": "Show",
|
||||
"size": "Size",
|
||||
"upload": "Upload",
|
||||
"uploadMessage": "Select an option to upload."
|
||||
"uploadMessage": "Select an option to upload.",
|
||||
"optionalPassword": "Optional password"
|
||||
},
|
||||
"search": {
|
||||
"images": "Images",
|
||||
|
||||
@@ -25,7 +25,8 @@ const state = {
|
||||
showMessage: null,
|
||||
showConfirm: null,
|
||||
previewMode: false,
|
||||
hash: ''
|
||||
hash: '',
|
||||
token: '',
|
||||
}
|
||||
|
||||
export default new Vuex.Store({
|
||||
|
||||
@@ -46,6 +46,7 @@ const mutations = {
|
||||
state.user = value
|
||||
},
|
||||
setJWT: (state, value) => (state.jwt = value),
|
||||
setToken: (state, value ) => (state.token = value),
|
||||
multiple: (state, value) => (state.multiple = value),
|
||||
addSelected: (state, value) => (state.selected.push(value)),
|
||||
addPlugin: (state, value) => {
|
||||
|
||||
@@ -74,6 +74,24 @@
|
||||
<div v-else-if="error">
|
||||
<not-found v-if="error.message === '404'"></not-found>
|
||||
<forbidden v-else-if="error.message === '403'"></forbidden>
|
||||
<div v-else-if="error.message === '401'">
|
||||
<div class="card floating" id="password">
|
||||
<div v-if="attemptedPasswordLogin" class="share__wrong__password">{{ $t('login.wrongCredentials') }}</div>
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('login.password') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<input v-focus type="password" :placeholder="$t('login.password')" v-model="password" @keyup.enter="fetchData">
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button class="button button--flat"
|
||||
@click="fetchData"
|
||||
:aria-label="$t('buttons.submit')"
|
||||
:title="$t('buttons.submit')">{{ $t('buttons.submit') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<internal-error v-else></internal-error>
|
||||
</div>
|
||||
</template>
|
||||
@@ -102,7 +120,9 @@ export default {
|
||||
data: () => ({
|
||||
error: null,
|
||||
path: '',
|
||||
showLimit: 500
|
||||
showLimit: 500,
|
||||
password: '',
|
||||
attemptedPasswordLogin: false
|
||||
}),
|
||||
watch: {
|
||||
'$route': 'fetchData'
|
||||
@@ -129,7 +149,11 @@ export default {
|
||||
return 'insert_drive_file'
|
||||
},
|
||||
link: function () {
|
||||
return `${baseURL}/api/public/dl/${this.hash}${this.path}`
|
||||
let queryArg = '';
|
||||
if (this.token !== ''){
|
||||
queryArg = `?token=${this.token}`
|
||||
}
|
||||
return `${baseURL}/api/public/dl/${this.hash}${this.path}${queryArg}`
|
||||
},
|
||||
fullLink: function () {
|
||||
return window.location.origin + this.link
|
||||
@@ -193,8 +217,13 @@ export default {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
let file = await api.getHash(encodeURIComponent(this.$route.params.pathMatch))
|
||||
if (this.password !== ''){
|
||||
this.attemptedPasswordLogin = true
|
||||
}
|
||||
let file = await api.getHash(encodeURIComponent(this.$route.params.pathMatch), this.password)
|
||||
this.path = file.path
|
||||
this.token = file.token || ''
|
||||
this.$store.commit('setToken', this.token)
|
||||
if (file.isDir) file.items = file.items.map((item, index) => {
|
||||
item.index = index
|
||||
item.url = `/share/${this.hash}${this.path}/${encodeURIComponent(item.name)}`
|
||||
|
||||
Reference in New Issue
Block a user