This commit is contained in:
j3d1 2023-05-11 04:09:26 +02:00
parent a83083d0eb
commit 86003a8582
10 changed files with 20860 additions and 100 deletions

File diff suppressed because it is too large Load diff

Binary file not shown.

After

(image error) Size: 11 KiB

View file

@ -47,80 +47,7 @@
</form>
<div class="navbar-collapse collapse">
<ul class="navbar-nav navbar-align">
<li class="nav-item dropdown">
<a class="nav-icon dropdown-toggle" href="#" id="alertsDropdown" data-toggle="dropdown">
<div class="position-relative">
<b-icon-bell class="bi-valign-middle"></b-icon-bell>
<span class="indicator">4</span>
</div>
</a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right py-0"
aria-labelledby="alertsDropdown">
<div class="dropdown-menu-header">
4 New Notifications
</div>
<div class="list-group">
<a href="#" class="list-group-item">
<div class="row g-0 align-items-center">
<div class="col-2">
<b-icon-exclamation-circle
class="bi-valign-middle text-danger"></b-icon-exclamation-circle>
</div>
<div class="col-10">
<div class="text-dark">Update completed</div>
<div class="text-muted small mt-1">Restart server 12 to complete the
update.
</div>
<div class="text-muted small mt-1">30m ago</div>
</div>
</div>
</a>
<a href="#" class="list-group-item">
<div class="row g-0 align-items-center">
<div class="col-2">
<b-icon-bell class="bi-valign-middle text-warning"></b-icon-bell>
</div>
<div class="col-10">
<div class="text-dark">Lorem ipsum</div>
<div class="text-muted small mt-1">Aliquam ex eros, imperdiet vulputate
hendrerit
et.
</div>
<div class="text-muted small mt-1">2h ago</div>
</div>
</div>
</a>
<a href="#" class="list-group-item">
<div class="row g-0 align-items-center">
<div class="col-2">
<b-icon-house class="bi-valign-middle text-primary"></b-icon-house>
</div>
<div class="col-10">
<div class="text-dark">Login from 192.186.1.8</div>
<div class="text-muted small mt-1">5h ago</div>
</div>
</div>
</a>
<!--{% for notification in top_notifications %}
<a href="#" class="list-group-item">
<div class="row g-0 align-items-center">
<div class="col-2">
{% bs_icon 'person-add' extra_classes="bi-valign-middle text-success" %}
</div>
<div class="col-10">
<div class="text-dark">New connection</div>
<div class="text-muted small mt-1">Christina accepted your request.</div>
<div class="text-muted small mt-1">14h ago</div>
</div>
</div>
</a>
{% endfor %}-->
</div>
<div class="dropdown-menu-footer">
<a href="#" class="text-muted">Show all notifications</a>
</div>
</div>
</li>
<Notifications :notifications="notifications"/>
<li class="nav-item dropdown">
<a class="nav-icon dropdown-toggle" href="#" id="messagesDropdown" data-toggle="dropdown">
<div class="position-relative">
@ -268,18 +195,25 @@
</template>
<script>
import {mapGetters, mapMutations} from 'vuex';
import {mapGetters, mapMutations, mapState} from 'vuex';
import * as BIcons from "bootstrap-icons-vue";
import Notifications from "@/components/Notifications.vue";
export default {
name: 'BaseLayout',
components: {
Notifications,
...BIcons
},
computed: {
//...mapState(['notifications']),
...mapGetters(['notifications']),
username() {
return this.$route.params.username
}
},
top_notifications() {
return this.notifications.slice(0, 5)
},
},
methods: {
...mapMutations(['logout'])

View file

@ -0,0 +1,90 @@
<template>
<li class="nav-item dropdown">
<a class="nav-icon dropdown-toggle" href="#" id="alertsDropdown" data-toggle="dropdown">
<div class="position-relative">
<b-icon-bell class="bi-valign-middle"></b-icon-bell>
<span :class="['indicator', notificationsColor(top_notifications)]">{{
top_notifications.length
}}</span>
</div>
</a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right py-0"
aria-labelledby="alertsDropdown">
<div class="dropdown-menu-header">
{{ top_notifications.length }} New Notifications
</div>
<div class="list-group">
<a href="#" class="list-group-item" v-for="notification in top_notifications" :key="notification.id">
<div class="row g-0 align-items-center">
<div class="col-2">
<b-icon-exclamation-circle class="bi-valign-middle text-danger"
v-if="notification.type === 'error'"></b-icon-exclamation-circle>
<b-icon-info-circle class="bi-valign-middle text-info"
v-else-if="notification.type === 'info'"></b-icon-info-circle>
<b-icon-check-circle class="bi-valign-middle text-success"
v-else-if="notification.type === 'success'"></b-icon-check-circle>
<b-icon-bell class="bi-valign-middle text-warning"
v-else-if="notification.type === 'warning'"></b-icon-bell>
<b-icon-house class="bi-valign-middle text-primary"
v-else-if="notification.type === 'login'"></b-icon-house>
<b-icon-person-plus class="bi-valign-middle text-success"
v-else-if="notification.type === 'friend'"></b-icon-person-plus>
</div>
<div class="col-10">
<div class="text-dark">{{ notification.title }}</div>
<div class="text-muted small mt-1" v-if="notification.msg">{{ notification.msg }}</div>
<div class="text-muted small mt-1">{{ humanizeTime(notification.time) }}</div>
</div>
</div>
</a>
</div>
<div class="dropdown-menu-footer">
<a href="#" class="text-muted">Show all notifications</a>
</div>
</div>
</li>
</template>
<script>
import {mapGetters, mapMutations} from 'vuex';
import * as BIcons from "bootstrap-icons-vue";
export default {
name: 'Notifications',
components: {
...BIcons
},
props: ['notifications'],
computed: {
top_notifications() {
return this.notifications.sort((a, b) => {
return new Date(b.time) - new Date(a.time);
}).slice(0, 8);
},
},
methods: {
...mapMutations([]),
humanizeTime(time) {
return moment(time).fromNow();
},
notificationsColor(notifications) {
if (notifications.length === 0) {
return 'invisible';
}
if (notifications.filter(n => n.type === 'error').length > 0) {
return 'bg-danger';
}
if (notifications.filter(n => n.type === 'warning').length > 0) {
return 'bg-warning';
}
return 'bg-primary';
}
},
async mounted() {
}
}
</script>
<style scoped>
</style>

View file

@ -33,6 +33,15 @@ class NeighborsCache {
}
return false;
}
list() {
return Object.entries(this._cache).map(([domain, elem]) => {
return {
domain: domain,
time: elem.time
}
})
}
}
export default NeighborsCache;

