stash
This commit is contained in:
parent
17bfb84a94
commit
2fe8d14c2c
35 changed files with 1976 additions and 2134 deletions
1
frontend/extras
Submodule
1
frontend/extras
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 21bf28a8b6293872f5f7a42c9ca0bc12ede5aaae
|
||||
0
frontend/node_modules/.forgit
generated
vendored
0
frontend/node_modules/.forgit
generated
vendored
2358
frontend/package-lock.json
generated
2358
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -16,7 +16,10 @@
|
|||
"vue": "^3.2.47",
|
||||
"vue-multiselect": "^2.1.7",
|
||||
"vue-router": "^4.1.6",
|
||||
"vuex": "^4.1.0"
|
||||
"vuex": "^4.1.0",
|
||||
"extras": "file:./extras",
|
||||
"jquery": "^3.6.0",
|
||||
"popper.js": "^1.16.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
<h3 v-if="create">Create New</h3>
|
||||
<h3 v-else>Update</h3>
|
||||
<ul>
|
||||
<li v-for="file in whithout_images(files)" :key="file.id">
|
||||
<li v-for="file in without_images(files)" :key="file.id">
|
||||
{{ file.name }}
|
||||
</li>
|
||||
<li v-for="file in whithout_images(item_files)" :key="file.id">
|
||||
<li v-for="file in without_images(item_files)" :key="file.id">
|
||||
{{ file.name }}
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -100,23 +100,26 @@
|
|||
<script>
|
||||
import * as BIcons from "bootstrap-icons-vue";
|
||||
import {mapActions, mapState} from "vuex";
|
||||
import DragDropFileSource from "@/components/DragDropFileSource.vue";
|
||||
import FsFileSource from "@/components/FsFileSource.vue";
|
||||
import CameraFileSource from "@/components/CameraFileSource.vue";
|
||||
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
|
||||
import DeletableWrapper from "@/components/DeletableWrapper.vue";
|
||||
import WebcamFileSource from "@/components/WebcamFileSource.vue";
|
||||
//import DragDropFileSource from "@/../extras/components/inputs/DragDropFileSource.vue";
|
||||
//import CameraFileSource from "@/../extras/components/inputs/CameraFileSource.vue";
|
||||
//import FsFileSource from "@/../extras/components/inputs/FsFileSource.vue";
|
||||
import DragDropFileSource from "@/components/inputs/DragDropFileSource.vue";
|
||||
import CameraFileSource from "@/components/inputs/CameraFileSource.vue";
|
||||
import FsFileSource from "@/components/inputs/FsFileSource.vue";
|
||||
import WebcamFileSource from "@/components/inputs/WebcamFileSource.vue";
|
||||
|
||||
export default {
|
||||
name: "CombinedFileField",
|
||||
components: {
|
||||
WebcamFileSource,
|
||||
...BIcons,
|
||||
AuthenticatedImage,
|
||||
DeletableWrapper,
|
||||
DragDropFileSource,
|
||||
CameraFileSource,
|
||||
FsFileSource
|
||||
FsFileSource,
|
||||
...BIcons
|
||||
},
|
||||
props: {
|
||||
item_files: {
|
||||
|
|
@ -182,7 +185,7 @@ export default {
|
|||
only_images(files) {
|
||||
return files.filter(file => file.mime_type.startsWith("image/"));
|
||||
},
|
||||
whithout_images(files) {
|
||||
without_images(files) {
|
||||
return files.filter(file => !file.mime_type.startsWith("image/"));
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@
|
|||
import * as BIcons from "bootstrap-icons-vue";
|
||||
import {mapActions, mapState} from "vuex";
|
||||
import PropertyBadge from "@/components/PropertyBadge.vue";
|
||||
import BadgeSelectField from "@/components/BadgeSelectField.vue";
|
||||
//import BadgeSelectField from "@/../extras/components/inputs/BadgeSelectField.vue";
|
||||
import BadgeSelectField from "@/components/inputs/BadgeSelectField.vue";
|
||||
|
||||
|
||||
export default {
|
||||
name: "PropertyField",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@
|
|||
<script>
|
||||
import * as BIcons from "bootstrap-icons-vue";
|
||||
import {mapActions, mapState} from "vuex";
|
||||
import BadgeSelectField from "@/components/BadgeSelectField.vue";
|
||||
//import BadgeSelectField from "@/../extras/components/inputs/BadgeSelectField.vue";
|
||||
import BadgeSelectField from "@/components/inputs/BadgeSelectField.vue";
|
||||
|
||||
export default {
|
||||
name: "TagField",
|
||||
|
|
|
|||
|
|
@ -33,16 +33,13 @@ class ServerSet {
|
|||
}
|
||||
const url = "https://" + server + target // TODO https
|
||||
return await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
method: 'GET', headers: {
|
||||
...auth.buildAuthHeader(url)
|
||||
},
|
||||
credentials: 'omit'
|
||||
}, credentials: 'omit'
|
||||
}).catch(err => {
|
||||
console.error('get from server failed', server, err)
|
||||
this.unreachable_neighbors.unreachable(server)
|
||||
}
|
||||
).then(response => response.json())
|
||||
console.error('get from server failed', server, err)
|
||||
this.unreachable_neighbors.unreachable(server)
|
||||
}).then(response => response.json())
|
||||
} catch (e) {
|
||||
console.error('get from server failed', server, e)
|
||||
}
|
||||
|
|
@ -61,18 +58,13 @@ class ServerSet {
|
|||
}
|
||||
const url = "https://" + server + target // TODO https
|
||||
return await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...auth.buildAuthHeader(url, data)
|
||||
},
|
||||
credentials: 'omit',
|
||||
body: JSON.stringify(data)
|
||||
method: 'POST', headers: {
|
||||
'Content-Type': 'application/json', ...auth.buildAuthHeader(url, data)
|
||||
}, credentials: 'omit', body: JSON.stringify(data)
|
||||
}).catch(err => {
|
||||
console.error('post to server failed', server, err)
|
||||
this.unreachable_neighbors.unreachable(server)
|
||||
}
|
||||
).then(response => response.json())
|
||||
console.error('post to server failed', server, err)
|
||||
this.unreachable_neighbors.unreachable(server)
|
||||
}).then(response => response.json())
|
||||
} catch (e) {
|
||||
console.error('post to server failed', server, e)
|
||||
}
|
||||
|
|
@ -91,18 +83,13 @@ class ServerSet {
|
|||
}
|
||||
const url = "https://" + server + target // TODO https
|
||||
return await fetch(url, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...auth.buildAuthHeader(url, data)
|
||||
},
|
||||
credentials: 'omit',
|
||||
body: JSON.stringify(data)
|
||||
method: 'PATCH', headers: {
|
||||
'Content-Type': 'application/json', ...auth.buildAuthHeader(url, data)
|
||||
}, credentials: 'omit', body: JSON.stringify(data)
|
||||
}).catch(err => {
|
||||
console.error('patch to server failed', server, err)
|
||||
this.unreachable_neighbors.unreachable(server)
|
||||
}
|
||||
).then(response => response.json())
|
||||
console.error('patch to server failed', server, err)
|
||||
this.unreachable_neighbors.unreachable(server)
|
||||
}).then(response => response.json())
|
||||
} catch (e) {
|
||||
console.error('patch to server failed', server, e)
|
||||
}
|
||||
|
|
@ -121,18 +108,13 @@ class ServerSet {
|
|||
}
|
||||
const url = "https://" + server + target // TODO https
|
||||
return await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...auth.buildAuthHeader(url, data)
|
||||
},
|
||||
credentials: 'omit',
|
||||
body: JSON.stringify(data)
|
||||
method: 'PUT', headers: {
|
||||
'Content-Type': 'application/json', ...auth.buildAuthHeader(url, data)
|
||||
}, credentials: 'omit', body: JSON.stringify(data)
|
||||
}).catch(err => {
|
||||
console.error('put to server failed', server, err)
|
||||
this.unreachable_neighbors.unreachable(server)
|
||||
}
|
||||
).then(response => response.json())
|
||||
console.error('put to server failed', server, err)
|
||||
this.unreachable_neighbors.unreachable(server)
|
||||
}).then(response => response.json())
|
||||
} catch (e) {
|
||||
console.error('put to server failed', server, e)
|
||||
}
|
||||
|
|
@ -151,16 +133,13 @@ class ServerSet {
|
|||
}
|
||||
const url = "https://" + server + target // TODO https
|
||||
return await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
method: 'DELETE', headers: {
|
||||
...auth.buildAuthHeader(url)
|
||||
},
|
||||
credentials: 'omit'
|
||||
}, credentials: 'omit'
|
||||
}).catch(err => {
|
||||
console.error('delete from server failed', server, err)
|
||||
this.unreachable_neighbors.unreachable(server)
|
||||
}
|
||||
)
|
||||
console.error('delete from server failed', server, err)
|
||||
this.unreachable_neighbors.unreachable(server)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('delete from server failed', server, e)
|
||||
}
|
||||
|
|
@ -179,102 +158,159 @@ class ServerSet {
|
|||
}
|
||||
const url = "https://" + server + target // TODO https
|
||||
return await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
method: 'GET', headers: {
|
||||
...auth.buildAuthHeader(url)
|
||||
},
|
||||
credentials: 'omit'
|
||||
}, credentials: 'omit'
|
||||
}).catch(err => {
|
||||
console.error('get from server failed', server, err)
|
||||
this.unreachable_neighbors.unreachable(server)
|
||||
}
|
||||
)
|
||||
console.error('get from server failed', server, err)
|
||||
this.unreachable_neighbors.unreachable(server)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('get from server failed', server, e)
|
||||
}
|
||||
}
|
||||
throw new Error('all servers failed')
|
||||
}
|
||||
|
||||
async postRaw(auth, target, data) {
|
||||
if (!auth || typeof auth.buildAuthHeader !== 'function') {
|
||||
throw new Error('no auth')
|
||||
}
|
||||
for (const server of this.servers) {
|
||||
try {
|
||||
if (this.unreachable_neighbors.queryUnreachable(server)) {
|
||||
continue
|
||||
}
|
||||
const url = "https://" + server + target // TODO https
|
||||
return await fetch(url, {
|
||||
method: 'POST', headers: {
|
||||
'Content-Type': 'application/json', ...auth.buildAuthHeader(url, data)
|
||||
}, credentials: 'omit', body: data
|
||||
}).catch(err => {
|
||||
console.error('post to server failed', server, err)
|
||||
this.unreachable_neighbors.unreachable(server)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('post to server failed', server, e)
|
||||
}
|
||||
}
|
||||
throw new Error('all servers failed')
|
||||
}
|
||||
}
|
||||
|
||||
class ServerSetUnion {
|
||||
constructor(serverSets) {
|
||||
if (!serverSets || !Array.isArray(serverSets)) {
|
||||
throw new Error('no serverSets')
|
||||
}
|
||||
this.serverSets = serverSets;
|
||||
}
|
||||
//class ServerSetUnion {
|
||||
// constructor(serverSets) {
|
||||
// if (!serverSets || !Array.isArray(serverSets)) {
|
||||
// throw new Error('no serverSets')
|
||||
// }
|
||||
// this.serverSets = serverSets;
|
||||
// }
|
||||
//
|
||||
// add(serverset) {
|
||||
// if (!serverset || !(serverset instanceof ServerSet)) {
|
||||
// throw new Error('no serverset')
|
||||
// }
|
||||
// if (this.serverSets.find(s => serverset.servers.every(s2 => s.servers.includes(s2)))) {
|
||||
// console.warn('serverset already in union', serverset)
|
||||
// return
|
||||
// }
|
||||
// this.serverSets.push(serverset)
|
||||
// }
|
||||
//
|
||||
// async get(auth, target) {
|
||||
// try {
|
||||
// return await this.serverSets.reduce(async (acc, serverset) => {
|
||||
// return acc.then(async (acc) => {
|
||||
// return acc.concat(await serverset.get(auth, target))
|
||||
// })
|
||||
// }, Promise.resolve([]))
|
||||
// } catch (e) {
|
||||
// throw new Error('all servers failed')
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// async post(auth, target, data) {
|
||||
// try {
|
||||
// return await this.serverSets.reduce(async (acc, serverset) => {
|
||||
// return acc.then(async (acc) => {
|
||||
// return acc.concat(await serverset.post(auth, target, data))
|
||||
// })
|
||||
// }, Promise.resolve([]))
|
||||
// } catch (e) {
|
||||
// throw new Error('all servers failed')
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// async patch(auth, target, data) {
|
||||
// try {
|
||||
// return await this.serverSets.reduce(async (acc, serverset) => {
|
||||
// return acc.then(async (acc) => {
|
||||
// return acc.concat(await serverset.patch(auth, target, data))
|
||||
// })
|
||||
// }, Promise.resolve([]))
|
||||
// } catch (e) {
|
||||
// throw new Error('all servers failed')
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// async put(auth, target, data) {
|
||||
// try {
|
||||
// return await this.serverSets.reduce(async (acc, serverset) => {
|
||||
// return acc.then(async (acc) => {
|
||||
// return acc.concat(await serverset.put(auth, target, data))
|
||||
// })
|
||||
// }, Promise.resolve([]))
|
||||
// } catch (e) {
|
||||
// throw new Error('all servers failed')
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// async delete(auth, target) {
|
||||
// try {
|
||||
// return await this.serverSets.reduce(async (acc, serverset) => {
|
||||
// return acc.then(async (acc) => {
|
||||
// return acc.concat(await serverset.delete(auth, target))
|
||||
// })
|
||||
// }, Promise.resolve([]))
|
||||
// } catch (e) {
|
||||
// throw new Error('all servers failed')
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
add(serverset) {
|
||||
if (!serverset || !(serverset instanceof ServerSet)) {
|
||||
throw new Error('no serverset')
|
||||
}
|
||||
if (this.serverSets.find(s => serverset.servers.every(s2 => s.servers.includes(s2)))) {
|
||||
console.warn('serverset already in union', serverset)
|
||||
return
|
||||
}
|
||||
this.serverSets.push(serverset)
|
||||
}
|
||||
|
||||
async get(auth, target) {
|
||||
try {
|
||||
return await this.serverSets.reduce(async (acc, serverset) => {
|
||||
return acc.then(async (acc) => {
|
||||
return acc.concat(await serverset.get(auth, target))
|
||||
})
|
||||
}, Promise.resolve([]))
|
||||
} catch (e) {
|
||||
throw new Error('all servers failed')
|
||||
}
|
||||
}
|
||||
|
||||
async post(auth, target, data) {
|
||||
try {
|
||||
return await this.serverSets.reduce(async (acc, serverset) => {
|
||||
return acc.then(async (acc) => {
|
||||
return acc.concat(await serverset.post(auth, target, data))
|
||||
})
|
||||
}, Promise.resolve([]))
|
||||
} catch (e) {
|
||||
throw new Error('all servers failed')
|
||||
}
|
||||
}
|
||||
|
||||
async patch(auth, target, data) {
|
||||
try {
|
||||
return await this.serverSets.reduce(async (acc, serverset) => {
|
||||
return acc.then(async (acc) => {
|
||||
return acc.concat(await serverset.patch(auth, target, data))
|
||||
})
|
||||
}, Promise.resolve([]))
|
||||
} catch (e) {
|
||||
throw new Error('all servers failed')
|
||||
}
|
||||
}
|
||||
|
||||
async put(auth, target, data) {
|
||||
try {
|
||||
return await this.serverSets.reduce(async (acc, serverset) => {
|
||||
return acc.then(async (acc) => {
|
||||
return acc.concat(await serverset.put(auth, target, data))
|
||||
})
|
||||
}, Promise.resolve([]))
|
||||
} catch (e) {
|
||||
throw new Error('all servers failed')
|
||||
}
|
||||
}
|
||||
|
||||
async delete(auth, target) {
|
||||
try {
|
||||
return await this.serverSets.reduce(async (acc, serverset) => {
|
||||
return acc.then(async (acc) => {
|
||||
return acc.concat(await serverset.delete(auth, target))
|
||||
})
|
||||
}, Promise.resolve([]))
|
||||
} catch (e) {
|
||||
throw new Error('all servers failed')
|
||||
}
|
||||
}
|
||||
function ServerSetUnion(serverSets) {
|
||||
return new Proxy(serverSets, {
|
||||
get: function (target, prop, receiver) {
|
||||
const funcs = Object.getOwnPropertyNames(ServerSet.prototype)
|
||||
if (funcs.includes(prop)) {
|
||||
return async function (...args) {
|
||||
try {
|
||||
return await target.reduce(async (acc, serverset) => {
|
||||
return acc.then(async (acc) => {
|
||||
return acc.concat(await serverset[prop](...args))
|
||||
})
|
||||
}, Promise.resolve([]))
|
||||
} catch (e) {
|
||||
throw new Error('all servers failed')
|
||||
}
|
||||
}
|
||||
} else if (prop === 'add') {
|
||||
return function (serverset) {
|
||||
if (!serverset || !(serverset instanceof ServerSet)) {
|
||||
throw new Error('no serverset')
|
||||
}
|
||||
if (target.find(s => serverset.servers.every(s2 => s.servers.includes(s2)))) {
|
||||
console.warn('serverset already in union', serverset)
|
||||
return
|
||||
}
|
||||
target.push(serverset)
|
||||
}
|
||||
} else {
|
||||
return target[prop]
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
112
frontend/src/identity.js
Normal file
112
frontend/src/identity.js
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
async function deriveKeyFromPassword(password, salt = null, iter = 100000) {
|
||||
if (!salt) {
|
||||
salt = window.crypto.getRandomValues(new Uint8Array(16))
|
||||
}
|
||||
const pwbuffer = await window.crypto.subtle.importKey(
|
||||
"raw",
|
||||
new TextEncoder().encode(password),
|
||||
{name: "PBKDF2"},
|
||||
false,
|
||||
["deriveBits"]
|
||||
);
|
||||
const key_bits = await window.crypto.subtle.deriveBits(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
salt,
|
||||
iterations: iter,
|
||||
hash: "SHA-256"
|
||||
},
|
||||
pwbuffer,
|
||||
256
|
||||
)
|
||||
const key = await window.crypto.subtle.importKey(
|
||||
"raw",
|
||||
key_bits,
|
||||
{name: "AES-GCM"},
|
||||
false,
|
||||
["encrypt", "decrypt"]
|
||||
)
|
||||
return {key, salt, alg: 'pbkdf2', iter}
|
||||
}
|
||||
|
||||
async function serializeIdentityRecord(user, keypair, password = null) {
|
||||
if (password) {
|
||||
const plain_key = `${nacl.to_hex(keypair.signSk).slice(0, 64)}:${nacl.to_hex(keypair.signPk).slice(56, 64)}`
|
||||
const {key, salt, alg, iter} = await deriveKeyFromPassword(password)
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(12))
|
||||
const encrypted_key = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv
|
||||
},
|
||||
key,
|
||||
new TextEncoder('utf-8').encode(plain_key)
|
||||
)
|
||||
return `${user}:$${alg}$${iter}$${nacl.to_hex(salt)}$${nacl.to_hex(iv)}$${nacl.to_hex(new Uint8Array(encrypted_key))}`
|
||||
}
|
||||
return `${user}:${nacl.to_hex(keypair.signSk).slice(0, 64)}:${nacl.to_hex(keypair.signPk).slice(56, 64)}`
|
||||
}
|
||||
|
||||
function parseIdentityRecord(record) {
|
||||
const segments = record.split(':');
|
||||
if (segments.length === 3) {
|
||||
try {
|
||||
const [user, key_hex, sign_hex] = segments
|
||||
const keypair = nacl.crypto_sign_keypair_from_seed(nacl.from_hex(key_hex))
|
||||
const [username, domain] = user.split('@')
|
||||
if (nacl.to_hex(keypair.signPk).slice(56, 64) !== sign_hex || username === '' || domain === '') {
|
||||
return {user: null, keypair: null, errors: ['Invalid identity record']}
|
||||
}
|
||||
return {user, keypair, errors: null}
|
||||
} catch (e) {
|
||||
return {user: null, keypair: null, errors: ['Invalid identity record']}
|
||||
}
|
||||
} else if (segments.length === 2) {
|
||||
try {
|
||||
const [user, rest] = segments
|
||||
if (!rest.startsWith('$')) {
|
||||
return {user: null, keypair: null, errors: ['Invalid identity record']}
|
||||
}
|
||||
const [alg, iter, salt, iv, crypted] = rest.slice(1).split('$')
|
||||
if (alg !== 'pbkdf2') {
|
||||
return {user: null, keypair: null, errors: ['Invalid identity record']}
|
||||
}
|
||||
return {
|
||||
user,
|
||||
alg,
|
||||
iter: parseInt(iter),
|
||||
salt: nacl.from_hex(salt),
|
||||
iv: nacl.from_hex(iv),
|
||||
crypted: nacl.from_hex(crypted),
|
||||
errors: null
|
||||
}
|
||||
} catch (e) {
|
||||
return {user: null, keypair: null, errors: ['Invalid identity record']}
|
||||
}
|
||||
}
|
||||
return {user: null, keypair: null, errors: ['Invalid identity record']}
|
||||
}
|
||||
|
||||
async function decryptIdentityRecord(meta, user, password) {
|
||||
try {
|
||||
const {alg, iter, salt, iv, crypted} = meta
|
||||
if (alg !== 'pbkdf2') {
|
||||
return {user: null, keypair: null, errors: ['Unsupported algorithm']}
|
||||
}
|
||||
const {key} = await deriveKeyFromPassword(password, salt, iter)
|
||||
const decrypted = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv
|
||||
},
|
||||
key,
|
||||
crypted
|
||||
)
|
||||
const plain_key = new TextDecoder('utf-8').decode(decrypted)
|
||||
return parseIdentityRecord(`${user}:${plain_key}`)
|
||||
}catch (e) {
|
||||
return {user: null, keypair: null, errors: ['could not decrypt identity record']}
|
||||
}
|
||||
}
|
||||
|
||||
export {parseIdentityRecord, serializeIdentityRecord, decryptIdentityRecord}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import {createApp} from 'vue'
|
||||
|
||||
import {BootstrapIconsPlugin} from 'bootstrap-icons-vue';
|
||||
import App from './App.vue'
|
||||
|
||||
|
|
@ -16,6 +17,8 @@ _nacl.instantiate((nacl) => {
|
|||
app.use(router).mount('#app')
|
||||
});
|
||||
|
||||
//app.component(VueQrcode.name, VueQrcode);
|
||||
|
||||
window.closeAllDropdowns = function () {
|
||||
const dropdowns = document.getElementsByClassName("dropdown-menu");
|
||||
let i;
|
||||
|
|
@ -27,6 +30,7 @@ window.closeAllDropdowns = function () {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
window.onclick = function (event) {
|
||||
if (!event.target.matches('.dropdown-toggle *')
|
||||
&& !event.target.matches('.dropdown-toggle')
|
||||
|
|
@ -39,7 +43,7 @@ window.onclick = function (event) {
|
|||
&& !event.target.matches('.sidebar *')
|
||||
&& !event.target.matches('.sidebar')) {
|
||||
const sidebar = document.getElementById("sidebar");
|
||||
const marginLeft = parseInt(getComputedStyle(sidebar).marginLeft);
|
||||
const marginLeft = parseInt(window.getComputedStyle(sidebar).marginLeft);
|
||||
if (sidebar.classList.contains('collapsed') && marginLeft === 0) {
|
||||
sidebar.classList.remove('collapsed');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {createRouter, createWebHistory} from 'vue-router'
|
|||
import Dashboard from '@/views/Dashboard.vue';
|
||||
import Login from '@/views/Login.vue';
|
||||
import Register from '@/views/Register.vue';
|
||||
import Pairing from '@/views/Pairing.vue';
|
||||
import store from '@/store';
|
||||
import Profile from '@/views/Profile.vue';
|
||||
import Settings from '@/views/Settings.vue';
|
||||
|
|
@ -15,29 +16,67 @@ import Admin from '@/views/Admin.vue';
|
|||
import Swatch from '@/views/Swatch.vue';
|
||||
import Files from '@/views/Files.vue';
|
||||
|
||||
import Account from '@/views/settings/Account.vue';
|
||||
import Password from '@/views/settings/Password.vue';
|
||||
import Privacy from '@/views/settings/Privacy.vue';
|
||||
import Email from '@/views/settings/Email.vue';
|
||||
import Notifications from '@/views/settings/Notifications.vue';
|
||||
import Data from '@/views/settings/Data.vue';
|
||||
|
||||
const routes = [
|
||||
{path: '/', component: Dashboard, meta: {requiresAuth: true}},
|
||||
{path: '/profile', component: Profile, meta: {requiresAuth: true}},
|
||||
{path: '/settings', component: Settings, meta: {requiresAuth: true}},
|
||||
{path: '/inventory', component: Inventory, meta: {requiresAuth: true}},
|
||||
{path: '/inventory/:id', component: InventoryDetail, meta: {requiresAuth: true}, props: true},
|
||||
{path: '/inventory/:id/edit', component: InventoryEdit, meta: {requiresAuth: true}, props: true},
|
||||
{path: '/inventory/new', component: InventoryNew, meta: {requiresAuth: true}},
|
||||
{path: '/friends', component: Friends, meta: {requiresAuth: true}},
|
||||
{path: '/files', component: Files, meta: {requiresAuth: true}},
|
||||
{path: '/admin', component: Admin, meta: {requiresAuth: true}},
|
||||
{path: '/swatch', component: Swatch, meta: {requiresAuth: true}},
|
||||
{path: '/search/:query', component: Search, meta: {requiresAuth: true}, props: true},
|
||||
{path: '/login', component: Login, meta: {requiresAuth: false}},
|
||||
{path: '/register', component: Register, meta: {requiresAuth: false}},
|
||||
{path: '/:pathMatch(.*)*', redirect: '/'}
|
||||
]
|
||||
|
||||
const routes = [{path: '/', component: Dashboard, meta: {requiresAuth: true}}, {
|
||||
path: '/inventory',
|
||||
component: Inventory,
|
||||
meta: {requiresAuth: true}
|
||||
}, {
|
||||
path: '/inventory/:id',
|
||||
component: InventoryDetail,
|
||||
meta: {requiresAuth: true},
|
||||
props: true
|
||||
}, {
|
||||
path: '/inventory/:id/edit',
|
||||
component: InventoryEdit,
|
||||
meta: {requiresAuth: true},
|
||||
props: true
|
||||
}, {path: '/inventory/new', component: InventoryNew, meta: {requiresAuth: true}}, {
|
||||
path: '/friends',
|
||||
component: Friends,
|
||||
meta: {requiresAuth: true}
|
||||
}, {path: '/files', component: Files, meta: {requiresAuth: true}}, {
|
||||
path: '/admin',
|
||||
component: Admin,
|
||||
meta: {requiresAuth: true}
|
||||
}, {path: '/swatch', component: Swatch, meta: {requiresAuth: true}}, {
|
||||
path: '/search/:query',
|
||||
component: Search,
|
||||
meta: {requiresAuth: true},
|
||||
props: true
|
||||
}, {path: '/login', component: Login, meta: {requiresAuth: false}}, {
|
||||
path: '/register',
|
||||
component: Register,
|
||||
meta: {requiresAuth: false}
|
||||
}, {path: '/pairing', component: Pairing, meta: {requiresAuth: false}}, {
|
||||
path: '/profile',
|
||||
component: Profile,
|
||||
meta: {requiresAuth: true}
|
||||
}, {
|
||||
path: '/settings', component: Settings, meta: {requiresAuth: true}, children: [{
|
||||
path: '', name: 'account', component: Account, meta: {requiresAuth: true}
|
||||
},{
|
||||
path: 'password/', name: 'password', component: Password, meta: {requiresAuth: true}
|
||||
}, {
|
||||
path: 'privacy/', name: 'privacy', component: Privacy, meta: {requiresAuth: true}
|
||||
}, {
|
||||
path: 'email/', name: 'email', component: Email, meta: {requiresAuth: true}
|
||||
}, {
|
||||
path: 'notifications/', name: 'notifications', component: Notifications, meta: {requiresAuth: true}
|
||||
}, {
|
||||
path: 'data/', name: 'data', component: Data, meta: {requiresAuth: true}
|
||||
}]
|
||||
}, {path: '/:pathMatch(.*)*', redirect: '/'}]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
linkActiveClass: "active",
|
||||
routes,
|
||||
history: createWebHistory(), linkActiveClass: "active", routes,
|
||||
})
|
||||
|
||||
router.beforeEach((to/*, from*/) => {
|
||||
|
|
@ -45,8 +84,7 @@ router.beforeEach((to/*, from*/) => {
|
|||
if (to.meta.requiresAuth && !store.getters.isLoggedIn) {
|
||||
console.log("Not logged in, redirecting to login page")
|
||||
return {
|
||||
path: '/login',
|
||||
// save the location we were at to come back later
|
||||
path: '/login', // save the location we were at to come back later
|
||||
query: {redirect: to.fullPath},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,5 +72,9 @@
|
|||
align-items: center;
|
||||
padding: .25rem .7rem;
|
||||
background-color: map-get($theme-colors, background-2);
|
||||
#border-right-width: 0;
|
||||
}
|
||||
|
||||
.input-group-text:not(:last-child) {
|
||||
border-right-width: 0;
|
||||
}
|
||||
|
|
@ -3,6 +3,9 @@ import router from '@/router';
|
|||
import FallBackResolver from "@/dns";
|
||||
import NeighborsCache from "@/neigbors";
|
||||
import {createNullAuth, createSignAuth, createTokenAuth, ServerSet, ServerSetUnion} from "@/federation";
|
||||
import {parseIdentityRecord, serializeIdentityRecord} from "@/identity";
|
||||
//import sharedStatePlugin from "@/../extras/shared-state-plugin";
|
||||
//import persistentStatePlugin from "@/../extras/persistent-state-plugin";
|
||||
|
||||
|
||||
export default createStore({
|
||||
|
|
@ -29,30 +32,6 @@ export default createStore({
|
|||
storage_locations: [],
|
||||
},
|
||||
mutations: {
|
||||
setUser(state, user) {
|
||||
state.user = user;
|
||||
if (state.remember)
|
||||
localStorage.setItem('user', user);
|
||||
},
|
||||
setToken(state, token) {
|
||||
state.token = token;
|
||||
if (state.remember)
|
||||
localStorage.setItem('token', token);
|
||||
},
|
||||
setKey(state, keypair) {
|
||||
state.keypair = nacl.crypto_sign_keypair_from_seed(nacl.from_hex(keypair))
|
||||
if (state.remember)
|
||||
localStorage.setItem('keypair', nacl.to_hex(state.keypair.signSk).slice(0, 64))
|
||||
},
|
||||
setRemember(state, remember) {
|
||||
state.remember = remember;
|
||||
if (!remember) {
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('keypair');
|
||||
}
|
||||
localStorage.setItem('remember', remember);
|
||||
},
|
||||
setInventoryItems(state, {url, items}) {
|
||||
state.item_map[url] = items;
|
||||
},
|
||||
|
|
@ -86,6 +65,45 @@ export default createStore({
|
|||
setFiles(state, files) {
|
||||
state.files = files;
|
||||
},
|
||||
setUser(state, user) {
|
||||
state.user = user;
|
||||
if (state.remember)
|
||||
localStorage.setItem('user', user);
|
||||
},
|
||||
setToken(state, token) {
|
||||
state.token = token;
|
||||
if (state.remember)
|
||||
localStorage.setItem('token', token);
|
||||
},
|
||||
setKey(state, keypair) {
|
||||
state.keypair = nacl.crypto_sign_keypair_from_seed(nacl.from_hex(keypair))
|
||||
if (state.remember)
|
||||
localStorage.setItem('keypair', nacl.to_hex(state.keypair.signSk).slice(0, 64))
|
||||
},
|
||||
setRemember(state, remember) {
|
||||
state.remember = remember;
|
||||
if (!remember) {
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('keypair');
|
||||
}
|
||||
localStorage.setItem('remember', remember);
|
||||
},
|
||||
setUserFromIdentityRecord(state, {user, keypair}) {
|
||||
try {
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('keypair');
|
||||
state.user = user;
|
||||
state.keypair = keypair;
|
||||
if (state.remember) {
|
||||
localStorage.setItem('user', user);
|
||||
localStorage.setItem('keypair', key)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Invalid identity record')
|
||||
}
|
||||
},
|
||||
logout(state) {
|
||||
state.user = null;
|
||||
state.token = null;
|
||||
|
|
@ -357,6 +375,9 @@ export default createStore({
|
|||
state.last_load.categories = Date.now()
|
||||
state.last_load.availability_policies = Date.now()
|
||||
return data
|
||||
},
|
||||
async userIdentityRecord({state}, {password}) {
|
||||
return await serializeIdentityRecord(state.user, state.keypair, password);
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
|
|
@ -371,7 +392,7 @@ export default createStore({
|
|||
state.local_loaded = true
|
||||
}
|
||||
|
||||
return state.user !== null && state.token !== null;
|
||||
return state.user !== null && (state.token !== null || state.keypair !== null)
|
||||
},
|
||||
signAuth(state) {
|
||||
return createSignAuth(state.user, state.keypair.signSk)
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@
|
|||
Don’t have an account?
|
||||
<router-link to="/register">Sign up</router-link>
|
||||
</p>
|
||||
<p class="mb-0 text-muted">
|
||||
Connect with existing account?
|
||||
<router-link to="/pairing">Pairing</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
236
frontend/src/views/Pairing.vue
Normal file
236
frontend/src/views/Pairing.vue
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
<template>
|
||||
<div class="main d-flex w-100">
|
||||
<div class="container d-flex flex-column">
|
||||
<div class="row vh-100">
|
||||
<div class="col-sm-10 col-md-8 col-lg-6 mx-auto d-table h-100">
|
||||
<div class="d-table-cell align-middle">
|
||||
<div class="text-center mt-4">
|
||||
<h1 class="h2">
|
||||
Toolshed
|
||||
</h1>
|
||||
|
||||
<p class="lead" v-if="msg">
|
||||
{{ msg }}
|
||||
</p>
|
||||
<p class="lead" v-else>
|
||||
Create an account to get started
|
||||
</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="m-sm-4">
|
||||
<div :class="errors.raw_record?['mb-3','is-invalid']:['mb-3']">
|
||||
<label class="form-label">Identity Record</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control form-control-lg" type="text"
|
||||
:class="errors.raw_record?['is-invalid']:[]"
|
||||
v-model="raw_record" placeholder="Paste identity record"
|
||||
@input.prevent="try_parsing">
|
||||
<span class="input-group-text form-control-lg" v-if="raw_record == ''"
|
||||
@click="pasteClipboard()">
|
||||
<b-icon-clipboard></b-icon-clipboard>
|
||||
</span>
|
||||
<span class="input-group-text form-control-lg" v-else
|
||||
@click="raw_record = ''">
|
||||
<b-icon-x-circle></b-icon-x-circle>
|
||||
</span>
|
||||
</div>
|
||||
<div class="invalid-feedback">
|
||||
{{ errors.raw_record }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" v-if="is_unlocked || is_locked">
|
||||
<label class="form-label">Username</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control form-control-lg" type="email"
|
||||
disabled v-model="username">
|
||||
<span class="input-group-text form-control-lg bg-success"
|
||||
v-if="is_unlocked">
|
||||
<b-icon-unlock></b-icon-unlock>
|
||||
</span>
|
||||
<span class="input-group-text form-control-lg bg-warning"
|
||||
v-if="is_locked">
|
||||
<b-icon-lock></b-icon-lock>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" v-if="is_locked" :class="errors.password?['is-invalid']:[]">
|
||||
<form role="form" method="post" @submit.prevent="try_unlock">
|
||||
<label class="form-label">Password</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control form-control-lg" type="password"
|
||||
name="password" placeholder="Enter your password"
|
||||
v-model="password" :class="errors.password?['is-invalid']:[]">
|
||||
<button class="btn btn-lg btn-primary" type="button"
|
||||
@click="try_unlock">
|
||||
<b-icon-unlock></b-icon-unlock>
|
||||
unlock
|
||||
</button>
|
||||
</div>
|
||||
<div class="invalid-feedback">
|
||||
{{ errors.password }}
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="remember-me"
|
||||
name="remember-me" checked v-model="remember"
|
||||
@change="setRemember(remember)">
|
||||
<span class="form-check-label">
|
||||
Remember me next time
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<button type="submit" class="btn btn-lg btn-primary" :disabled="!is_unlocked"
|
||||
@click="try_pairing">
|
||||
Pair
|
||||
</button>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="text-center">
|
||||
<p class="mb-0 text-muted">
|
||||
Already have an account?
|
||||
<router-link to="/login">Login</router-link>
|
||||
</p>
|
||||
<p class="mb-0 text-muted">
|
||||
Don’t have an account?
|
||||
<router-link to="/register">Sign up</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapActions, mapMutations} from 'vuex';
|
||||
import {ServerSet, createSignAuth} from '../federation';
|
||||
import NeighborsCache from '../neigbors';
|
||||
import {decryptIdentityRecord, parseIdentityRecord, serializeIdentityRecord} from "@/identity";
|
||||
import router from "@/router";
|
||||
|
||||
export default {
|
||||
name: 'Pairing',
|
||||
data() {
|
||||
return {
|
||||
msg: 'Pair with existing Account',
|
||||
//raw_record: 'test_a@a.localhost:4ab79601edc400fd0263c7d5fd045ea7f5de28f1dd8905e2479f96893f3257d8:7f0ee42b',
|
||||
//raw_record: 'test_a@a.localhost:$pbkdf2$100000$67da1c2a055dd41fc9707c37d2cdecea$8173b4d626352bcafab626b2$60027066142f55ecfd6f91b587f99c5f02ee536ce08c31e3f69148a51711489220a2b10d5450166ee09a781f891be8ab3a418e31a8e71f6468f189ba0baf31ece21166b63ba608683bdbea8afa7dfc33b1a994d8be4e4b2123',
|
||||
raw_record: '',
|
||||
crypted_record: null,
|
||||
username: '',
|
||||
password: '',
|
||||
keypair: '',
|
||||
remember: false,
|
||||
errors: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
is_unlocked() {
|
||||
return !!this.keypair && !!this.username;
|
||||
},
|
||||
is_locked() {
|
||||
return !!this.username && !!this.crypted_record && !this.keypair;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['lookupServer']),
|
||||
...mapMutations(['setRemember', 'setUserFromIdentityRecord']),
|
||||
async try_pairing() {
|
||||
const unreachable = new NeighborsCache();
|
||||
const servers = await this.lookupServer({username: this.username}).then(servers => new ServerSet(servers, unreachable))
|
||||
const auth = createSignAuth(this.username, this.keypair.signSk);
|
||||
const reply = await servers.getRaw(auth, '/api/friends/');
|
||||
if (reply.status === 200) {
|
||||
this.setUserFromIdentityRecord({username: this.username, keypair: this.keypair});
|
||||
router.push({path: '/'});
|
||||
} else {
|
||||
console.error('Error:', reply);
|
||||
}
|
||||
},
|
||||
try_parsing() {
|
||||
if (!this.raw_record) {
|
||||
this.username = '';
|
||||
this.keypair = '';
|
||||
this.errors = {};
|
||||
return;
|
||||
}
|
||||
const parsed = parseIdentityRecord(this.raw_record)
|
||||
const {user, keypair, errors} = parsed;
|
||||
if (user) {
|
||||
this.username = user;
|
||||
}
|
||||
if (errors) {
|
||||
this.errors.raw_record = errors;
|
||||
}
|
||||
if (keypair) {
|
||||
this.keypair = keypair;
|
||||
}
|
||||
if (parsed.crypted) {
|
||||
this.crypted_record = parsed;
|
||||
}
|
||||
},
|
||||
async try_unlock() {
|
||||
const {
|
||||
user,
|
||||
keypair,
|
||||
errors
|
||||
} = await decryptIdentityRecord(this.crypted_record, this.username, this.password);
|
||||
if (user) {
|
||||
this.username = user;
|
||||
}
|
||||
if (errors) {
|
||||
this.errors.password = errors;
|
||||
}
|
||||
if (keypair) {
|
||||
this.keypair = keypair;
|
||||
}
|
||||
},
|
||||
async pasteClipboard() {
|
||||
try {
|
||||
this.raw_record = await navigator.clipboard.readText();
|
||||
this.try_parsing()
|
||||
} catch (e) {
|
||||
console.error('Error:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
try {
|
||||
fetch('/local/domains')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.domains = data;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.is-invalid input, .is-invalid select {
|
||||
border: 1px solid var(--bs-danger);
|
||||
}
|
||||
|
||||
.is-invalid .invalid-feedback {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input-group-text svg {
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -27,9 +27,7 @@
|
|||
type="text" v-model="form.username" id="validationCustomUsername"
|
||||
placeholder="Enter your username" required/>
|
||||
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text form-control form-control-lg">@</span>
|
||||
</div>
|
||||
<span class="input-group-text form-control-lg">@</span>
|
||||
<select class="form-control form-control-lg"
|
||||
id="exampleFormControlSelect1"
|
||||
placeholder="Domain" v-model="form.domain" required>
|
||||
|
|
@ -77,6 +75,10 @@
|
|||
Already have an account?
|
||||
<router-link to="/login">Login</router-link>
|
||||
</p>
|
||||
<p class="mb-0 text-muted">
|
||||
Connect with existing account?
|
||||
<router-link to="/pairing">Pairing</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,43 +2,32 @@
|
|||
<BaseLayout>
|
||||
<main class="content">
|
||||
<div class="container-fluid p-0">
|
||||
|
||||
<h1 class="h3 mb-3">Settings</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-xl-2">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Profile Settings</h5>
|
||||
</div>
|
||||
|
||||
<div class="list-group list-group-flush" role="tablist">
|
||||
<a class="list-group-item list-group-item-action" @click="active_tab = 'account'"
|
||||
:class="{ active: active_tab === 'account' }" href="#" role="tab">
|
||||
Account
|
||||
</a>
|
||||
<a class="list-group-item list-group-item-action" @click="active_tab = 'password'"
|
||||
:class="{ active: active_tab === 'password' }" href="#"
|
||||
role="tab">
|
||||
Password
|
||||
</a>
|
||||
<a class="list-group-item list-group-item-action" @click="active_tab = 'privacy'"
|
||||
:class="{ active: active_tab === 'privacy' }" href="#" role="tab">
|
||||
Privacy and safety
|
||||
</a>
|
||||
<a class="list-group-item list-group-item-action" @click="active_tab = 'email'"
|
||||
:class="{ active: active_tab === 'email' }" href="#" role="tab">
|
||||
Email notifications
|
||||
</a>
|
||||
<a class="list-group-item list-group-item-action" @click="active_tab = 'notifications'"
|
||||
:class="{ active: active_tab === 'notifications' }" href="#">
|
||||
Web notifications
|
||||
</a>
|
||||
<a class="list-group-item list-group-item-action" @click="active_tab = 'data'"
|
||||
:class="{ active: active_tab === 'data' }" href="#">
|
||||
Your data
|
||||
</a>
|
||||
<router-link class="list-group-item list-group-item-action" :to="{name: 'account'}"
|
||||
active-class="dummy" exact-active-class="active">Account
|
||||
</router-link>
|
||||
<router-link class="list-group-item list-group-item-action" :to="{name: 'password'}"
|
||||
active-class="active">Password
|
||||
</router-link>
|
||||
<router-link class="list-group-item list-group-item-action" :to="{name: 'privacy'}"
|
||||
active-class="active">Privacy and safety
|
||||
</router-link>
|
||||
<router-link class="list-group-item list-group-item-action" :to="{name: 'email'}"
|
||||
active-class="active">Email notifications
|
||||
</router-link>
|
||||
<router-link class="list-group-item list-group-item-action"
|
||||
:to="{name: 'notifications'}" active-class="active">Web notifications
|
||||
</router-link>
|
||||
<router-link class="list-group-item list-group-item-action" :to="{name: 'data'}"
|
||||
active-class="active">Your data
|
||||
</router-link>
|
||||
<a class="list-group-item list-group-item-action delete" @click="deleteAccount"
|
||||
href="#">
|
||||
Delete account
|
||||
|
|
@ -46,306 +35,12 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-9 col-xl-10">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade" :class="{ 'show active': active_tab === 'account' }" id="account"
|
||||
role="tabpanel">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
|
||||
<h5 class="card-title mb-0">Public info</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="mb-3">
|
||||
<label class="form-label"
|
||||
for="inputUsername">Username</label>
|
||||
<input type="text" class="form-control" id="inputUsername"
|
||||
placeholder="Username">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"
|
||||
for="inputUsername">Biography</label>
|
||||
<textarea rows="2" class="form-control" id="inputBio"
|
||||
placeholder="Tell something about yourself"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<img alt="Charles Hall"
|
||||
src="/assets/img/avatars/avatar.png"
|
||||
class="rounded-circle img-responsive mt-2" width="128"
|
||||
height="128"/>
|
||||
<div class="mt-2">
|
||||
<span class="btn btn-primary"><i
|
||||
class="fas fa-upload"></i> Upload</span>
|
||||
</div>
|
||||
<small>For best results, use an image at least 128px by
|
||||
128px in .jpg format</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
|
||||
<h5 class="card-title mb-0">Private info</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="row">
|
||||
<div class="mb-3 col-md-6">
|
||||
<label class="form-label" for="inputFirstName">First
|
||||
name</label>
|
||||
<input type="text" class="form-control" id="inputFirstName"
|
||||
placeholder="First name">
|
||||
</div>
|
||||
<div class="mb-3 col-md-6">
|
||||
<label class="form-label" for="inputLastName">Last name</label>
|
||||
<input type="text" class="form-control" id="inputLastName"
|
||||
placeholder="Last name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="inputEmail4">Email</label>
|
||||
<input type="email" class="form-control" id="inputEmail4"
|
||||
placeholder="Email">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="inputAddress">Address</label>
|
||||
<input type="text" class="form-control" id="inputAddress"
|
||||
placeholder="1234 Main St">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="inputAddress2">Address 2</label>
|
||||
<input type="text" class="form-control" id="inputAddress2"
|
||||
placeholder="Apartment, studio, or floor">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="mb-3 col-md-6">
|
||||
<label class="form-label" for="inputCity">City</label>
|
||||
<input type="text" class="form-control" id="inputCity">
|
||||
</div>
|
||||
<div class="mb-3 col-md-4">
|
||||
<label class="form-label" for="inputState">State</label>
|
||||
<select id="inputState" class="form-control">
|
||||
<option selected>Choose...</option>
|
||||
<option>...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3 col-md-2">
|
||||
<label class="form-label" for="inputZip">Zip</label>
|
||||
<input type="text" class="form-control" id="inputZip">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="tab-pane fade" :class="{ 'show active': active_tab === 'password' }"
|
||||
id="password" role="tabpanel">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Password</h5>
|
||||
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="inputPasswordCurrent">Current
|
||||
password</label>
|
||||
<input type="password" class="form-control"
|
||||
id="inputPasswordCurrent">
|
||||
<small><a href="#">Forgot your password?</a></small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="inputPasswordNew">New
|
||||
password</label>
|
||||
<input type="password" class="form-control" id="inputPasswordNew">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="inputPasswordNew2">Verify
|
||||
password</label>
|
||||
<input type="password" class="form-control" id="inputPasswordNew2">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" :class="{ 'show active': active_tab === 'privacy' }" id="privacy"
|
||||
role="tabpanel">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Privacy and safety</h5>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">Profile visibility</label>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check input-switch" type="checkbox"
|
||||
id="flexSwitchCheckDefault">
|
||||
<label class="form-check
|
||||
label-switch" for="flexSwitchCheckDefault">Visible to
|
||||
everyone</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label
|
||||
d-block">Photo visibility</label>
|
||||
<div class="form-check
|
||||
form-switch">
|
||||
<input class="form-check
|
||||
input-switch" type="checkbox"
|
||||
id="flexSwitchCheckDefault">
|
||||
<label class="form-check
|
||||
label-switch" for="flexSwitchCheckDefault">Visible to
|
||||
everyone</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label
|
||||
d-block">Last seen</label>
|
||||
<div class="form-check
|
||||
form-switch">
|
||||
<input class="form-check
|
||||
input-switch" type="checkbox"
|
||||
id="flexSwitchCheckDefault">
|
||||
<label class="form-check
|
||||
label-switch" for="flexSwitchCheckDefault">Visible to
|
||||
everyone</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save
|
||||
changes
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<small>Changes will be reflected to all your devices</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="tab-pane fade" :class="{ 'show active': active_tab === 'email' }" id="email"
|
||||
role="tabpanel">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title
|
||||
mb-0">Email notifications</h5>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label
|
||||
d-block">Email notifications</label>
|
||||
<div class="form-check
|
||||
form-switch">
|
||||
<input class="form-check
|
||||
input-switch" type="checkbox"
|
||||
id="flexSwitchCheckDefault">
|
||||
<label class="form-check
|
||||
label-switch" for="flexSwitchCheckDefault">Receive
|
||||
email notifications</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label
|
||||
d-block">Send email notifications to</label>
|
||||
<input type="email" class="form-control"
|
||||
id="inputEmail">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save
|
||||
changes
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" :class="{ 'show active': active_tab === 'notifications' }"
|
||||
id="notifications" role="tabpanel">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-0">Web notifications</h5>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">Web notifications</label>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check input-switch" type="checkbox"
|
||||
id="flexSwitchCheckDefault">
|
||||
<label class="form-check label-switch" for="flexSwitchCheckDefault">Receive
|
||||
web notifications</label>
|
||||
<small>These notifications can be sent to your email or
|
||||
phone</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">Send web notifications to</label>
|
||||
<input type="email" class="form-control"
|
||||
id="inputEmail">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" :class="{ 'show active': active_tab === 'data' }" id="data"
|
||||
role="tabpanel">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-0">Export</h5>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label
|
||||
d-block">Download your data</label>
|
||||
<button type="button" class="btn btn-primary">Download</button>
|
||||
<small>Download all your data including photos, videos,
|
||||
comments, profile information and more</small>
|
||||
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label
|
||||
d-block">Delete your data</label>
|
||||
<button type="button" class="btn btn-danger">Delete</button>
|
||||
<small>Delete all your data including photos, videos,
|
||||
comments, profile information and more</small>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-0">Import</h5>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label
|
||||
d-block">Import data</label>
|
||||
<button type="button" class="btn btn-primary">Import</button>
|
||||
<small>Import data from backup or other instances</small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</BaseLayout>
|
||||
|
|
@ -357,15 +52,12 @@ import BaseLayout from "@/components/BaseLayout.vue";
|
|||
export default {
|
||||
name: 'Settings',
|
||||
components: {BaseLayout},
|
||||
data: () => ({
|
||||
active_tab: "account"
|
||||
}),
|
||||
methods: {
|
||||
deleteAccount() {
|
||||
if (confirm('Are you sure you want to delete your account?')) {
|
||||
alert('Account deleted');
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
111
frontend/src/views/settings/Account.vue
Normal file
111
frontend/src/views/settings/Account.vue
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Public info</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="mb-3">
|
||||
<label class="form-label"
|
||||
for="inputUsername">Username</label>
|
||||
<input type="text" class="form-control" id="inputUsername"
|
||||
placeholder="Username">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"
|
||||
for="inputUsername">Biography</label>
|
||||
<textarea rows="2" class="form-control" id="inputBio"
|
||||
placeholder="Tell something about yourself"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<img alt="Charles Hall"
|
||||
src="/assets/img/avatars/avatar.png"
|
||||
class="rounded-circle img-responsive mt-2" width="128"
|
||||
height="128"/>
|
||||
<div class="mt-2">
|
||||
<span class="btn btn-primary"><i
|
||||
class="fas fa-upload"></i> Upload</span>
|
||||
</div>
|
||||
<small>For best results, use an image at least 128px by
|
||||
128px in .jpg format</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Private info</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="row">
|
||||
<div class="mb-3 col-md-6">
|
||||
<label class="form-label" for="inputFirstName">First
|
||||
name</label>
|
||||
<input type="text" class="form-control" id="inputFirstName"
|
||||
placeholder="First name">
|
||||
</div>
|
||||
<div class="mb-3 col-md-6">
|
||||
<label class="form-label" for="inputLastName">Last name</label>
|
||||
<input type="text" class="form-control" id="inputLastName"
|
||||
placeholder="Last name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="inputEmail4">Email</label>
|
||||
<input type="email" class="form-control" id="inputEmail4"
|
||||
placeholder="Email">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="inputAddress">Address</label>
|
||||
<input type="text" class="form-control" id="inputAddress"
|
||||
placeholder="1234 Main St">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="inputAddress2">Address 2</label>
|
||||
<input type="text" class="form-control" id="inputAddress2"
|
||||
placeholder="Apartment, studio, or floor">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="mb-3 col-md-6">
|
||||
<label class="form-label" for="inputCity">City</label>
|
||||
<input type="text" class="form-control" id="inputCity">
|
||||
</div>
|
||||
<div class="mb-3 col-md-4">
|
||||
<label class="form-label" for="inputState">State</label>
|
||||
<select id="inputState" class="form-control">
|
||||
<option selected>Choose...</option>
|
||||
<option>...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3 col-md-2">
|
||||
<label class="form-label" for="inputZip">Zip</label>
|
||||
<input type="text" class="form-control" id="inputZip">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Account',
|
||||
components: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
147
frontend/src/views/settings/Data.vue
Normal file
147
frontend/src/views/settings/Data.vue
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-0">Backup Account Key</h5>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">Backup your account key</label>
|
||||
<button type="button" class="btn btn-primary" @click="exportKey">
|
||||
Backup
|
||||
</button>
|
||||
<br>
|
||||
<small>Backup your account key to restore your account in case
|
||||
of loss</small>
|
||||
<br>
|
||||
<p>{{ localUserIdentityRecord }}</p>
|
||||
|
||||
<vue-qrcode :value="'foo'" tag="svg" :size="200" :options="{errorCorrectionLevel: 'H'}"></vue-qrcode>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-0">Export</h5>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label
|
||||
d-block">Download your data</label>
|
||||
<button type="button" class="btn btn-primary" @click="exportData">
|
||||
Download
|
||||
</button>
|
||||
<br>
|
||||
<small>Download all your data including photos, videos,
|
||||
comments, profile information and more</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label
|
||||
d-block">Delete your data</label>
|
||||
<button type="button" class="btn btn-danger" @click="deleteData">
|
||||
Delete
|
||||
</button>
|
||||
<br>
|
||||
<small>Delete all your data including photos, videos,
|
||||
comments, profile information and more</small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-0">Import</h5>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label
|
||||
d-block">Import data</label>
|
||||
<div class="btn-group">
|
||||
<input type="file" class="form-control" id="inputFile"
|
||||
accept=".zip" @change="chooseFile">
|
||||
<button type="button" class="btn btn-primary" @click="importData"
|
||||
:disabled="!selectedFile">
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
<br>
|
||||
<small>Import data from backup or other instances</small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapActions, mapGetters, mapMutations} from 'vuex';
|
||||
import router from "@/router";
|
||||
|
||||
//import VueQrcode from '@chenfengyuan/vue-qrcode';
|
||||
|
||||
export default {
|
||||
name: 'Data',
|
||||
components: {},
|
||||
data: () => ({
|
||||
selectedFile: null,
|
||||
localUserIdentityRecord: null
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters(['signAuth'])
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['getHomeServers', 'userIdentityRecord']),
|
||||
chooseFile(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
this.selectedFile = file;
|
||||
}
|
||||
},
|
||||
deleteData() {
|
||||
if (confirm('Are you sure you want to delete your data?')) {
|
||||
alert('Data deleted');
|
||||
}
|
||||
},
|
||||
exportKey() {
|
||||
const key = this.userIdentityRecord;
|
||||
},
|
||||
async importData() {
|
||||
if (!this.selectedFile) {
|
||||
alert('Please select a file to import');
|
||||
return;
|
||||
}
|
||||
alert('Data import not implemented');
|
||||
return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', this.selectedFile);
|
||||
console.log(formData);
|
||||
const servers = await this.getHomeServers();
|
||||
const data = await servers.postRaw(this.signHashedAuth, '/api/import/', formData);
|
||||
console.log(data);
|
||||
},
|
||||
async exportData() {
|
||||
const servers = await this.getHomeServers();
|
||||
const data = await servers.getRaw(this.signAuth, '/api/export/');
|
||||
const blob = new Blob([await data.blob()], {type: 'application/zip'});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const date_str = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '').replace(/:/g, '-');
|
||||
a.download = 'toolshed_data_' + date_str + '.zip';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.userIdentityRecord({}).then((data) => {
|
||||
this.localUserIdentityRecord = data;
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
41
frontend/src/views/settings/Email.vue
Normal file
41
frontend/src/views/settings/Email.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-0">Email notifications</h5>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">Email notifications</label>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check input-switch" type="checkbox"
|
||||
id="flexSwitchCheckDefault">
|
||||
<label class="form-check label-switch" for="flexSwitchCheckDefault">Receive
|
||||
email notifications</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">Send email notifications to</label>
|
||||
<input type="email" class="form-control"
|
||||
id="inputEmail">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save
|
||||
changes
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Email',
|
||||
components: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
42
frontend/src/views/settings/Notifications.vue
Normal file
42
frontend/src/views/settings/Notifications.vue
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-0">Web notifications</h5>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">Web notifications</label>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check input-switch" type="checkbox"
|
||||
id="flexSwitchCheckDefault">
|
||||
<label class="form-check label-switch" for="flexSwitchCheckDefault">Receive
|
||||
web notifications</label>
|
||||
<small>These notifications can be sent to your email or
|
||||
phone</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">Send web notifications to</label>
|
||||
<input type="email" class="form-control"
|
||||
id="inputEmail">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Notifications',
|
||||
components: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
41
frontend/src/views/settings/Password.vue
Normal file
41
frontend/src/views/settings/Password.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Password</h5>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="inputPasswordCurrent">Current
|
||||
password</label>
|
||||
<input type="password" class="form-control"
|
||||
id="inputPasswordCurrent">
|
||||
<small><a href="#">Forgot your password?</a></small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="inputPasswordNew">New
|
||||
password</label>
|
||||
<input type="password" class="form-control" id="inputPasswordNew">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="inputPasswordNew2">Verify
|
||||
password</label>
|
||||
<input type="password" class="form-control" id="inputPasswordNew2">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Password',
|
||||
components: {},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
68
frontend/src/views/settings/Privacy.vue
Normal file
68
frontend/src/views/settings/Privacy.vue
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Privacy and safety</h5>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">Profile visibility</label>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check input-switch" type="checkbox"
|
||||
id="flexSwitchCheckDefault">
|
||||
<label class="form-check
|
||||
label-switch" for="flexSwitchCheckDefault">Visible to
|
||||
everyone</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label
|
||||
d-block">Photo visibility</label>
|
||||
<div class="form-check
|
||||
form-switch">
|
||||
<input class="form-check
|
||||
input-switch" type="checkbox"
|
||||
id="flexSwitchCheckDefault">
|
||||
<label class="form-check
|
||||
label-switch" for="flexSwitchCheckDefault">Visible to
|
||||
everyone</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label
|
||||
d-block">Last seen</label>
|
||||
<div class="form-check
|
||||
form-switch">
|
||||
<input class="form-check
|
||||
input-switch" type="checkbox"
|
||||
id="flexSwitchCheckDefault">
|
||||
<label class="form-check
|
||||
label-switch" for="flexSwitchCheckDefault">Visible to
|
||||
everyone</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save
|
||||
changes
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<small>Changes will be reflected to all your devices</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapActions, mapGetters, mapMutations} from 'vuex';
|
||||
import router from "@/router";
|
||||
|
||||
export default {
|
||||
name: 'Privacy',
|
||||
components: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -19,11 +19,12 @@ export default defineConfig({
|
|||
'Access-Control-Allow-Headers': 'Origin, Content-Type, X-Auth-Token, Authorization, Accept, charset, boundary, Content-Length',
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
//'Upgrade-Insecure-Requests': '1',
|
||||
'Content-Security-Policy': 'default-src \'self\';'
|
||||
+ ' script-src \'self\' \'wasm-unsafe-eval\';'
|
||||
+ ' script-src \'self\' \'wasm-unsafe-eval\' \'unsafe-eval\';'
|
||||
+ ' style-src \'self\' \'unsafe-inline\';'
|
||||
+ ' img-src \'self\' https://* data:;'
|
||||
+ ' connect-src https://* data:',
|
||||
+ ' img-src \'self\' * data:;'
|
||||
+ ' connect-src * data:',
|
||||
},
|
||||
},
|
||||
test: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue