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">
|
||||
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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
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 vue from '@vitejs/plugin-vue'
|
||||
import * as fs from "fs";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
|
|
Loading…
Reference in a new issue