add /friends page

This commit is contained in:
j3d1 2024-04-09 17:19:38 +02:00
parent c49a40df01
commit 157e47d3ef
6 changed files with 293 additions and 10 deletions

View file

@ -9,6 +9,12 @@
<li class="sidebar-header">
Tools & Components
</li>
<li class="sidebar-item">
<router-link to="/friends" class="sidebar-link">
<b-icon-people class="bi-valign-middle"></b-icon-people>
<span class="align-middle">Friends</span>
</router-link>
</li>
</ul>
</div>
</nav>
@ -68,6 +74,19 @@ export default {
flex-grow: 1
}
.sidebar-link, a.sidebar-link {
display: block;
padding: .625rem 1.625rem;
font-weight: 400;
transition: background .1s ease-in-out;
position: relative;
text-decoration: none;
cursor: pointer;
color: #e9ecef80;
background: #222e3c;
border-left: 3px solid transparent;
}
.sidebar-link i, .sidebar-link svg, a.sidebar-link i, a.sidebar-link svg {
margin-right: .75rem;
color: rgba(233, 236, 239, .5)

View file

@ -1,29 +1,27 @@
import {createRouter, createWebHistory} from 'vue-router'
import Index from '@/views/Index.vue';
import Dashboard from '@/views/Dashboard.vue';
import Login from '@/views/Login.vue';
import Register from '@/views/Register.vue';
import store from '@/store';
import Friends from "@/views/Friends.vue";
const routes = [
{path: '/', component: Index, meta: {requiresAuth: true}},
{path: '/', component: Dashboard, meta: {requiresAuth: true}},
{path: '/friends', component: Friends, meta: {requiresAuth: true}},
{path: '/login', component: Login, meta: {requiresAuth: false}},
{path: '/register', component: Register, meta: {requiresAuth: false}},
]
const router = createRouter({
// 4. Provide the history implementation to use. We are using the hash history for simplicity here.
history: createWebHistory(),
linkActiveClass: "active",
routes, // short for `routes: routes`
routes,
})
router.beforeEach((to/*, from*/) => {
// instead of having to check every route record with
// to.matched.some(record => record.meta.requiresAuth)
// if this route requires auth, check if logged in, if not, redirect to login page.
if (to.meta.requiresAuth && !store.getters.isLoggedIn) {
// this route requires auth, check if logged in
// if not, redirect to login page.
console.log("Not logged in, redirecting to login page")
return {
path: '/login',

View file

@ -14,6 +14,7 @@ export default createStore({
keypair: null,
remember: false,
home_servers: null,
all_friends_servers: null,
resolver: new FallBackResolver(),
unreachable_neighbors: new NeighborsCache(),
},
@ -42,9 +43,15 @@ export default createStore({
}
localStorage.setItem('remember', remember);
},
setFriends(state, friends) {
state.friends = friends;
},
setHomeServers(state, home_servers) {
state.home_servers = home_servers;
},
setAllFriendsServers(state, servers) {
state.all_friends_servers = servers;
},
logout(state) {
state.user = null;
state.token = null;
@ -101,9 +108,79 @@ export default createStore({
commit('setHomeServers', promise)
return promise
},
async getAllKnownServers({state, dispatch, commit}) {
const friends = await dispatch('fetchFriends')
if (state.all_friends_servers)
return state.all_friends_servers
const promise = (async () => {
const servers = new ServerSetUnion([])
const home = await dispatch('getHomeServers')
servers.add(home)
for (const friend of friends) {
const s = await dispatch('lookupServer', {username: friend.username})
servers.add(new ServerSet(s, state.unreachable_neighbors))
}
return servers
})()
commit('setAllFriendsServers', promise)
return promise
},
async getFriendServers({state, dispatch, commit}, {username}) {
return dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors))
},
async fetchFriends({commit, dispatch, getters, state}) {
const servers = await dispatch('getHomeServers')
const data = await servers.get(getters.signAuth, '/api/friends/')
commit('setFriends', data)
return data
},
async fetchFriendRequests({state, dispatch, getters}) {
const servers = await dispatch('getHomeServers')
return await servers.get(getters.signAuth, '/api/friendrequests/')
},
async requestFriend({state, dispatch, getters}, {username}) {
if (username in state.friends) {
return true;
}
const home_servers = await dispatch('getHomeServers')
const home_reply = await home_servers.post(getters.signAuth, '/api/friendrequests/', {
befriender: state.user,
befriendee: username
})
if (home_reply.status !== 'pending' || !home_reply.secret)
return false;
const befriendee_servers = await dispatch('getFriendServers', {username})
const ext_reply = await befriendee_servers.post(getters.signAuth, '/api/friendrequests/', {
befriender: state.user,
befriendee: username,
befriender_key: nacl.to_hex(state.keypair.signPk),
secret: home_reply.secret
})
return true;
},
async acceptFriend({state, dispatch, getters}, {id, secret, befriender}) {
const home_servers = await dispatch('getHomeServers')
const home_reply = await home_servers.post(getters.signAuth, '/api/friends/', {
friend_request_id: id, secret: secret
})
const ext_servers = await dispatch('getFriendServers', {username: befriender})
const ext_reply = await ext_servers.post(getters.signAuth, '/api/friendrequests/', {
befriender: state.user,
befriendee: befriender,
befriender_key: nacl.to_hex(state.keypair.signPk),
secret: secret
})
return true
},
async declineFriend({state, dispatch, getters}, {id}) {
const servers = await dispatch('getHomeServers')
return await servers.delete(getters.signAuth, '/api/friendrequests/' + id + '/')
},
async dropFriend({state, dispatch, getters}, {id}) {
const servers = await dispatch('getHomeServers')
return await servers.delete(getters.signAuth, '/api/friends/' + id + '/')
},
},
getters: {
isLoggedIn(state) {

View file

@ -18,7 +18,7 @@ import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
export default {
name: 'Index',
name: 'Dashboard',
components: {
...BIcons,
BaseLayout

View file

@ -0,0 +1,190 @@
<template>
<BaseLayout>
<main class="content">
<div class="container-fluid p-0">
<h1 class="h3 mb-3">Friends</h1>
<div class="row">
<div class="col-12 col-xl-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">All Friends</h5>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th class="d-none d-md-table-cell" style="width:25%">Server</th>
<th style="width: 16em">
<a @click="fetchContent" class="align-middle">
<b-icon-arrow-clockwise></b-icon-arrow-clockwise>
Refresh
</a>
<a @click="showNewFriend" class="align-middle">
<b-icon-plus></b-icon-plus>
Add friend
</a>
</th>
</tr>
</thead>
<tbody>
<tr v-if="show_newfriend">
<td colspan="2">
<input type="text" class="form-control" placeholder="user@domain"
v-model="newfriend">
</td>
<td>
<button class="btn btn-primary" @click="tryRequestFriend">Send request</button>
</td>
</tr>
<tr v-for="friend in friendslist" :key="friend.name">
<td>{{ friend.username }}</td>
<td class="d-none d-md-table-cell">{{ friend.server.join(', ')}}</td>
<td class="table-action">
<!--a href="#" class="align-middle">
<b-icon-pencil-square></b-icon-pencil-square>
Edit
</a-->
<a href="#" class="align-middle" @click="tryDropFriend(friend)">
<b-icon-trash></b-icon-trash>
Delete
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-12 col-xl-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">Open friend requests</h5>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th class="d-none d-md-table-cell" style="width:25%">Key</th>
<th style="width: 16em">
<a @click="fetchContent" class="align-middle">
<b-icon-arrow-clockwise></b-icon-arrow-clockwise>
Refresh
</a>
</th>
</tr>
</thead>
<tbody>
<tr v-for="request in requests" :key="request.befriender">
<td>{{ request.befriender }}</td>
<td class="d-none d-md-table-cell">
{{ request.befriender_public_key.slice(0, 32) }}...
</td>
<td class="table-action">
<button class="btn btn-sm btn-success" @click="tryAcceptFriend(request)">
<b-icon-check></b-icon-check>
Accept
</button> &nbsp;
<button class="btn btn-sm btn-danger" @click="tryDeclineFriend(request)">
<b-icon-x></b-icon-x>
Decline
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
</BaseLayout>
</template>
<script>
import {mapActions, mapGetters} from "vuex";
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
export default {
name: 'Inventory',
components: {
BaseLayout,
...BIcons
},
data() {
return {
friends: {},
show_newfriend: false,
newfriend: "",
requests: []
}
},
computed: {
username() {
return this.$route.params.username
},
friendslist() {
return Object.entries(this.friends).map(([_, friend]) => friend)
}
},
methods: {
...mapActions(['fetchFriends', "lookupServer", "requestFriend", "acceptFriend", "fetchFriendRequests",
"declineFriend", "dropFriend"]),
fetchContent() {
this.fetchFriends().then((friends) => {
friends.map((friend) => {
this.lookupServer(friend).then((server) => {
this.friends[friend.username] = {...friend, server: server}
})
})
})
this.fetchFriendRequests().then((requests) => {
this.requests = requests
})
},
showNewFriend() {
this.show_newfriend = true
},
tryRequestFriend() {
this.requestFriend({username: this.newfriend}).then((ok) => {
if (ok) {
this.show_newfriend = false
this.newfriend = ""
this.fetchContent()
}
}).catch(() => {
})
},
tryAcceptFriend(request) {
this.acceptFriend({id: request.id, secret: request.secret, befriender: request.befriender}).then((ok) => {
if (ok) {
this.fetchContent()
}
}).catch(() => {
})
},
tryDropFriend(friend) {
this.dropFriend(friend).then((ok) => {
if (ok) {
delete this.friends[friend.username]
}
}).catch(() => {
})
},
tryDeclineFriend(request) {
this.declineFriend(request).then((ok) => {
if (ok) {
this.fetchContent()
}
}).catch(() => {
})
}
},
mounted() {
this.fetchContent()
}
}
</script>
<style scoped>
</style>

View file

@ -2,7 +2,6 @@ import {fileURLToPath, URL} from 'node:url'
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import * as fs from "fs";
export default defineConfig({
plugins: [vue()],