View file

@ -12,16 +12,9 @@ export default createStore({
remember: false,
friends: [],
item_map: {},
//notifications: [],
resolver: new FallBackResolver(),
unreachable_neighbors: new NeighborsCache(),
/*wk: new Wellknown({
update: true, // Will load latest definitions from updateURL.
updateURL: new URL(), // URL to load the latest definitions. (default: project URL)
persist: false, // True to persist the loaded definitions (nodejs: in filesystem, browser: localStorage)
localStoragePrefix: 'dnsquery_', // Prefix for files persisted.
maxAge: 300000, // Max age of persisted data to be used in ms.
timeout: 5000 // Timeout when loading updates.
})*/
},
mutations: {
setUser(state, user) {
@ -105,7 +98,6 @@ export default createStore({
} else {
return false;
}
},
async getFriends(state) {
return ['jedi@j3d1.de', 'foobar@example.com', 'foobaz@example.eleon'];
@ -123,10 +115,11 @@ export default createStore({
(result) => result.map(
(answer) => answer.target + ':' + answer.port))
},
async apiFederatedGet({state}, url) {
if (state.unreachable_neighbors.queryUnreachable(url)) {
async apiFederatedGet({state}, {host, target}) {
if (state.unreachable_neighbors.queryUnreachable(host)) {
throw new Error('unreachable neighbor')
}
const url = host + target
const signature = nacl.crypto_sign_detached(nacl.encode_utf8(url), state.keypair.signSk)
const auth = 'Signature ' + state.user + ':' + nacl.to_hex(signature)
return await fetch(url, {
@ -134,13 +127,14 @@ export default createStore({
headers: {
'Authorization': auth
}
}).catch( err => state.unreachable_neighbors.unreachable(url)
}).catch( err => state.unreachable_neighbors.unreachable(host)
).then(response => response.json())
},
async apiFederatedPost({state}, {url, data}) {
if (state.unreachable_neighbors.queryUnreachable(url)) {
async apiFederatedPost({state}, {host, target, data}) {
if (state.unreachable_neighbors.queryUnreachable(host)) {
throw new Error('unreachable neighbor')
}
const url = host + target
const json = JSON.stringify(data)
const signature = nacl.crypto_sign_detached(nacl.encode_utf8(url + json), state.keypair.signSk)
const auth = 'Signature ' + state.user + ':' + nacl.to_hex(signature)
@ -151,7 +145,7 @@ export default createStore({
'Content-Type': 'application/json'
},
body: json
}).catch( err => state.unreachable_neighbors.unreachable(url)
}).catch( err => state.unreachable_neighbors.unreachable(host)
).then(response => response.json())
},
async apiLocalGet({state}, {target}) {
@ -181,6 +175,42 @@ export default createStore({
return Object.entries(state.item_map).reduce((acc, [url, items]) => {
return acc.concat(items)
}, [])
},
notifications(state) {
// supported types: error, warning, info, login, success, friend
const u = state.unreachable_neighbors.list().map(elem => {
return {
type: 'error',
title: elem.domain + ' unreachable',
msg: 'The neighbor ' + elem.domain + ' is currently unreachable. Please try again later.',
time: elem.time
}
})
return [...u, {
type: 'info',
title: 'Welcome to the Toolshed',
msg: 'This is a federated social network. You can add friends from other servers and share items with them.',
time: Date.now()
}, {
type: 'warning',
title: 'Lorem ipsum',
msg: 'Aliquam ex eros, imperdiet vulputate hendrerit et.',
time: Date.now() - 1000 * 60 * 60 * 2
}, {
type: 'login',
title: 'Login from 192.186.1.8',
time: Date.now() - 1000 * 60 * 60 * 5
}, {
type: 'friend',
title: 'New connection',
msg: 'Christina accepted your request.',
time: Date.now() - 1000 * 60 * 60 * 14
}, {
type: 'success',
title: 'Lorem ipsum',
msg: 'Aliquam ex eros, imperdiet vulputate hendrerit et.',
time: Date.now() - 1000 * 60 * 60 * 24
}]
},
}
})

View file

@ -72,9 +72,11 @@ export default {
async getInventoryItems() {
try {
const servers = await this.getFriends().then(friends => friends.map(friend => this.getFriendServer({username: friend})))
const urls = servers.map(server => server.then(s => `http://${s}/api/inventory_items/`))
const urls = servers.map(server => server.then(s => {
return {host: `http://${s}`, target: "/api/inventory_items/"}
}))
urls.map(url => url.then(u => this.apiFederatedGet(u).then(items => {
this.setInventoryItems({url: u, items})
this.setInventoryItems({url: u.domain, items})
}).catch(e => {
}))) // TODO: handle error
} catch (e) {

View file

@ -3,8 +3,8 @@
</script>
<template>
<BaseLayout :username="username">
<main class="container">
<BaseLayout>
<main class="content">
<div class="row">
<div class="col">
<div class="card">

View file

@ -3,8 +3,8 @@
</script>
<template>
<BaseLayout :username="username">
<main class="container">
<BaseLayout>
<main class="content">
<div class="row">
<div class="col">
<div class="card">

View file

@ -3,8 +3,8 @@
</script>
<template>
<BaseLayout :username="username">
<main class="container">
<BaseLayout>
<main class="content">
<div class="row">
<div class="col">
<div class="card">