diff --git a/pkg/rest/router.go b/pkg/rest/router.go index c01cae8..1206d01 100644 --- a/pkg/rest/router.go +++ b/pkg/rest/router.go @@ -62,7 +62,7 @@ func (c *Context) getRouter(assets fs.FS, indexHtml []byte) *http.ServeMux { mux.Handle("/api/saml-setup/{id}", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.samlSetupElementHandler))))) mux.Handle("/api/users", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.usersHandler))))) mux.Handle("/api/user/{id}", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.userHandler))))) - mux.Handle("/api/stats/user", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.userStatsHandler))))) + mux.Handle("/api/stats/user/{date}", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.userStatsHandler))))) return mux } diff --git a/pkg/rest/stats.go b/pkg/rest/stats.go index c39f7b8..229b5d4 100644 --- a/pkg/rest/stats.go +++ b/pkg/rest/stats.go @@ -5,6 +5,7 @@ import ( "bytes" "encoding/json" "fmt" + "math" "net/http" "path" "sort" @@ -16,6 +17,24 @@ import ( ) func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { + if r.PathValue("date") == "" { + c.returnError(w, fmt.Errorf("no date supplied"), http.StatusBadRequest) + return + } + date, err := time.Parse("2006-01-02", r.PathValue("date")) + if err != nil { + c.returnError(w, fmt.Errorf("invalid date: %s", err), http.StatusBadRequest) + return + } + unitAdjustment := int64(1) + switch r.FormValue("unit") { + case "KB": + unitAdjustment = 1024 + case "MB": + unitAdjustment = 1024 * 1024 + case "GB": + unitAdjustment = 1024 * 1024 * 1024 + } // get all users users := c.UserStore.ListUsers() userMap := make(map[string]string) @@ -24,7 +43,7 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { } // calculate stats var userStatsResponse UserStatsResponse - statsFile := c.Storage.Client.ConfigPath(path.Join(wireguard.VPN_STATS_DIR, "user-"+time.Now().Format("2006-01-02")) + ".log") + statsFile := c.Storage.Client.ConfigPath(path.Join(wireguard.VPN_STATS_DIR, "user-"+date.Format("2006-01-02")+".log")) if !c.Storage.Client.FileExists(statsFile) { // file does not exist so just return empty response out, err := json.Marshal(userStatsResponse) if err != nil { @@ -69,14 +88,16 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { if _, ok := receiveBytesData[userID]; !ok { receiveBytesData[userID] = []UserStatsDataPoint{} } - receiveBytesData[userID] = append(receiveBytesData[userID], UserStatsDataPoint{X: inputSplit[0], Y: receiveBytes - receiveBytesLast[userID]}) + value := math.Round(float64((receiveBytes-receiveBytesLast[userID])/unitAdjustment*100)) / 100 + receiveBytesData[userID] = append(receiveBytesData[userID], UserStatsDataPoint{X: inputSplit[0], Y: value}) } transmitBytes, err := strconv.ParseInt(inputSplit[4], 10, 64) if err == nil { if _, ok := transmitBytesData[userID]; !ok { transmitBytesData[userID] = []UserStatsDataPoint{} } - transmitBytesData[userID] = append(transmitBytesData[userID], UserStatsDataPoint{X: inputSplit[0], Y: transmitBytes - transmitBytesLast[userID]}) + value := math.Round(float64((transmitBytes-transmitBytesLast[userID])/unitAdjustment*100)) / 100 + transmitBytesData[userID] = append(transmitBytesData[userID], UserStatsDataPoint{X: inputSplit[0], Y: value}) } receiveBytesLast[userID] = receiveBytes transmitBytesLast[userID] = transmitBytes @@ -98,10 +119,11 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { login = "unknown" } userStatsResponse.ReceiveBytes.Datasets = append(userStatsResponse.ReceiveBytes.Datasets, UserStatsDataset{ - BorderColor: getColor(len(userStatsResponse.ReceiveBytes.Datasets)), - Label: login, - Data: data, - Tension: 0.1, + BorderColor: getColor(len(userStatsResponse.ReceiveBytes.Datasets)), + BackgroundColor: getColor(len(userStatsResponse.ReceiveBytes.Datasets)), + Label: login, + Data: data, + Tension: 0.1, }) } for userID, data := range transmitBytesData { @@ -110,10 +132,11 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { login = "unknown" } userStatsResponse.TransmitBytes.Datasets = append(userStatsResponse.TransmitBytes.Datasets, UserStatsDataset{ - BorderColor: getColor(len(userStatsResponse.TransmitBytes.Datasets)), - Label: login, - Data: data, - Tension: 0.1, + BorderColor: getColor(len(userStatsResponse.TransmitBytes.Datasets)), + BackgroundColor: getColor(len(userStatsResponse.TransmitBytes.Datasets)), + Label: login, + Data: data, + Tension: 0.1, }) } diff --git a/pkg/rest/types.go b/pkg/rest/types.go index 6c49f36..e3443a3 100644 --- a/pkg/rest/types.go +++ b/pkg/rest/types.go @@ -184,14 +184,15 @@ type UserStatsData struct { } type UserStatsDatasets []UserStatsDataset type UserStatsDataset struct { - Label string `json:"label"` - Data []UserStatsDataPoint `json:"data"` - Fill bool `json:"fill"` - BorderColor string `json:"borderColor"` - Tension float64 `json:"tension"` + Label string `json:"label"` + Data []UserStatsDataPoint `json:"data"` + Fill bool `json:"fill"` + BorderColor string `json:"borderColor"` + BackgroundColor string `json:"backgroundColor"` + Tension float64 `json:"tension"` } type UserStatsDataPoint struct { - X string `json:"x"` - Y int64 `json:"y"` + X string `json:"x"` + Y float64 `json:"y"` } diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index fc63aec..ed795a8 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -1,4 +1,5 @@ import "@mantine/core/styles.css"; +import '@mantine/dates/styles.css'; import { AppShell, MantineProvider } from "@mantine/core"; import { theme } from "./theme"; import { NavBar } from "./NavBar/NavBar"; diff --git a/webapp/src/Routes/Home/UserStats.tsx b/webapp/src/Routes/Home/UserStats.tsx index fa04a76..60b44e0 100644 --- a/webapp/src/Routes/Home/UserStats.tsx +++ b/webapp/src/Routes/Home/UserStats.tsx @@ -1,20 +1,25 @@ -import { Card, Center, Text } from "@mantine/core"; -//import { DatePicker } from '@mantine/dates'; +import { Card, Center, Divider, Grid, Select, Text } from "@mantine/core"; +import { DatePickerInput } from '@mantine/dates'; import { useQuery } from "@tanstack/react-query"; import { useAuthContext } from "../../Auth/Auth"; import { AppSettings } from '../../Constants/Constants'; +import { format } from "date-fns"; import { Chart } from 'react-chartjs-2'; import 'chartjs-adapter-date-fns'; import { Chart as ChartJS, LineController, LineElement, PointElement, LinearScale, Title, CategoryScale, TimeScale, ChartOptions, Legend } from 'chart.js'; +import { useState } from "react"; + export function UserStats() { ChartJS.register(LineController, LineElement, PointElement, LinearScale, Title, CategoryScale, TimeScale, Legend); const {authInfo} = useAuthContext() + const [statsDate, setStatsDate] = useState(new Date()); + const [unit, setUnit] = useState("MB") const { isPending, error, data } = useQuery({ - queryKey: ['userstats'], + queryKey: ['userstats', statsDate, unit], queryFn: () => - fetch(AppSettings.url + '/stats/user', { + fetch(AppSettings.url + '/stats/user/' + format(statsDate === null ? new Date() : statsDate, "yyyy-MM-dd") + "?unit=" +unit, { headers: { "Content-Type": "application/json", "Authorization": "Bearer " + authInfo.token @@ -54,27 +59,42 @@ export function UserStats() { if (isPending) return '' if (error) return 'cannot retrieve licensed users' - if(data.receivedBytes.datasets === null) { - data.receivedBytes.datasets = [{ data: [0], label: "no data"}] - } - if(data.transmitBytes.datasets === null) { - data.transmitBytes.datasets = [{ data: [0], label: "no data"}] - } return ( <> -
- VPN Data Received (bytes) - - - + + + + + + + +