stash
This commit is contained in:
parent
17bfb84a94
commit
2fe8d14c2c
35 changed files with 1976 additions and 2134 deletions
|
|
@ -1,5 +1,8 @@
|
||||||
# toolshed
|
# toolshed
|
||||||
|
|
||||||
|
|
||||||
|
## foo
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
|
|
@ -87,4 +90,4 @@ for detailed instructions see [docs](/docs/deployment.md).
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
cli-client/toolshed-client.py --key <hex private key> --user name@example.com --host 1.2.3.4:8000 getinventory
|
cli-client/toolshed-client.py --key <hex private key> --user name@example.com --host 1.2.3.4:8000 getinventory
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ urlpatterns = [
|
||||||
path('api/', include('toolshed.api.inventory')),
|
path('api/', include('toolshed.api.inventory')),
|
||||||
path('api/', include('toolshed.api.info')),
|
path('api/', include('toolshed.api.info')),
|
||||||
path('api/', include('toolshed.api.files')),
|
path('api/', include('toolshed.api.files')),
|
||||||
|
path('api/', include('toolshed.api.offlinedata')),
|
||||||
path('media/', include('files.media_urls')),
|
path('media/', include('files.media_urls')),
|
||||||
path('docs/', schema_view.with_ui('swagger', cache_timeout=0), name='api-docs'),
|
path('docs/', schema_view.with_ui('swagger', cache_timeout=0), name='api-docs'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
63
backend/toolshed/api/offlinedata.py
Normal file
63
backend/toolshed/api/offlinedata.py
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.urls import path
|
||||||
|
from rest_framework.decorators import api_view, permission_classes, authentication_classes
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from authentication.signature_auth import SignatureAuthentication
|
||||||
|
|
||||||
|
|
||||||
|
def user_data():
|
||||||
|
import io
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
zip_buffer = io.BytesIO()
|
||||||
|
|
||||||
|
with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
|
||||||
|
for file_name, data in [('1.txt', io.BytesIO(b'111')),
|
||||||
|
('2.txt', io.BytesIO(b'222'))]:
|
||||||
|
zip_file.writestr(file_name, data.getvalue())
|
||||||
|
|
||||||
|
return zip_buffer.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_user_data(data):
|
||||||
|
import io
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
with zipfile.ZipFile(io.BytesIO(data), "r") as zip_file:
|
||||||
|
for file_name in zip_file.namelist():
|
||||||
|
yield file_name, zip_file.read(file_name)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['POST'])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
@authentication_classes([SignatureAuthentication])
|
||||||
|
def import_data(request, format=None):
|
||||||
|
zip = request.data.get('zip')
|
||||||
|
if not zip:
|
||||||
|
return Response(status=400)
|
||||||
|
for file_name, data in parse_user_data(zip):
|
||||||
|
print(file_name, data)
|
||||||
|
return Response(status=200)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['GET'])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
@authentication_classes([SignatureAuthentication])
|
||||||
|
def export_data(request, format=None):
|
||||||
|
return HttpResponse(user_data(), content_type='application/zip', status=200)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['DELETE'])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
@authentication_classes([SignatureAuthentication])
|
||||||
|
def delete_account(request, format=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('export/', export_data, name='export_data'),
|
||||||
|
path('import/', import_data, name='import_data'),
|
||||||
|
path('account_data/', delete_account, name='delete_account'),
|
||||||
|
]
|
||||||
|
|
@ -8,6 +8,7 @@ WORKDIR /app
|
||||||
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
||||||
COPY package.json ./
|
COPY package.json ./
|
||||||
|
|
||||||
|
COPY extras/ ./extras/
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
CMD [ "npm", "run", "dev", "--", "--host"]
|
CMD [ "npm", "run", "dev", "--", "--host"]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
[
|
[
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"speed": 1,
|
||||||
|
"position": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"speed": 1,
|
||||||
|
"position": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"speed": 1,
|
||||||
|
"position": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
arr[1].speed
|
||||||
"a.localhost"
|
"a.localhost"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -21,4 +21,4 @@
|
||||||
"target": "127.0.0.2."
|
"target": "127.0.0.2."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
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": "^3.2.47",
|
||||||
"vue-multiselect": "^2.1.7",
|
"vue-multiselect": "^2.1.7",
|
||||||
"vue-router": "^4.1.6",
|
"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": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^4.0.0",
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
<h3 v-if="create">Create New</h3>
|
<h3 v-if="create">Create New</h3>
|
||||||
<h3 v-else>Update</h3>
|
<h3 v-else>Update</h3>
|
||||||
<ul>
|
<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 }}
|
{{ file.name }}
|
||||||
</li>
|
</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 }}
|
{{ file.name }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -100,23 +100,26 @@
|
||||||
<script>
|
<script>
|
||||||
import * as BIcons from "bootstrap-icons-vue";
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
import {mapActions, mapState} from "vuex";
|
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 AuthenticatedImage from "@/components/AuthenticatedImage.vue";
|
||||||
import DeletableWrapper from "@/components/DeletableWrapper.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 {
|
export default {
|
||||||
name: "CombinedFileField",
|
name: "CombinedFileField",
|
||||||
components: {
|
components: {
|
||||||
WebcamFileSource,
|
WebcamFileSource,
|
||||||
...BIcons,
|
|
||||||
AuthenticatedImage,
|
AuthenticatedImage,
|
||||||
DeletableWrapper,
|
DeletableWrapper,
|
||||||
DragDropFileSource,
|
DragDropFileSource,
|
||||||
CameraFileSource,
|
CameraFileSource,
|
||||||
FsFileSource
|
FsFileSource,
|
||||||
|
...BIcons
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
item_files: {
|
item_files: {
|
||||||
|
|
@ -182,7 +185,7 @@ export default {
|
||||||
only_images(files) {
|
only_images(files) {
|
||||||
return files.filter(file => file.mime_type.startsWith("image/"));
|
return files.filter(file => file.mime_type.startsWith("image/"));
|
||||||
},
|
},
|
||||||
whithout_images(files) {
|
without_images(files) {
|
||||||
return files.filter(file => !file.mime_type.startsWith("image/"));
|
return files.filter(file => !file.mime_type.startsWith("image/"));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@
|
||||||
import * as BIcons from "bootstrap-icons-vue";
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
import {mapActions, mapState} from "vuex";
|
import {mapActions, mapState} from "vuex";
|
||||||
import PropertyBadge from "@/components/PropertyBadge.vue";
|
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 {
|
export default {
|
||||||
name: "PropertyField",
|
name: "PropertyField",
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@
|
||||||
<script>
|
<script>
|
||||||
import * as BIcons from "bootstrap-icons-vue";
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
import {mapActions, mapState} from "vuex";
|
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 {
|
export default {
|
||||||
name: "TagField",
|
name: "TagField",
|
||||||
|
|
|
||||||
|
|
@ -33,16 +33,13 @@ class ServerSet {
|
||||||
}
|
}
|
||||||
const url = "https://" + server + target // TODO https
|
const url = "https://" + server + target // TODO https
|
||||||
return await fetch(url, {
|
return await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET', headers: {
|
||||||
headers: {
|
|
||||||
...auth.buildAuthHeader(url)
|
...auth.buildAuthHeader(url)
|
||||||
},
|
}, credentials: 'omit'
|
||||||
credentials: 'omit'
|
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error('get from server failed', server, err)
|
console.error('get from server failed', server, err)
|
||||||
this.unreachable_neighbors.unreachable(server)
|
this.unreachable_neighbors.unreachable(server)
|
||||||
}
|
}).then(response => response.json())
|
||||||
).then(response => response.json())
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('get from server failed', server, e)
|
console.error('get from server failed', server, e)
|
||||||
}
|
}
|
||||||
|
|
@ -61,18 +58,13 @@ class ServerSet {
|
||||||
}
|
}
|
||||||
const url = "https://" + server + target // TODO https
|
const url = "https://" + server + target // TODO https
|
||||||
return await fetch(url, {
|
return await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST', headers: {
|
||||||
headers: {
|
'Content-Type': 'application/json', ...auth.buildAuthHeader(url, data)
|
||||||
'Content-Type': 'application/json',
|
}, credentials: 'omit', body: JSON.stringify(data)
|
||||||
...auth.buildAuthHeader(url, data)
|
|
||||||
},
|
|
||||||
credentials: 'omit',
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error('post to server failed', server, err)
|
console.error('post to server failed', server, err)
|
||||||
this.unreachable_neighbors.unreachable(server)
|
this.unreachable_neighbors.unreachable(server)
|
||||||
}
|
}).then(response => response.json())
|
||||||
).then(response => response.json())
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('post to server failed', server, e)
|
console.error('post to server failed', server, e)
|
||||||
}
|
}
|
||||||
|
|
@ -91,18 +83,13 @@ class ServerSet {
|
||||||
}
|
}
|
||||||
const url = "https://" + server + target // TODO https
|
const url = "https://" + server + target // TODO https
|
||||||
return await fetch(url, {
|
return await fetch(url, {
|
||||||
method: 'PATCH',
|
method: 'PATCH', headers: {
|
||||||
headers: {
|
'Content-Type': 'application/json', ...auth.buildAuthHeader(url, data)
|
||||||
'Content-Type': 'application/json',
|
}, credentials: 'omit', body: JSON.stringify(data)
|
||||||
...auth.buildAuthHeader(url, data)
|
|
||||||
},
|
|
||||||
credentials: 'omit',
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error('patch to server failed', server, err)
|
console.error('patch to server failed', server, err)
|
||||||
this.unreachable_neighbors.unreachable(server)
|
this.unreachable_neighbors.unreachable(server)
|
||||||
}
|
}).then(response => response.json())
|
||||||
).then(response => response.json())
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('patch to server failed', server, e)
|
console.error('patch to server failed', server, e)
|
||||||
}
|
}
|
||||||
|
|
@ -121,18 +108,13 @@ class ServerSet {
|
||||||
}
|
}
|
||||||
const url = "https://" + server + target // TODO https
|
const url = "https://" + server + target // TODO https
|
||||||
return await fetch(url, {
|
return await fetch(url, {
|
||||||
method: 'PUT',
|
method: 'PUT', headers: {
|
||||||
headers: {
|
'Content-Type': 'application/json', ...auth.buildAuthHeader(url, data)
|
||||||
'Content-Type': 'application/json',
|
}, credentials: 'omit', body: JSON.stringify(data)
|
||||||
...auth.buildAuthHeader(url, data)
|
|
||||||
},
|
|
||||||
credentials: 'omit',
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error('put to server failed', server, err)
|
console.error('put to server failed', server, err)
|
||||||
this.unreachable_neighbors.unreachable(server)
|
this.unreachable_neighbors.unreachable(server)
|
||||||
}
|
}).then(response => response.json())
|
||||||
).then(response => response.json())
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('put to server failed', server, e)
|
console.error('put to server failed', server, e)
|
||||||
}
|
}
|
||||||
|
|
@ -151,16 +133,13 @@ class ServerSet {
|
||||||
}
|
}
|
||||||
const url = "https://" + server + target // TODO https
|
const url = "https://" + server + target // TODO https
|
||||||
return await fetch(url, {
|
return await fetch(url, {
|
||||||
method: 'DELETE',
|
method: 'DELETE', headers: {
|
||||||
headers: {
|
|
||||||
...auth.buildAuthHeader(url)
|
...auth.buildAuthHeader(url)
|
||||||
},
|
}, credentials: 'omit'
|
||||||
credentials: 'omit'
|
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error('delete from server failed', server, err)
|
console.error('delete from server failed', server, err)
|
||||||
this.unreachable_neighbors.unreachable(server)
|
this.unreachable_neighbors.unreachable(server)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('delete from server failed', server, e)
|
console.error('delete from server failed', server, e)
|
||||||
}
|
}
|
||||||
|
|
@ -179,102 +158,159 @@ class ServerSet {
|
||||||
}
|
}
|
||||||
const url = "https://" + server + target // TODO https
|
const url = "https://" + server + target // TODO https
|
||||||
return await fetch(url, {
|
return await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET', headers: {
|
||||||
headers: {
|
|
||||||
...auth.buildAuthHeader(url)
|
...auth.buildAuthHeader(url)
|
||||||
},
|
}, credentials: 'omit'
|
||||||
credentials: 'omit'
|
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error('get from server failed', server, err)
|
console.error('get from server failed', server, err)
|
||||||
this.unreachable_neighbors.unreachable(server)
|
this.unreachable_neighbors.unreachable(server)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('get from server failed', server, e)
|
console.error('get from server failed', server, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error('all servers failed')
|
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 {
|
//class ServerSetUnion {
|
||||||
constructor(serverSets) {
|
// constructor(serverSets) {
|
||||||
if (!serverSets || !Array.isArray(serverSets)) {
|
// if (!serverSets || !Array.isArray(serverSets)) {
|
||||||
throw new Error('no serverSets')
|
// throw new Error('no serverSets')
|
||||||
}
|
// }
|
||||||
this.serverSets = 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) {
|
function ServerSetUnion(serverSets) {
|
||||||
try {
|
return new Proxy(serverSets, {
|
||||||
return await this.serverSets.reduce(async (acc, serverset) => {
|
get: function (target, prop, receiver) {
|
||||||
return acc.then(async (acc) => {
|
const funcs = Object.getOwnPropertyNames(ServerSet.prototype)
|
||||||
return acc.concat(await serverset.get(auth, target))
|
if (funcs.includes(prop)) {
|
||||||
})
|
return async function (...args) {
|
||||||
}, Promise.resolve([]))
|
try {
|
||||||
} catch (e) {
|
return await target.reduce(async (acc, serverset) => {
|
||||||
throw new Error('all servers failed')
|
return acc.then(async (acc) => {
|
||||||
}
|
return acc.concat(await serverset[prop](...args))
|
||||||
}
|
})
|
||||||
|
}, Promise.resolve([]))
|
||||||
async post(auth, target, data) {
|
} catch (e) {
|
||||||
try {
|
throw new Error('all servers failed')
|
||||||
return await this.serverSets.reduce(async (acc, serverset) => {
|
}
|
||||||
return acc.then(async (acc) => {
|
}
|
||||||
return acc.concat(await serverset.post(auth, target, data))
|
} else if (prop === 'add') {
|
||||||
})
|
return function (serverset) {
|
||||||
}, Promise.resolve([]))
|
if (!serverset || !(serverset instanceof ServerSet)) {
|
||||||
} catch (e) {
|
throw new Error('no serverset')
|
||||||
throw new Error('all servers failed')
|
}
|
||||||
}
|
if (target.find(s => serverset.servers.every(s2 => s.servers.includes(s2)))) {
|
||||||
}
|
console.warn('serverset already in union', serverset)
|
||||||
|
return
|
||||||
async patch(auth, target, data) {
|
}
|
||||||
try {
|
target.push(serverset)
|
||||||
return await this.serverSets.reduce(async (acc, serverset) => {
|
}
|
||||||
return acc.then(async (acc) => {
|
} else {
|
||||||
return acc.concat(await serverset.patch(auth, target, data))
|
return target[prop]
|
||||||
})
|
}
|
||||||
}, 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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
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 {createApp} from 'vue'
|
||||||
|
|
||||||
import {BootstrapIconsPlugin} from 'bootstrap-icons-vue';
|
import {BootstrapIconsPlugin} from 'bootstrap-icons-vue';
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
|
|
@ -16,6 +17,8 @@ _nacl.instantiate((nacl) => {
|
||||||
app.use(router).mount('#app')
|
app.use(router).mount('#app')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//app.component(VueQrcode.name, VueQrcode);
|
||||||
|
|
||||||
window.closeAllDropdowns = function () {
|
window.closeAllDropdowns = function () {
|
||||||
const dropdowns = document.getElementsByClassName("dropdown-menu");
|
const dropdowns = document.getElementsByClassName("dropdown-menu");
|
||||||
let i;
|
let i;
|
||||||
|
|
@ -27,6 +30,7 @@ window.closeAllDropdowns = function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
window.onclick = function (event) {
|
window.onclick = function (event) {
|
||||||
if (!event.target.matches('.dropdown-toggle *')
|
if (!event.target.matches('.dropdown-toggle *')
|
||||||
&& !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 *')
|
||||||
&& !event.target.matches('.sidebar')) {
|
&& !event.target.matches('.sidebar')) {
|
||||||
const sidebar = document.getElementById("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) {
|
if (sidebar.classList.contains('collapsed') && marginLeft === 0) {
|
||||||
sidebar.classList.remove('collapsed');
|
sidebar.classList.remove('collapsed');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import {createRouter, createWebHistory} from 'vue-router'
|
||||||
import Dashboard from '@/views/Dashboard.vue';
|
import Dashboard from '@/views/Dashboard.vue';
|
||||||
import Login from '@/views/Login.vue';
|
import Login from '@/views/Login.vue';
|
||||||
import Register from '@/views/Register.vue';
|
import Register from '@/views/Register.vue';
|
||||||
|
import Pairing from '@/views/Pairing.vue';
|
||||||
import store from '@/store';
|
import store from '@/store';
|
||||||
import Profile from '@/views/Profile.vue';
|
import Profile from '@/views/Profile.vue';
|
||||||
import Settings from '@/views/Settings.vue';
|
import Settings from '@/views/Settings.vue';
|
||||||
|
|
@ -15,29 +16,67 @@ import Admin from '@/views/Admin.vue';
|
||||||
import Swatch from '@/views/Swatch.vue';
|
import Swatch from '@/views/Swatch.vue';
|
||||||
import Files from '@/views/Files.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}},
|
const routes = [{path: '/', component: Dashboard, meta: {requiresAuth: true}}, {
|
||||||
{path: '/profile', component: Profile, meta: {requiresAuth: true}},
|
path: '/inventory',
|
||||||
{path: '/settings', component: Settings, meta: {requiresAuth: true}},
|
component: Inventory,
|
||||||
{path: '/inventory', component: Inventory, meta: {requiresAuth: true}},
|
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/:id',
|
||||||
{path: '/inventory/new', component: InventoryNew, meta: {requiresAuth: true}},
|
component: InventoryDetail,
|
||||||
{path: '/friends', component: Friends, meta: {requiresAuth: true}},
|
meta: {requiresAuth: true},
|
||||||
{path: '/files', component: Files, meta: {requiresAuth: true}},
|
props: true
|
||||||
{path: '/admin', component: Admin, meta: {requiresAuth: true}},
|
}, {
|
||||||
{path: '/swatch', component: Swatch, meta: {requiresAuth: true}},
|
path: '/inventory/:id/edit',
|
||||||
{path: '/search/:query', component: Search, meta: {requiresAuth: true}, props: true},
|
component: InventoryEdit,
|
||||||
{path: '/login', component: Login, meta: {requiresAuth: false}},
|
meta: {requiresAuth: true},
|
||||||
{path: '/register', component: Register, meta: {requiresAuth: false}},
|
props: true
|
||||||
{path: '/:pathMatch(.*)*', redirect: '/'}
|
}, {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({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(), linkActiveClass: "active", routes,
|
||||||
linkActiveClass: "active",
|
|
||||||
routes,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach((to/*, from*/) => {
|
router.beforeEach((to/*, from*/) => {
|
||||||
|
|
@ -45,8 +84,7 @@ router.beforeEach((to/*, from*/) => {
|
||||||
if (to.meta.requiresAuth && !store.getters.isLoggedIn) {
|
if (to.meta.requiresAuth && !store.getters.isLoggedIn) {
|
||||||
console.log("Not logged in, redirecting to login page")
|
console.log("Not logged in, redirecting to login page")
|
||||||
return {
|
return {
|
||||||
path: '/login',
|
path: '/login', // save the location we were at to come back later
|
||||||
// save the location we were at to come back later
|
|
||||||
query: {redirect: to.fullPath},
|
query: {redirect: to.fullPath},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,5 +72,9 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: .25rem .7rem;
|
padding: .25rem .7rem;
|
||||||
background-color: map-get($theme-colors, background-2);
|
background-color: map-get($theme-colors, background-2);
|
||||||
|
#border-right-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-text:not(:last-child) {
|
||||||
border-right-width: 0;
|
border-right-width: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,9 @@ import router from '@/router';
|
||||||
import FallBackResolver from "@/dns";
|
import FallBackResolver from "@/dns";
|
||||||
import NeighborsCache from "@/neigbors";
|
import NeighborsCache from "@/neigbors";
|
||||||
import {createNullAuth, createSignAuth, createTokenAuth, ServerSet, ServerSetUnion} from "@/federation";
|
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({
|
export default createStore({
|
||||||
|
|
@ -29,30 +32,6 @@ export default createStore({
|
||||||
storage_locations: [],
|
storage_locations: [],
|
||||||
},
|
},
|
||||||
mutations: {
|
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}) {
|
setInventoryItems(state, {url, items}) {
|
||||||
state.item_map[url] = items;
|
state.item_map[url] = items;
|
||||||
},
|
},
|
||||||
|
|
@ -86,6 +65,45 @@ export default createStore({
|
||||||
setFiles(state, files) {
|
setFiles(state, files) {
|
||||||
state.files = 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) {
|
logout(state) {
|
||||||
state.user = null;
|
state.user = null;
|
||||||
state.token = null;
|
state.token = null;
|
||||||
|
|
@ -357,6 +375,9 @@ export default createStore({
|
||||||
state.last_load.categories = Date.now()
|
state.last_load.categories = Date.now()
|
||||||
state.last_load.availability_policies = Date.now()
|
state.last_load.availability_policies = Date.now()
|
||||||
return data
|
return data
|
||||||
|
},
|
||||||
|
async userIdentityRecord({state}, {password}) {
|
||||||
|
return await serializeIdentityRecord(state.user, state.keypair, password);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
|
|
@ -371,7 +392,7 @@ export default createStore({
|
||||||
state.local_loaded = true
|
state.local_loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.user !== null && state.token !== null;
|
return state.user !== null && (state.token !== null || state.keypair !== null)
|
||||||
},
|
},
|
||||||
signAuth(state) {
|
signAuth(state) {
|
||||||
return createSignAuth(state.user, state.keypair.signSk)
|
return createSignAuth(state.user, state.keypair.signSk)
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,10 @@
|
||||||
Don’t have an account?
|
Don’t have an account?
|
||||||
<router-link to="/register">Sign up</router-link>
|
<router-link to="/register">Sign up</router-link>
|
||||||
</p>
|
</p>
|
||||||
|
<p class="mb-0 text-muted">
|
||||||
|
Connect with existing account?
|
||||||
|
<router-link to="/pairing">Pairing</router-link>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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"
|
type="text" v-model="form.username" id="validationCustomUsername"
|
||||||
placeholder="Enter your username" required/>
|
placeholder="Enter your username" required/>
|
||||||
|
|
||||||
<div class="input-group-prepend">
|
<span class="input-group-text form-control-lg">@</span>
|
||||||
<span class="input-group-text form-control form-control-lg">@</span>
|
|
||||||
</div>
|
|
||||||
<select class="form-control form-control-lg"
|
<select class="form-control form-control-lg"
|
||||||
id="exampleFormControlSelect1"
|
id="exampleFormControlSelect1"
|
||||||
placeholder="Domain" v-model="form.domain" required>
|
placeholder="Domain" v-model="form.domain" required>
|
||||||
|
|
@ -77,6 +75,10 @@
|
||||||
Already have an account?
|
Already have an account?
|
||||||
<router-link to="/login">Login</router-link>
|
<router-link to="/login">Login</router-link>
|
||||||
</p>
|
</p>
|
||||||
|
<p class="mb-0 text-muted">
|
||||||
|
Connect with existing account?
|
||||||
|
<router-link to="/pairing">Pairing</router-link>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,43 +2,32 @@
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<main class="content">
|
<main class="content">
|
||||||
<div class="container-fluid p-0">
|
<div class="container-fluid p-0">
|
||||||
|
|
||||||
<h1 class="h3 mb-3">Settings</h1>
|
<h1 class="h3 mb-3">Settings</h1>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-3 col-xl-2">
|
<div class="col-md-3 col-xl-2">
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0">Profile Settings</h5>
|
<h5 class="card-title mb-0">Profile Settings</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="list-group list-group-flush" role="tablist">
|
<div class="list-group list-group-flush" role="tablist">
|
||||||
<a class="list-group-item list-group-item-action" @click="active_tab = 'account'"
|
<router-link class="list-group-item list-group-item-action" :to="{name: 'account'}"
|
||||||
:class="{ active: active_tab === 'account' }" href="#" role="tab">
|
active-class="dummy" exact-active-class="active">Account
|
||||||
Account
|
</router-link>
|
||||||
</a>
|
<router-link class="list-group-item list-group-item-action" :to="{name: 'password'}"
|
||||||
<a class="list-group-item list-group-item-action" @click="active_tab = 'password'"
|
active-class="active">Password
|
||||||
:class="{ active: active_tab === 'password' }" href="#"
|
</router-link>
|
||||||
role="tab">
|
<router-link class="list-group-item list-group-item-action" :to="{name: 'privacy'}"
|
||||||
Password
|
active-class="active">Privacy and safety
|
||||||
</a>
|
</router-link>
|
||||||
<a class="list-group-item list-group-item-action" @click="active_tab = 'privacy'"
|
<router-link class="list-group-item list-group-item-action" :to="{name: 'email'}"
|
||||||
:class="{ active: active_tab === 'privacy' }" href="#" role="tab">
|
active-class="active">Email notifications
|
||||||
Privacy and safety
|
</router-link>
|
||||||
</a>
|
<router-link class="list-group-item list-group-item-action"
|
||||||
<a class="list-group-item list-group-item-action" @click="active_tab = 'email'"
|
:to="{name: 'notifications'}" active-class="active">Web notifications
|
||||||
:class="{ active: active_tab === 'email' }" href="#" role="tab">
|
</router-link>
|
||||||
Email notifications
|
<router-link class="list-group-item list-group-item-action" :to="{name: 'data'}"
|
||||||
</a>
|
active-class="active">Your data
|
||||||
<a class="list-group-item list-group-item-action" @click="active_tab = 'notifications'"
|
</router-link>
|
||||||
: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>
|
|
||||||
<a class="list-group-item list-group-item-action delete" @click="deleteAccount"
|
<a class="list-group-item list-group-item-action delete" @click="deleteAccount"
|
||||||
href="#">
|
href="#">
|
||||||
Delete account
|
Delete account
|
||||||
|
|
@ -46,306 +35,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-9 col-xl-10">
|
<div class="col-md-9 col-xl-10">
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="tab-pane fade" :class="{ 'show active': active_tab === 'account' }" id="account"
|
<router-view></router-view>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
@ -357,15 +52,12 @@ import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
export default {
|
export default {
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
components: {BaseLayout},
|
components: {BaseLayout},
|
||||||
data: () => ({
|
|
||||||
active_tab: "account"
|
|
||||||
}),
|
|
||||||
methods: {
|
methods: {
|
||||||
deleteAccount() {
|
deleteAccount() {
|
||||||
if (confirm('Are you sure you want to delete your account?')) {
|
if (confirm('Are you sure you want to delete your account?')) {
|
||||||
alert('Account deleted');
|
alert('Account deleted');
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</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-Headers': 'Origin, Content-Type, X-Auth-Token, Authorization, Accept, charset, boundary, Content-Length',
|
||||||
'Access-Control-Allow-Credentials': 'true',
|
'Access-Control-Allow-Credentials': 'true',
|
||||||
'Access-Control-Max-Age': '86400',
|
'Access-Control-Max-Age': '86400',
|
||||||
|
//'Upgrade-Insecure-Requests': '1',
|
||||||
'Content-Security-Policy': 'default-src \'self\';'
|
'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\';'
|
+ ' style-src \'self\' \'unsafe-inline\';'
|
||||||
+ ' img-src \'self\' https://* data:;'
|
+ ' img-src \'self\' * data:;'
|
||||||
+ ' connect-src https://* data:',
|
+ ' connect-src * data:',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue