mirror of
https://github.com/h44z/wg-portal.git
synced 2025-08-09 15:02:24 +00:00
user/interface deletion, wip profile, wip peer endpoints
This commit is contained in:
parent
bd89258e81
commit
9a5efdc627
@ -72,9 +72,7 @@ const languageFlag = computed(() => {
|
||||
<a aria-expanded="false" aria-haspopup="true" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#"
|
||||
role="button">{{ auth.User.Firstname }} {{ auth.User.Lastname }}</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="/user/profile">
|
||||
<i class="fas fa-user"></i> {{ $t('menu.profile') }}
|
||||
</a>
|
||||
<RouterLink :to="{ name: 'profile' }" class="dropdown-item"><i class="fas fa-user"></i> {{ $t('menu.profile') }}</RouterLink>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" @click.prevent="auth.Logout">
|
||||
<i class="fas fa-sign-out-alt"></i> {{ $t('menu.logout') }}
|
||||
|
@ -441,7 +441,7 @@ async function del() {
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interfaceedit.defaults.endpoint') }}</label>
|
||||
<input v-model="formData.PeerDefEndpoint" class="form-control" placeholder="Endpoint Addresses" type="text">
|
||||
<small class="form-text text-muted">Peers will get IP addresses from those subnets.</small>
|
||||
<small class="form-text text-muted">The endpoint address that peers will connect to.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interfaceedit.defaults.networks') }}</label>
|
||||
|
@ -54,13 +54,6 @@ const title = computed(() => {
|
||||
|
||||
const formData = ref(freshFormData())
|
||||
|
||||
const notificationData = ref({
|
||||
title: "",
|
||||
content: "",
|
||||
cause: "",
|
||||
type: "normal",
|
||||
})
|
||||
|
||||
|
||||
function freshFormData() {
|
||||
return {
|
||||
@ -141,30 +134,25 @@ function freshFormData() {
|
||||
|
||||
watch(() => props.visible, async (newValue, oldValue) => {
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
console.log(selectedInterface.value)
|
||||
console.log(selectedPeer.value)
|
||||
if (!selectedPeer.value) {
|
||||
await loadNewPeerData()
|
||||
await peers.PreparePeer(selectedInterface.value.Identifier)
|
||||
|
||||
formData.value.Disabled = peers.Prepared.Disabled
|
||||
formData.value.Identifier = peers.Prepared.Identifier
|
||||
formData.value.DisplayName = peers.Prepared.DisplayName
|
||||
|
||||
} else { // fill existing data
|
||||
formData.value.Disabled = selectedPeer.value.Disabled
|
||||
formData.value.Identifier = selectedPeer.value.Identifier
|
||||
formData.value.DisplayName = selectedPeer.value.DisplayName
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async function loadNewPeerData() {
|
||||
console.log("loading new peer data...")
|
||||
notify({
|
||||
title: "Authorization",
|
||||
text: "You have been logged in!",
|
||||
})
|
||||
notify({
|
||||
title: "Authorization2",
|
||||
text: "You have been logged in!",
|
||||
})
|
||||
notify({
|
||||
title: "Authorization3",
|
||||
text: "You have been logged in!",
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function close() {
|
||||
formData.value = freshFormData()
|
||||
emit('close')
|
||||
|
@ -90,8 +90,8 @@ async function save() {
|
||||
close()
|
||||
} catch (e) {
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to save user!",
|
||||
title: "Failed to save user!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
@ -103,8 +103,8 @@ async function del() {
|
||||
close()
|
||||
} catch (e) {
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to delete user!",
|
||||
title: "Failed to delete user!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
@ -39,6 +39,14 @@ const router = createRouter({
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/UserView.vue')
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'profile',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/ProfileView.vue')
|
||||
}
|
||||
],
|
||||
linkActiveClass: "active",
|
||||
|
@ -63,7 +63,7 @@ export const interfaceStore = defineStore({
|
||||
})
|
||||
})
|
||||
},
|
||||
async InterfaceConfig(id) {
|
||||
async InterfaceConfig(id) {
|
||||
return apiWrapper.get(`${baseUrl}/config/${id}`)
|
||||
.then(this.setInterfaceConfig)
|
||||
.catch(error => {
|
||||
@ -74,7 +74,7 @@ export const interfaceStore = defineStore({
|
||||
text: "Failed to load interface configuration!",
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
async DeleteInterface(id) {
|
||||
this.fetching = true
|
||||
return apiWrapper.delete(`${baseUrl}/${id}`)
|
||||
|
@ -9,6 +9,9 @@ export const peerStore = defineStore({
|
||||
id: 'peers',
|
||||
state: () => ({
|
||||
peers: [],
|
||||
prepared: {
|
||||
Identifier: "",
|
||||
},
|
||||
filter: "",
|
||||
pageSize: 10,
|
||||
pageOffset: 0,
|
||||
@ -20,6 +23,7 @@ export const peerStore = defineStore({
|
||||
return (id) => state.peers.find((p) => p.Identifier === id)
|
||||
},
|
||||
Count: (state) => state.peers.length,
|
||||
Prepared: (state) => {console.log("STATE:", state.prepared); return state.prepared},
|
||||
FilteredCount: (state) => state.Filtered.length,
|
||||
All: (state) => state.peers,
|
||||
Filtered: (state) => {
|
||||
@ -71,6 +75,21 @@ export const peerStore = defineStore({
|
||||
this.calculatePages()
|
||||
this.fetching = false
|
||||
},
|
||||
setPreparedPeer(peer) {
|
||||
this.prepared = peer;
|
||||
},
|
||||
async PreparePeer(interfaceId) {
|
||||
return apiWrapper.get(`${baseUrl}/prepare/${interfaceId}`)
|
||||
.then(this.setPreparedPeer)
|
||||
.catch(error => {
|
||||
this.prepared = {}
|
||||
console.log("Failed to load prepared peer: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load prepared peer!",
|
||||
})
|
||||
})
|
||||
},
|
||||
async LoadPeers() {
|
||||
let iface = interfaceStore().GetSelected
|
||||
if (!iface) {
|
||||
|
108
frontend/src/stores/profile.js
Normal file
108
frontend/src/stores/profile.js
Normal file
@ -0,0 +1,108 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import {apiWrapper} from "@/helpers/fetch-wrapper";
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
import {authStore} from "@/stores/auth";
|
||||
|
||||
|
||||
const baseUrl = `/user`
|
||||
|
||||
export const profileStore = defineStore({
|
||||
id: 'profile',
|
||||
state: () => ({
|
||||
peers: [],
|
||||
user: {},
|
||||
filter: "",
|
||||
pageSize: 10,
|
||||
pageOffset: 0,
|
||||
pages: [],
|
||||
fetching: false,
|
||||
}),
|
||||
getters: {
|
||||
FindPeers: (state) => {
|
||||
return (id) => state.peers.find((p) => p.Identifier === id)
|
||||
},
|
||||
CountPeers: (state) => state.peers.length,
|
||||
FilteredPeerCount: (state) => state.FilteredPeers.length,
|
||||
Peers: (state) => state.peers,
|
||||
FilteredPeers: (state) => {
|
||||
if (!state.filter) {
|
||||
return state.peers
|
||||
}
|
||||
return state.peers.filter((p) => {
|
||||
return p.DisplayName.includes(state.filter) || p.Identifier.includes(state.filter)
|
||||
})
|
||||
},
|
||||
FilteredAndPagedPeers: (state) => {
|
||||
return state.FilteredPeers.slice(state.pageOffset, state.pageOffset + state.pageSize)
|
||||
},
|
||||
isFetching: (state) => state.fetching,
|
||||
hasNextPage: (state) => state.pageOffset < (state.FilteredPeerCount - state.pageSize),
|
||||
hasPrevPage: (state) => state.pageOffset > 0,
|
||||
currentPage: (state) => (state.pageOffset / state.pageSize)+1,
|
||||
},
|
||||
actions: {
|
||||
afterPageSizeChange() {
|
||||
// reset pageOffset to avoid problems with new page sizes
|
||||
this.pageOffset = 0
|
||||
this.calculatePages()
|
||||
},
|
||||
calculatePages() {
|
||||
let pageCounter = 1;
|
||||
this.pages = []
|
||||
for (let i = 0; i < this.FilteredPeerCount; i+=this.pageSize) {
|
||||
this.pages.push(pageCounter++)
|
||||
}
|
||||
},
|
||||
gotoPage(page) {
|
||||
this.pageOffset = (page-1) * this.pageSize
|
||||
|
||||
this.calculatePages()
|
||||
},
|
||||
nextPage() {
|
||||
this.pageOffset += this.pageSize
|
||||
|
||||
this.calculatePages()
|
||||
},
|
||||
previousPage() {
|
||||
this.pageOffset -= this.pageSize
|
||||
|
||||
this.calculatePages()
|
||||
},
|
||||
setPeers(peers) {
|
||||
this.peers = peers
|
||||
this.fetching = false
|
||||
},
|
||||
setUser(user) {
|
||||
this.user = user
|
||||
this.fetching = false
|
||||
},
|
||||
async LoadPeers() {
|
||||
this.fetching = true
|
||||
let currentUser = authStore().user.Identifier
|
||||
return apiWrapper.get(`${baseUrl}/${currentUser}/peers`)
|
||||
.then(this.setPeers)
|
||||
.catch(error => {
|
||||
this.setPeers([])
|
||||
console.log("Failed to load user peers for ", currentUser, ": ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load user peers!",
|
||||
})
|
||||
})
|
||||
},
|
||||
async LoadUser() {
|
||||
this.fetching = true
|
||||
let currentUser = authStore().user.Identifier
|
||||
return apiWrapper.get(`${baseUrl}/${currentUser}`)
|
||||
.then(this.setUser)
|
||||
.catch(error => {
|
||||
this.setUser({})
|
||||
console.log("Failed to load user for ", currentUser, ": ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load user!",
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
111
frontend/src/views/ProfileView.vue
Normal file
111
frontend/src/views/ProfileView.vue
Normal file
@ -0,0 +1,111 @@
|
||||
<script setup>
|
||||
import PeerViewModal from "../components/PeerViewModal.vue";
|
||||
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {profileStore} from "@/stores/profile";
|
||||
|
||||
const profile = profileStore()
|
||||
|
||||
const viewedPeerId = ref("")
|
||||
const editPeerId = ref("")
|
||||
|
||||
onMounted(async () => {
|
||||
await profile.LoadUser()
|
||||
await profile.LoadPeers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PeerViewModal :peerId="viewedPeerId" :visible="viewedPeerId!==''" @close="viewedPeerId=''"></PeerViewModal>
|
||||
|
||||
<!-- Peer list -->
|
||||
<div class="mt-4 row">
|
||||
<div class="col-12 col-lg-5">
|
||||
<h2 class="mt-2">{{ $t('profile.h2-clients') }}</h2>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4 text-lg-end">
|
||||
<div class="form-group d-inline">
|
||||
<div class="input-group mb-3">
|
||||
<input v-model="profile.filter" class="form-control" placeholder="Search..." type="text" @keyup="profile.afterPageSizeChange">
|
||||
<button class="input-group-text btn btn-primary" title="Search"><i class="fa-solid fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3 text-lg-end">
|
||||
<a class="btn btn-primary ms-2" href="#" title="Add a peer" @click.prevent="editPeerId='#NEW#'"><i class="fa fa-plus me-1"></i><i class="fa fa-user"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 table-responsive">
|
||||
<div v-if="profile.CountPeers===0">
|
||||
<h4>{{ $t('profile.noPeerSelect.h4') }}</h4>
|
||||
<p>{{ $t('profile.noPeerSelect.message') }}</p>
|
||||
</div>
|
||||
<table v-if="profile.CountPeers!==0" id="peerTable" class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<input id="flexCheckDefault" class="form-check-input" title="Select all" type="checkbox" value="">
|
||||
</th><!-- select -->
|
||||
<th scope="col">{{ $t('profile.tableHeadings[0]') }}</th>
|
||||
<th scope="col">{{ $t('profile.tableHeadings[1]') }}</th>
|
||||
<th scope="col">{{ $t('profile.tableHeadings[2]') }}</th>
|
||||
<th scope="col">{{ $t('profile.tableHeadings[3]') }}</th>
|
||||
<th scope="col">{{ $t('profile.tableHeadings[4]') }}</th>
|
||||
<th scope="col"></th><!-- Actions -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="peer in profile.FilteredAndPagedPeers" :key="peer.Identifier">
|
||||
<th scope="row">
|
||||
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
|
||||
</th>
|
||||
<td>{{peer.DisplayName}}</td>
|
||||
<td>{{peer.Identifier}}</td>
|
||||
<td>{{peer.UserIdentifier}}</td>
|
||||
<td>
|
||||
<span v-for="ip in peer.Addresses" :key="ip" class="badge rounded-pill bg-light">{{ ip }}</span>
|
||||
</td>
|
||||
<td>{{peer.LastConnected}}</td>
|
||||
<td class="text-center">
|
||||
<a href="#" title="Show peer" @click.prevent="viewedPeerId=peer.Identifier"><i class="fas fa-eye me-2"></i></a>
|
||||
<a href="#" title="Edit peer" @click.prevent="editPeerId=peer.Identifier"><i class="fas fa-cog"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="mt-3">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<ul class="pagination pagination-sm">
|
||||
<li :class="{disabled:profile.pageOffset===0}" class="page-item">
|
||||
<a class="page-link" @click="profile.previousPage">«</a>
|
||||
</li>
|
||||
|
||||
<li v-for="page in profile.pages" :key="page" :class="{active:profile.currentPage===page}" class="page-item">
|
||||
<a class="page-link" @click="profile.gotoPage(page)">{{page}}</a>
|
||||
</li>
|
||||
|
||||
<li :class="{disabled:!profile.hasNextPage}" class="page-item">
|
||||
<a class="page-link" @click="profile.nextPage">»</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('profile.pagination.size') }}:</label>
|
||||
<div class="col-sm-6">
|
||||
<select v-model.number="profile.pageSize" class="form-select" @click="profile.afterPageSizeChange()">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="999999999">{{ $t('profile.pagination.all') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -24,6 +24,7 @@ func (e interfaceEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *aut
|
||||
apiGroup.GET("/all", e.handleAllGet())
|
||||
apiGroup.GET("/get/:id", e.handleSingleGet())
|
||||
apiGroup.PUT("/:id", e.handleUpdatePut())
|
||||
apiGroup.DELETE("/:id", e.handleDelete())
|
||||
apiGroup.POST("/new", e.handleCreatePost())
|
||||
apiGroup.GET("/config/:id", e.handleConfigGet())
|
||||
|
||||
@ -252,3 +253,36 @@ func (e interfaceEndpoint) handlePeersGet() gin.HandlerFunc {
|
||||
c.JSON(http.StatusOK, model.NewPeers(peers))
|
||||
}
|
||||
}
|
||||
|
||||
// handleDelete returns a gorm handler function.
|
||||
//
|
||||
// @ID interfaces_handleDelete
|
||||
// @Tags Interface
|
||||
// @Summary Delete the interface record.
|
||||
// @Produce json
|
||||
// @Param id path string true "The interface identifier"
|
||||
// @Success 204 "No content if deletion was successful"
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /interface/{id} [delete]
|
||||
func (e interfaceEndpoint) handleDelete() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||
return
|
||||
}
|
||||
|
||||
err := e.app.DeleteInterface(ctx, domain.InterfaceIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{
|
||||
Code: http.StatusInternalServerError, Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,11 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
model2 "github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||
"net/http"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type userEndpoint struct {
|
||||
@ -23,7 +21,9 @@ func (e userEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
|
||||
apiGroup := g.Group("/user", e.authenticator.LoggedIn())
|
||||
|
||||
apiGroup.GET("/all", e.handleAllGet())
|
||||
apiGroup.GET("/:id", e.handleSingleGet())
|
||||
apiGroup.PUT("/:id", e.handleUpdatePut())
|
||||
apiGroup.DELETE("/:id", e.handleDelete())
|
||||
apiGroup.POST("/new", e.handleCreatePost())
|
||||
apiGroup.GET("/:id/peers", e.handlePeersGet())
|
||||
}
|
||||
@ -36,16 +36,46 @@ func (e userEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
|
||||
// @Produce json
|
||||
// @Success 200 {object} []model.User
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /users [get]
|
||||
// @Router /user/all [get]
|
||||
func (e userEndpoint) handleAllGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
users, err := e.app.GetAllUsers(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model2.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model2.NewUsers(users))
|
||||
c.JSON(http.StatusOK, model.NewUsers(users))
|
||||
}
|
||||
}
|
||||
|
||||
// handleSingleGet returns a gorm handler function.
|
||||
//
|
||||
// @ID users_handleSingleGet
|
||||
// @Tags Users
|
||||
// @Summary Get a single user record.
|
||||
// @Produce json
|
||||
// @Param id path string true "The user identifier"
|
||||
// @Success 200 {object} model.User
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /user/{id} [get]
|
||||
func (e userEndpoint) handleSingleGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := e.app.GetUser(ctx, domain.UserIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.NewUser(user))
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,36 +90,36 @@ func (e userEndpoint) handleAllGet() gin.HandlerFunc {
|
||||
// @Success 200 {object} model.User
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /users/{id} [put]
|
||||
// @Router /user/{id} [put]
|
||||
func (e userEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, model2.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||
return
|
||||
}
|
||||
|
||||
var user model2.User
|
||||
var user model.User
|
||||
err := c.BindJSON(&user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model2.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if id != user.Identifier {
|
||||
c.JSON(http.StatusBadRequest, model2.Error{Code: http.StatusBadRequest, Message: "user id mismatch"})
|
||||
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "user id mismatch"})
|
||||
return
|
||||
}
|
||||
|
||||
updateUser, err := e.app.UpdateUser(ctx, model2.NewDomainUser(&user))
|
||||
updateUser, err := e.app.UpdateUser(ctx, model.NewDomainUser(&user))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model2.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model2.NewUser(updateUser))
|
||||
c.JSON(http.StatusOK, model.NewUser(updateUser))
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,25 +133,25 @@ func (e userEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||
// @Success 200 {object} model.User
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /users/new [post]
|
||||
// @Router /user/new [post]
|
||||
func (e userEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
var user model2.User
|
||||
var user model.User
|
||||
err := c.BindJSON(&user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, model2.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
newUser, err := e.app.CreateUser(ctx, model2.NewDomainUser(&user))
|
||||
newUser, err := e.app.CreateUser(ctx, model.NewDomainUser(&user))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model2.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model2.NewUser(newUser))
|
||||
c.JSON(http.StatusOK, model.NewUser(newUser))
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,21 +163,52 @@ func (e userEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||
// @Produce json
|
||||
// @Success 200 {object} []model.Peer
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /users/{id}/peers [get]
|
||||
// @Router /user/{id}/peers [get]
|
||||
func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
interfaceId := c.Param("id")
|
||||
if interfaceId == "" {
|
||||
c.JSON(http.StatusBadRequest, model2.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||
return
|
||||
}
|
||||
|
||||
peers, err := e.app.GetUserPeers(c.Request.Context(), domain.UserIdentifier(interfaceId))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model2.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model2.NewPeers(peers))
|
||||
c.JSON(http.StatusOK, model.NewPeers(peers))
|
||||
}
|
||||
}
|
||||
|
||||
// handleDelete returns a gorm handler function.
|
||||
//
|
||||
// @ID users_handleDelete
|
||||
// @Tags Users
|
||||
// @Summary Delete the user record.
|
||||
// @Produce json
|
||||
// @Param id path string true "The user identifier"
|
||||
// @Success 204 "No content if deletion was successful"
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /user/{id} [delete]
|
||||
func (e userEndpoint) handleDelete() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||
return
|
||||
}
|
||||
|
||||
err := e.app.DeleteUser(ctx, domain.UserIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
@ -3,4 +3,5 @@ package app
|
||||
const TopicUserCreated = "user:created"
|
||||
const TopicUserRegistered = "user:registered"
|
||||
const TopicUserDisabled = "user:disabled"
|
||||
const TopicUserDeleted = "user:deleted"
|
||||
const TopicAuthLogin = "auth:login"
|
||||
|
@ -21,6 +21,7 @@ type UserManager interface {
|
||||
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
||||
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
CreateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
DeleteUser(ctx context.Context, id domain.UserIdentifier) error
|
||||
}
|
||||
|
||||
type WireGuardManager interface {
|
||||
@ -34,6 +35,7 @@ type WireGuardManager interface {
|
||||
PrepareInterface(ctx context.Context) (*domain.Interface, error)
|
||||
CreateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
|
||||
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
|
||||
DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error
|
||||
}
|
||||
|
||||
type StatisticsCollector interface {
|
||||
|
@ -181,9 +181,33 @@ func (m Manager) CreateUser(ctx context.Context, user *domain.User) (*domain.Use
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (m Manager) DeleteUser(ctx context.Context, id domain.UserIdentifier) error {
|
||||
existingUser, err := m.users.GetUser(ctx, id)
|
||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||
return fmt.Errorf("unable to find user %s: %w", id, err)
|
||||
}
|
||||
|
||||
if err := m.validateDeletion(ctx, existingUser); err != nil {
|
||||
return fmt.Errorf("deletion not allowed: %w", err)
|
||||
}
|
||||
|
||||
err = m.users.DeleteUser(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deletion failure: %w", err)
|
||||
}
|
||||
|
||||
m.bus.Publish(app.TopicUserDeleted, existingUser)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Manager) validateModifications(ctx context.Context, old, new *domain.User) error {
|
||||
currentUser := domain.GetUserInfo(ctx)
|
||||
|
||||
if currentUser.Id != new.Identifier && !currentUser.IsAdmin {
|
||||
return fmt.Errorf("insufficient permissions")
|
||||
}
|
||||
|
||||
if err := old.EditAllowed(); err != nil {
|
||||
return fmt.Errorf("no access: %w", err)
|
||||
}
|
||||
@ -208,10 +232,20 @@ func (m Manager) validateModifications(ctx context.Context, old, new *domain.Use
|
||||
}
|
||||
|
||||
func (m Manager) validateCreation(ctx context.Context, new *domain.User) error {
|
||||
currentUser := domain.GetUserInfo(ctx)
|
||||
|
||||
if !currentUser.IsAdmin {
|
||||
return fmt.Errorf("insufficient permissions")
|
||||
}
|
||||
|
||||
if new.Identifier == "" {
|
||||
return fmt.Errorf("invalid user identifier")
|
||||
}
|
||||
|
||||
if new.Identifier == "all" { // the all user identifier collides with the rest api routes
|
||||
return fmt.Errorf("reserved user identifier")
|
||||
}
|
||||
|
||||
if new.Source != domain.UserSourceDatabase {
|
||||
return fmt.Errorf("invalid user source: %s", new.Source)
|
||||
}
|
||||
@ -223,6 +257,24 @@ func (m Manager) validateCreation(ctx context.Context, new *domain.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Manager) validateDeletion(ctx context.Context, del *domain.User) error {
|
||||
currentUser := domain.GetUserInfo(ctx)
|
||||
|
||||
if !currentUser.IsAdmin {
|
||||
return fmt.Errorf("insufficient permissions")
|
||||
}
|
||||
|
||||
if err := del.EditAllowed(); err != nil {
|
||||
return fmt.Errorf("no access: %w", err)
|
||||
}
|
||||
|
||||
if currentUser.Id == del.Identifier {
|
||||
return fmt.Errorf("cannot delete own user")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Manager) runLdapSynchronizationService(ctx context.Context) {
|
||||
running := true
|
||||
for running {
|
||||
|
@ -524,6 +524,34 @@ func (m Manager) UpdateInterface(ctx context.Context, in *domain.Interface) (*do
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error {
|
||||
existingInterface, err := m.db.GetInterface(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to find interface %s: %w", id, err)
|
||||
}
|
||||
|
||||
if err := m.validateDeletion(ctx, existingInterface); err != nil {
|
||||
return fmt.Errorf("deletion not allowed: %w", err)
|
||||
}
|
||||
|
||||
err = m.deleteInterfacePeers(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("peer deletion failure: %w", err)
|
||||
}
|
||||
|
||||
err = m.wg.DeleteInterface(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wireguard deletion failure: %w", err)
|
||||
}
|
||||
|
||||
err = m.db.DeleteInterface(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deletion failure: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Manager) validateModifications(ctx context.Context, old, new *domain.Interface) error {
|
||||
currentUser := domain.GetUserInfo(ctx)
|
||||
|
||||
@ -547,3 +575,33 @@ func (m Manager) validateCreation(ctx context.Context, old, new *domain.Interfac
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Manager) validateDeletion(ctx context.Context, del *domain.Interface) error {
|
||||
currentUser := domain.GetUserInfo(ctx)
|
||||
|
||||
if !currentUser.IsAdmin {
|
||||
return fmt.Errorf("insufficient permissions")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Manager) deleteInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) error {
|
||||
allPeers, err := m.db.GetInterfacePeers(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, peer := range allPeers {
|
||||
err = m.wg.DeletePeer(ctx, id, peer.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wireguard peer deletion failure for %s: %w", peer.Identifier, err)
|
||||
}
|
||||
|
||||
err = m.db.DeletePeer(ctx, peer.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("peer deletion failure for %s: %w", peer.Identifier, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -70,6 +70,10 @@ func (u *User) CheckPassword(password string) error {
|
||||
return errors.New("invalid user source")
|
||||
}
|
||||
|
||||
if u.IsDisabled() {
|
||||
return errors.New("user disabled")
|
||||
}
|
||||
|
||||
if u.Password == "" {
|
||||
return errors.New("empty user password")
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user