parent
95e767a85d
commit
cae3dabf1e
|
|
@ -4,7 +4,6 @@ import (
|
|||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -25,6 +24,10 @@ import (
|
|||
// connectedThreshold defines how recently a peer must have handshaked to be considered connected
|
||||
var connectedThreshold = 3 * time.Minute
|
||||
|
||||
func isConnected(lastHandshake time.Time) bool {
|
||||
return !lastHandshake.IsZero() && time.Since(lastHandshake) < connectedThreshold
|
||||
}
|
||||
|
||||
func connectedPeerKeys() map[string]bool {
|
||||
keys := make(map[string]bool)
|
||||
wgClient, err := wgctrl.New()
|
||||
|
|
@ -42,7 +45,7 @@ func connectedPeerKeys() map[string]bool {
|
|||
|
||||
for _, dev := range devices {
|
||||
for _, peer := range dev.Peers {
|
||||
if time.Since(peer.LastHandshakeTime) < connectedThreshold {
|
||||
if isConnected(peer.LastHandshakeTime) {
|
||||
keys[peer.PublicKey.String()] = true
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +92,6 @@ func APIListClients(db store.IStore) echo.HandlerFunc {
|
|||
|
||||
filtered := make([]model.ClientData, 0, len(clientDataList))
|
||||
for _, clientData := range clientDataList {
|
||||
clientData = util.FillClientSubnetRange(clientData)
|
||||
cl := clientData.Client
|
||||
|
||||
// Non-admin users can only see clients matching their email
|
||||
|
|
@ -121,7 +123,7 @@ func APIListClients(db store.IStore) echo.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
filtered = append(filtered, clientData)
|
||||
filtered = append(filtered, util.FillClientSubnetRange(clientData))
|
||||
}
|
||||
return c.JSON(http.StatusOK, filtered)
|
||||
}
|
||||
|
|
@ -193,7 +195,10 @@ func APICreateClient(db store.IStore, cw *ConfigWriter) echo.HandlerFunc {
|
|||
}
|
||||
|
||||
// validate name + public key uniqueness in one pass
|
||||
existingClients, _ := db.GetClients(false)
|
||||
existingClients, err := db.GetClients(false)
|
||||
if err != nil {
|
||||
return apiInternalError(c, "Cannot check for duplicates")
|
||||
}
|
||||
for _, ec := range existingClients {
|
||||
if strings.EqualFold(ec.Client.Name, client.Name) {
|
||||
return apiBadRequest(c, "A client with this name already exists")
|
||||
|
|
@ -300,7 +305,10 @@ func APIUpdateClient(db store.IStore, cw *ConfigWriter) echo.HandlerFunc {
|
|||
nameChanged := !strings.EqualFold(_client.Name, client.Name)
|
||||
pubKeyChanged := _client.PublicKey != "" && client.PublicKey != _client.PublicKey
|
||||
if nameChanged || pubKeyChanged {
|
||||
existingClients, _ := db.GetClients(false)
|
||||
existingClients, err := db.GetClients(false)
|
||||
if err != nil {
|
||||
return apiInternalError(c, "Cannot check for duplicates")
|
||||
}
|
||||
for _, ec := range existingClients {
|
||||
if ec.Client.ID == client.ID {
|
||||
continue
|
||||
|
|
@ -645,7 +653,6 @@ func APIServerStatus(db store.IStore) echo.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
conv := map[bool]int{true: 1, false: 0}
|
||||
for i := range devices {
|
||||
dev := DeviceStatus{Name: devices[i].Name}
|
||||
for j := range devices[i].Peers {
|
||||
|
|
@ -669,7 +676,7 @@ func APIServerStatus(db store.IStore) echo.HandlerFunc {
|
|||
LastHandshakeRel: handshakeRel,
|
||||
AllocatedIP: allocatedIPs,
|
||||
}
|
||||
p.Connected = p.LastHandshakeRel < connectedThreshold
|
||||
p.Connected = isConnected(handshakeTime)
|
||||
|
||||
if devices[i].Peers[j].Endpoint != nil {
|
||||
p.Endpoint = devices[i].Peers[j].Endpoint.String()
|
||||
|
|
@ -681,8 +688,6 @@ func APIServerStatus(db store.IStore) echo.HandlerFunc {
|
|||
}
|
||||
dev.Peers = append(dev.Peers, p)
|
||||
}
|
||||
sort.SliceStable(dev.Peers, func(a, b int) bool { return dev.Peers[a].Name < dev.Peers[b].Name })
|
||||
sort.SliceStable(dev.Peers, func(a, b int) bool { return conv[dev.Peers[a].Connected] > conv[dev.Peers[b].Connected] })
|
||||
devicesStatus = append(devicesStatus, dev)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2071,3 +2071,22 @@ func TestAPIServerStatus(t *testing.T) {
|
|||
// On systems without, it returns 500
|
||||
assert.Contains(t, []int{http.StatusOK, http.StatusInternalServerError}, rec.Code)
|
||||
}
|
||||
|
||||
// Regression: peers that never connected (zero handshake time) must not be
|
||||
// reported as connected. Both connectedPeerKeys and APIServerStatus use
|
||||
// isConnected() which guards against time.Since(zero).
|
||||
func TestIsConnected_ZeroHandshake(t *testing.T) {
|
||||
assert.False(t, isConnected(time.Time{}), "zero time must not be connected")
|
||||
}
|
||||
|
||||
func TestIsConnected_RecentHandshake(t *testing.T) {
|
||||
assert.True(t, isConnected(time.Now().Add(-30*time.Second)), "30s ago must be connected")
|
||||
}
|
||||
|
||||
func TestIsConnected_OldHandshake(t *testing.T) {
|
||||
assert.False(t, isConnected(time.Now().Add(-10*time.Minute)), "10min ago must be disconnected")
|
||||
}
|
||||
|
||||
func TestIsConnected_ExactlyAtThreshold(t *testing.T) {
|
||||
assert.False(t, isConnected(time.Now().Add(-connectedThreshold)), "exactly at threshold must be disconnected")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"github.com/labstack/gommon/log"
|
||||
|
||||
"github.com/DigitalTolk/wireguard-ui/store"
|
||||
"github.com/DigitalTolk/wireguard-ui/util"
|
||||
)
|
||||
|
||||
// APIListUsers returns all users (read-only, managed via SSO)
|
||||
|
|
@ -48,11 +47,6 @@ func APIPatchUserAdmin(db store.IStore) echo.HandlerFunc {
|
|||
return apiInternalError(c, "Cannot update user")
|
||||
}
|
||||
|
||||
// update CRC32 cache so existing sessions reflect the change
|
||||
util.DBUsersToCRC32Mutex.Lock()
|
||||
util.DBUsersToCRC32[user.Username] = util.GetDBUserCRC32(user)
|
||||
util.DBUsersToCRC32Mutex.Unlock()
|
||||
|
||||
action := "user.demote"
|
||||
if body.Admin {
|
||||
action = "user.promote"
|
||||
|
|
|
|||
|
|
@ -42,8 +42,7 @@ func RegisterAPIv1(g *echo.Group, db store.IStore, mailer emailer.Emailer, cw *h
|
|||
settings.GET("", handler.APIGetSettings(db))
|
||||
settings.PUT("", handler.APIUpdateSettings(db, cw), handler.ContentTypeJson)
|
||||
|
||||
// Users (admin only for list/create/delete)
|
||||
// Users (read-only — managed via SSO)
|
||||
// Users (admin only)
|
||||
users := g.Group("/users", handler.APIAuth, handler.APIAdmin)
|
||||
users.GET("", handler.APIListUsers(db))
|
||||
users.GET("/:username", handler.APIGetUser(db))
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ function QrCodeDialog({ client, onClose }: { client: ClientData | null; onClose:
|
|||
queryKey: ["client-qr", client?.Client.id],
|
||||
queryFn: () => apiGet<{ qr_code: string }>(`/clients/${client!.Client.id}/qrcode`),
|
||||
enabled: !!client,
|
||||
staleTime: Infinity,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
@ -570,7 +570,7 @@ export function ClientsPage() {
|
|||
<DialogHeader>
|
||||
<DialogTitle>New Client</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-5 py-4 sm:grid-cols-2" onChange={() => setCreateTouched(true)}>
|
||||
<div className="grid items-start gap-5 py-4 sm:grid-cols-2" onChange={() => setCreateTouched(true)}>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="new-name">Name</Label>
|
||||
<Input
|
||||
|
|
@ -739,7 +739,7 @@ export function ClientsPage() {
|
|||
<DialogHeader>
|
||||
<DialogTitle>Edit Client</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-5 py-4 sm:grid-cols-2">
|
||||
<div className="grid items-start gap-5 py-4 sm:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-name">Name</Label>
|
||||
<Input
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ type SortKey =
|
|||
| "endpoint";
|
||||
type SortDir = "asc" | "desc";
|
||||
|
||||
function getSortValue(peer: PeerStatus, key: SortKey): string | number | boolean {
|
||||
function getSortValue(peer: PeerStatus, key: SortKey): string | number {
|
||||
switch (key) {
|
||||
case "name":
|
||||
return (peer.name || "").toLowerCase();
|
||||
|
|
|
|||
Loading…
Reference in New Issue