add /friends page
This commit is contained in:
parent
c49a40df01
commit
157e47d3ef
6 changed files with 293 additions and 10 deletions
|
@ -9,6 +9,12 @@
|
||||||
<li class="sidebar-header">
|
<li class="sidebar-header">
|
||||||
Tools & Components
|
Tools & Components
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -68,6 +74,19 @@ export default {
|
||||||
flex-grow: 1
|
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 {
|
.sidebar-link i, .sidebar-link svg, a.sidebar-link i, a.sidebar-link svg {
|
||||||
margin-right: .75rem;
|
margin-right: .75rem;
|
||||||
color: rgba(233, 236, 239, .5)
|
color: rgba(233, 236, 239, .5)
|
||||||
|
|
|
@ -1,29 +1,27 @@
|
||||||
import {createRouter, createWebHistory} from 'vue-router'
|
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 Login from '@/views/Login.vue';
|
||||||
import Register from '@/views/Register.vue';
|
import Register from '@/views/Register.vue';
|
||||||
import store from '@/store';
|
import store from '@/store';
|
||||||
|
import Friends from "@/views/Friends.vue";
|
||||||
|
|
||||||
|
|
||||||
const routes = [
|
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: '/login', component: Login, meta: {requiresAuth: false}},
|
||||||
{path: '/register', component: Register, meta: {requiresAuth: false}},
|
{path: '/register', component: Register, meta: {requiresAuth: false}},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
// 4. Provide the history implementation to use. We are using the hash history for simplicity here.
|
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
linkActiveClass: "active",
|
linkActiveClass: "active",
|
||||||
routes, // short for `routes: routes`
|
routes,
|
||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach((to/*, from*/) => {
|
router.beforeEach((to/*, from*/) => {
|
||||||
// instead of having to check every route record with
|
// if this route requires auth, check if logged in, if not, redirect to login page.
|
||||||
// to.matched.some(record => record.meta.requiresAuth)
|
|
||||||
if (to.meta.requiresAuth && !store.getters.isLoggedIn) {
|
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")
|
console.log("Not logged in, redirecting to login page")
|
||||||
return {
|
return {
|
||||||
path: '/login',
|
path: '/login',
|
||||||
|
|
|
@ -14,6 +14,7 @@ export default createStore({
|
||||||
keypair: null,
|
keypair: null,
|
||||||
remember: false,
|
remember: false,
|
||||||
home_servers: null,
|
home_servers: null,
|
||||||
|
all_friends_servers: null,
|
||||||
resolver: new FallBackResolver(),
|
resolver: new FallBackResolver(),
|
||||||
unreachable_neighbors: new NeighborsCache(),
|
unreachable_neighbors: new NeighborsCache(),
|
||||||
},
|
},
|
||||||
|
@ -42,9 +43,15 @@ export default createStore({
|
||||||
}
|
}
|
||||||
localStorage.setItem('remember', remember);
|
localStorage.setItem('remember', remember);
|
||||||
},
|
},
|
||||||
|
setFriends(state, friends) {
|
||||||
|
state.friends = friends;
|
||||||
|
},
|
||||||
setHomeServers(state, home_servers) {
|
setHomeServers(state, home_servers) {
|
||||||
state.home_servers = home_servers;
|
state.home_servers = home_servers;
|
||||||
},
|
},
|
||||||
|
setAllFriendsServers(state, servers) {
|
||||||
|
state.all_friends_servers = servers;
|
||||||
|
},
|
||||||
logout(state) {
|
logout(state) {
|
||||||
state.user = null;
|
state.user = null;
|
||||||
state.token = null;
|
state.token = null;
|
||||||
|
@ -101,9 +108,79 @@ export default createStore({
|
||||||
commit('setHomeServers', promise)
|
commit('setHomeServers', promise)
|
||||||
return 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}) {
|
async getFriendServers({state, dispatch, commit}, {username}) {
|
||||||
return dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors))
|
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: {
|
getters: {
|
||||||
isLoggedIn(state) {
|
isLoggedIn(state) {
|
||||||
|
|
|
@ -18,7 +18,7 @@ import * as BIcons from "bootstrap-icons-vue";
|
||||||
import BaseLayout from "@/components/BaseLayout.vue";
|
import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Index',
|
name: 'Dashboard',
|
||||||
components: {
|
components: {
|
||||||
...BIcons,
|
...BIcons,
|
||||||
BaseLayout
|
BaseLayout
|
190
frontend/src/views/Friends.vue
Normal file
190
frontend/src/views/Friends.vue
Normal 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>
|
||||||
|
<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>
|
|
@ -2,7 +2,6 @@ import {fileURLToPath, URL} from 'node:url'
|
||||||
|
|
||||||
import {defineConfig} from 'vite'
|
import {defineConfig} from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
|
|
Loading…
Reference in a new issue