diff --git a/Makefile b/Makefile index b0c2775a1..aa3290fed 100644 --- a/Makefile +++ b/Makefile @@ -384,7 +384,7 @@ docs/user-guide/nexctl.md: dist/nexctl hack/nexctl-docs.sh $(ECHO_PREFIX) printf " %-12s nexctl\n" "[DOCS]" $(CMD_PREFIX) hack/nexctl-docs.sh -dist/.generate: $(SWAGGER_YAML) $(PRIVATE_SWAGGER_YAML) dist/.ui-fmt docs/user-guide/nexd.md docs/user-guide/nexctl.md | dist +dist/.generate: $(SWAGGER_YAML) $(PRIVATE_SWAGGER_YAML) docs/user-guide/nexd.md docs/user-guide/nexctl.md | dist $(ECHO_PREFIX) printf " %-12s \n" "[MOD TIDY]" $(CMD_PREFIX) go mod tidy diff --git a/docs/development/design/status-api.md b/docs/development/design/status-api.md new file mode 100644 index 000000000..48e61c993 --- /dev/null +++ b/docs/development/design/status-api.md @@ -0,0 +1,75 @@ +# Design Document: Status API + +## Introduction + +The goal of this design document is to outline the purpose and implementation of the status API. + +## The Problem + +There is a visual disconnect for a user's Nexodus network of devices. + +## The Solution + +With the addition of an API endpoint, device statuses can be created and stored. The status of devices in a user's Nexodus network can then be visualized in a web interface. The network will be displayed as a graph. + +## Implementation + +A general view of a device's connectivity includes: + + 1. Devices a source device is attempting to directly connect to + 2. Is the device connected to a relay and which devices is it using the relay to connect to + 3. Which devices can it reach successfully + 4. The latency of the source to each device in the network + +The proposed API additions are as follows: + +```go + // Create new statuses + private.POST("/status", api.CreateStatus) + // List all satuses for a user's devices + private.GET("/status", api.ListStatuses) + // Updates a device status if a status already exists + private.PUT("/status", api.UpdateStatus) +``` + +## New Tables + + A new table will be defined for device statuses. + +### Status Model + +- New models + +```go + // Status represents the status of single user device + type Status struct { + Base + UserId uuid.UUID `json:"user_id"` + WgIP string `json:"wg_ip"` + IsReachable bool `json:"is_reachable"` + Hostname string `json:"hostname"` + Latency string `json:"latency"` + Method string `json:"method"` + } + + // AddStatus is the information necessary to add a device status + type AddStatus struct { + WgIP string `json:"wg_ip"` + IsReachable bool `json:"is_reachable"` + Hostname string `json:"hostname"` + Latency string `json:"latency"` + Method string `json:"method"` + } + + // UpdateStatus is the information needed to update a device status that already exists + type UpdateStatus struct{ + WgIP string `json:"wg_ip"` + IsReachable bool `json:"is_reachable"` + Latency string `json:"latency"` + Method string `json:"method"` + } +``` + +### The Graph Display + +React Flow library was chosen for its flexibility and support for customization. The custom nodes were created with a set of criteria in mind including the shape of the node, the border color based on latency, icons for the connection status, and what information to include in the table when the node is clicked on. The device nodes are rectangular in shape, while the relay nodes are oval shaped. The border color will display green if the latency is in the range of 0 to 40 milliseconds, gold/orange if the latency is between 41 to 80 milliseconds, and red if the latency is 81 milliseconds or higher. If a device or relay is reachable, then a green check mark icon will appear on the right side of the node. If a device or relay becomes unreachable, then a red X icon will appear instead. The graph organizes itself show devices connected directly to the host device and those connected to a relay. The nodes are draggable for better organization based on user preferences. The data is fetched every 3 minutes and the information in the nodes is updated. diff --git a/go.mod b/go.mod index 5c8829ef6..1819f3af9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/nexodus-io/nexodus -go 1.22 +go 1.22.0 require ( github.com/Nerzal/gocloak/v13 v13.9.0 @@ -60,10 +60,10 @@ require ( go.opentelemetry.io/otel/trace v1.22.0 go.uber.org/zap v1.26.0 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 - golang.org/x/net v0.23.0 + golang.org/x/net v0.21.0 golang.org/x/oauth2 v0.17.0 - golang.org/x/sys v0.18.0 - golang.org/x/term v0.18.0 + golang.org/x/sys v0.17.0 + golang.org/x/term v0.17.0 golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 diff --git a/go.sum b/go.sum index eb00a5bad..13cb94720 100644 --- a/go.sum +++ b/go.sum @@ -684,11 +684,11 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= -golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/internal/client/.openapi-generator/FILES b/internal/client/.openapi-generator/FILES index 6821985e7..6bbaf68ee 100644 --- a/internal/client/.openapi-generator/FILES +++ b/internal/client/.openapi-generator/FILES @@ -9,6 +9,8 @@ api_reg_key.go api_security_group.go api_service_network.go api_sites.go +api_status.go +api_statuses.go api_users.go api_vpc.go client.go @@ -20,6 +22,7 @@ model_models_add_reg_key.go model_models_add_security_group.go model_models_add_service_network.go model_models_add_site.go +model_models_add_status.go model_models_add_vpc.go model_models_base_error.go model_models_certificate_signing_request.go @@ -39,6 +42,7 @@ model_models_security_group.go model_models_security_rule.go model_models_service_network.go model_models_site.go +model_models_status.go model_models_tunnel_ip.go model_models_update_device.go model_models_update_reg_key.go diff --git a/internal/client/api_status.go b/internal/client/api_status.go new file mode 100644 index 000000000..fb120cc82 --- /dev/null +++ b/internal/client/api_status.go @@ -0,0 +1,185 @@ +/* +Nexodus API + +This is the Nexodus API Server. + +API version: 1.0 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" + "strings" +) + +// StatusApiService StatusApi service +type StatusApiService service + +type ApiStatusIdGetRequest struct { + ctx context.Context + ApiService *StatusApiService + id string + id2 string +} + +func (r ApiStatusIdGetRequest) Execute() (*ModelsStatus, *http.Response, error) { + return r.ApiService.StatusIdGetExecute(r) +} + +/* +StatusIdGet Get user status + +Gets statuses based on userd + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param id id + @param id2 Unique identifier for the status + @return ApiStatusIdGetRequest +*/ +func (a *StatusApiService) StatusIdGet(ctx context.Context, id string, id2 string) ApiStatusIdGetRequest { + return ApiStatusIdGetRequest{ + ApiService: a, + ctx: ctx, + id: id, + id2: id2, + } +} + +// Execute executes the request +// +// @return ModelsStatus +func (a *StatusApiService) StatusIdGetExecute(r ApiStatusIdGetRequest) (*ModelsStatus, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *ModelsStatus + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "StatusApiService.StatusIdGet") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/status/{id}" + localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterValueToString(r.id, "id")), -1) + localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterValueToString(r.id2, "id2")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ModelsBaseError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 401 { + var v ModelsBaseError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 404 { + var v ModelsBaseError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 429 { + var v ModelsBaseError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v ModelsInternalServerError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/internal/client/api_statuses.go b/internal/client/api_statuses.go new file mode 100644 index 000000000..b7e566b69 --- /dev/null +++ b/internal/client/api_statuses.go @@ -0,0 +1,440 @@ +/* +Nexodus API + +This is the Nexodus API Server. + +API version: 1.0 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" +) + +// StatusesApiService StatusesApi service +type StatusesApiService service + +type ApiApiStatusDeleteRequest struct { + ctx context.Context + ApiService *StatusesApiService +} + +func (r ApiApiStatusDeleteRequest) Execute() (*http.Response, error) { + return r.ApiService.ApiStatusDeleteExecute(r) +} + +/* +ApiStatusDelete Delete All Statuses + +Deletes all statuses from the database + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiApiStatusDeleteRequest +*/ +func (a *StatusesApiService) ApiStatusDelete(ctx context.Context) ApiApiStatusDeleteRequest { + return ApiApiStatusDeleteRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +func (a *StatusesApiService) ApiStatusDeleteExecute(r ApiApiStatusDeleteRequest) (*http.Response, error) { + var ( + localVarHTTPMethod = http.MethodDelete + localVarPostBody interface{} + formFiles []formFile + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "StatusesApiService.ApiStatusDelete") + if err != nil { + return nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/api/status" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 401 { + var v ModelsBaseError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 429 { + var v ModelsBaseError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v ModelsInternalServerError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarHTTPResponse, newErr + } + + return localVarHTTPResponse, nil +} + +type ApiCreateStatusRequest struct { + ctx context.Context + ApiService *StatusesApiService + status *ModelsAddStatus +} + +// Add Status +func (r ApiCreateStatusRequest) Status(status ModelsAddStatus) ApiCreateStatusRequest { + r.status = &status + return r +} + +func (r ApiCreateStatusRequest) Execute() (*ModelsStatus, *http.Response, error) { + return r.ApiService.CreateStatusExecute(r) +} + +/* +CreateStatus Add Statuses + +Adds a new status + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiCreateStatusRequest +*/ +func (a *StatusesApiService) CreateStatus(ctx context.Context) ApiCreateStatusRequest { + return ApiCreateStatusRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return ModelsStatus +func (a *StatusesApiService) CreateStatusExecute(r ApiCreateStatusRequest) (*ModelsStatus, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPost + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *ModelsStatus + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "StatusesApiService.CreateStatus") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/api/status" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + if r.status == nil { + return localVarReturnValue, nil, reportError("status is required and must be specified") + } + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.status + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ModelsBaseError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 401 { + var v ModelsBaseError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 409 { + var v ModelsConflictsError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 429 { + var v ModelsBaseError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v ModelsInternalServerError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type ApiListStatusesRequest struct { + ctx context.Context + ApiService *StatusesApiService +} + +func (r ApiListStatusesRequest) Execute() ([]ModelsStatus, *http.Response, error) { + return r.ApiService.ListStatusesExecute(r) +} + +/* +ListStatuses List Statuses + +Lists all Statuses + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiListStatusesRequest +*/ +func (a *StatusesApiService) ListStatuses(ctx context.Context) ApiListStatusesRequest { + return ApiListStatusesRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return []ModelsStatus +func (a *StatusesApiService) ListStatusesExecute(r ApiListStatusesRequest) ([]ModelsStatus, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue []ModelsStatus + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "StatusesApiService.ListStatuses") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/api/status" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 401 { + var v ModelsBaseError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 429 { + var v ModelsBaseError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v ModelsInternalServerError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/internal/client/client.go b/internal/client/client.go index 08a7cb647..1d09b0ac0 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -72,6 +72,10 @@ type APIClient struct { SitesApi *SitesApiService + StatusApi *StatusApiService + + StatusesApi *StatusesApiService + UsersApi *UsersApiService VPCApi *VPCApiService @@ -104,6 +108,8 @@ func NewAPIClient(cfg *Configuration) *APIClient { c.SecurityGroupApi = (*SecurityGroupApiService)(&c.common) c.ServiceNetworkApi = (*ServiceNetworkApiService)(&c.common) c.SitesApi = (*SitesApiService)(&c.common) + c.StatusApi = (*StatusApiService)(&c.common) + c.StatusesApi = (*StatusesApiService)(&c.common) c.UsersApi = (*UsersApiService)(&c.common) c.VPCApi = (*VPCApiService)(&c.common) diff --git a/internal/client/model_models_add_status.go b/internal/client/model_models_add_status.go new file mode 100644 index 000000000..e29854cb0 --- /dev/null +++ b/internal/client/model_models_add_status.go @@ -0,0 +1,268 @@ +/* +Nexodus API + +This is the Nexodus API Server. + +API version: 1.0 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// checks if the ModelsAddStatus type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ModelsAddStatus{} + +// ModelsAddStatus struct for ModelsAddStatus +type ModelsAddStatus struct { + Hostname *string `json:"hostname,omitempty"` + IsReachable *bool `json:"is_reachable,omitempty"` + Latency *string `json:"latency,omitempty"` + Method *string `json:"method,omitempty"` + WgIp *string `json:"wg_ip,omitempty"` +} + +// NewModelsAddStatus instantiates a new ModelsAddStatus object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewModelsAddStatus() *ModelsAddStatus { + this := ModelsAddStatus{} + return &this +} + +// NewModelsAddStatusWithDefaults instantiates a new ModelsAddStatus object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewModelsAddStatusWithDefaults() *ModelsAddStatus { + this := ModelsAddStatus{} + return &this +} + +// GetHostname returns the Hostname field value if set, zero value otherwise. +func (o *ModelsAddStatus) GetHostname() string { + if o == nil || IsNil(o.Hostname) { + var ret string + return ret + } + return *o.Hostname +} + +// GetHostnameOk returns a tuple with the Hostname field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ModelsAddStatus) GetHostnameOk() (*string, bool) { + if o == nil || IsNil(o.Hostname) { + return nil, false + } + return o.Hostname, true +} + +// HasHostname returns a boolean if a field has been set. +func (o *ModelsAddStatus) HasHostname() bool { + if o != nil && !IsNil(o.Hostname) { + return true + } + + return false +} + +// SetHostname gets a reference to the given string and assigns it to the Hostname field. +func (o *ModelsAddStatus) SetHostname(v string) { + o.Hostname = &v +} + +// GetIsReachable returns the IsReachable field value if set, zero value otherwise. +func (o *ModelsAddStatus) GetIsReachable() bool { + if o == nil || IsNil(o.IsReachable) { + var ret bool + return ret + } + return *o.IsReachable +} + +// GetIsReachableOk returns a tuple with the IsReachable field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ModelsAddStatus) GetIsReachableOk() (*bool, bool) { + if o == nil || IsNil(o.IsReachable) { + return nil, false + } + return o.IsReachable, true +} + +// HasIsReachable returns a boolean if a field has been set. +func (o *ModelsAddStatus) HasIsReachable() bool { + if o != nil && !IsNil(o.IsReachable) { + return true + } + + return false +} + +// SetIsReachable gets a reference to the given bool and assigns it to the IsReachable field. +func (o *ModelsAddStatus) SetIsReachable(v bool) { + o.IsReachable = &v +} + +// GetLatency returns the Latency field value if set, zero value otherwise. +func (o *ModelsAddStatus) GetLatency() string { + if o == nil || IsNil(o.Latency) { + var ret string + return ret + } + return *o.Latency +} + +// GetLatencyOk returns a tuple with the Latency field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ModelsAddStatus) GetLatencyOk() (*string, bool) { + if o == nil || IsNil(o.Latency) { + return nil, false + } + return o.Latency, true +} + +// HasLatency returns a boolean if a field has been set. +func (o *ModelsAddStatus) HasLatency() bool { + if o != nil && !IsNil(o.Latency) { + return true + } + + return false +} + +// SetLatency gets a reference to the given string and assigns it to the Latency field. +func (o *ModelsAddStatus) SetLatency(v string) { + o.Latency = &v +} + +// GetMethod returns the Method field value if set, zero value otherwise. +func (o *ModelsAddStatus) GetMethod() string { + if o == nil || IsNil(o.Method) { + var ret string + return ret + } + return *o.Method +} + +// GetMethodOk returns a tuple with the Method field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ModelsAddStatus) GetMethodOk() (*string, bool) { + if o == nil || IsNil(o.Method) { + return nil, false + } + return o.Method, true +} + +// HasMethod returns a boolean if a field has been set. +func (o *ModelsAddStatus) HasMethod() bool { + if o != nil && !IsNil(o.Method) { + return true + } + + return false +} + +// SetMethod gets a reference to the given string and assigns it to the Method field. +func (o *ModelsAddStatus) SetMethod(v string) { + o.Method = &v +} + +// GetWgIp returns the WgIp field value if set, zero value otherwise. +func (o *ModelsAddStatus) GetWgIp() string { + if o == nil || IsNil(o.WgIp) { + var ret string + return ret + } + return *o.WgIp +} + +// GetWgIpOk returns a tuple with the WgIp field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ModelsAddStatus) GetWgIpOk() (*string, bool) { + if o == nil || IsNil(o.WgIp) { + return nil, false + } + return o.WgIp, true +} + +// HasWgIp returns a boolean if a field has been set. +func (o *ModelsAddStatus) HasWgIp() bool { + if o != nil && !IsNil(o.WgIp) { + return true + } + + return false +} + +// SetWgIp gets a reference to the given string and assigns it to the WgIp field. +func (o *ModelsAddStatus) SetWgIp(v string) { + o.WgIp = &v +} + +func (o ModelsAddStatus) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ModelsAddStatus) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Hostname) { + toSerialize["hostname"] = o.Hostname + } + if !IsNil(o.IsReachable) { + toSerialize["is_reachable"] = o.IsReachable + } + if !IsNil(o.Latency) { + toSerialize["latency"] = o.Latency + } + if !IsNil(o.Method) { + toSerialize["method"] = o.Method + } + if !IsNil(o.WgIp) { + toSerialize["wg_ip"] = o.WgIp + } + return toSerialize, nil +} + +type NullableModelsAddStatus struct { + value *ModelsAddStatus + isSet bool +} + +func (v NullableModelsAddStatus) Get() *ModelsAddStatus { + return v.value +} + +func (v *NullableModelsAddStatus) Set(val *ModelsAddStatus) { + v.value = val + v.isSet = true +} + +func (v NullableModelsAddStatus) IsSet() bool { + return v.isSet +} + +func (v *NullableModelsAddStatus) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableModelsAddStatus(val *ModelsAddStatus) *NullableModelsAddStatus { + return &NullableModelsAddStatus{value: val, isSet: true} +} + +func (v NullableModelsAddStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableModelsAddStatus) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client/model_models_status.go b/internal/client/model_models_status.go new file mode 100644 index 000000000..6a8882175 --- /dev/null +++ b/internal/client/model_models_status.go @@ -0,0 +1,340 @@ +/* +Nexodus API + +This is the Nexodus API Server. + +API version: 1.0 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// checks if the ModelsStatus type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ModelsStatus{} + +// ModelsStatus struct for ModelsStatus +type ModelsStatus struct { + Hostname *string `json:"hostname,omitempty"` + Id *string `json:"id,omitempty"` + IsReachable *bool `json:"is_reachable,omitempty"` + Latency *string `json:"latency,omitempty"` + Method *string `json:"method,omitempty"` + UserId *string `json:"user_id,omitempty"` + WgIp *string `json:"wg_ip,omitempty"` +} + +// NewModelsStatus instantiates a new ModelsStatus object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewModelsStatus() *ModelsStatus { + this := ModelsStatus{} + return &this +} + +// NewModelsStatusWithDefaults instantiates a new ModelsStatus object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewModelsStatusWithDefaults() *ModelsStatus { + this := ModelsStatus{} + return &this +} + +// GetHostname returns the Hostname field value if set, zero value otherwise. +func (o *ModelsStatus) GetHostname() string { + if o == nil || IsNil(o.Hostname) { + var ret string + return ret + } + return *o.Hostname +} + +// GetHostnameOk returns a tuple with the Hostname field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ModelsStatus) GetHostnameOk() (*string, bool) { + if o == nil || IsNil(o.Hostname) { + return nil, false + } + return o.Hostname, true +} + +// HasHostname returns a boolean if a field has been set. +func (o *ModelsStatus) HasHostname() bool { + if o != nil && !IsNil(o.Hostname) { + return true + } + + return false +} + +// SetHostname gets a reference to the given string and assigns it to the Hostname field. +func (o *ModelsStatus) SetHostname(v string) { + o.Hostname = &v +} + +// GetId returns the Id field value if set, zero value otherwise. +func (o *ModelsStatus) GetId() string { + if o == nil || IsNil(o.Id) { + var ret string + return ret + } + return *o.Id +} + +// GetIdOk returns a tuple with the Id field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ModelsStatus) GetIdOk() (*string, bool) { + if o == nil || IsNil(o.Id) { + return nil, false + } + return o.Id, true +} + +// HasId returns a boolean if a field has been set. +func (o *ModelsStatus) HasId() bool { + if o != nil && !IsNil(o.Id) { + return true + } + + return false +} + +// SetId gets a reference to the given string and assigns it to the Id field. +func (o *ModelsStatus) SetId(v string) { + o.Id = &v +} + +// GetIsReachable returns the IsReachable field value if set, zero value otherwise. +func (o *ModelsStatus) GetIsReachable() bool { + if o == nil || IsNil(o.IsReachable) { + var ret bool + return ret + } + return *o.IsReachable +} + +// GetIsReachableOk returns a tuple with the IsReachable field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ModelsStatus) GetIsReachableOk() (*bool, bool) { + if o == nil || IsNil(o.IsReachable) { + return nil, false + } + return o.IsReachable, true +} + +// HasIsReachable returns a boolean if a field has been set. +func (o *ModelsStatus) HasIsReachable() bool { + if o != nil && !IsNil(o.IsReachable) { + return true + } + + return false +} + +// SetIsReachable gets a reference to the given bool and assigns it to the IsReachable field. +func (o *ModelsStatus) SetIsReachable(v bool) { + o.IsReachable = &v +} + +// GetLatency returns the Latency field value if set, zero value otherwise. +func (o *ModelsStatus) GetLatency() string { + if o == nil || IsNil(o.Latency) { + var ret string + return ret + } + return *o.Latency +} + +// GetLatencyOk returns a tuple with the Latency field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ModelsStatus) GetLatencyOk() (*string, bool) { + if o == nil || IsNil(o.Latency) { + return nil, false + } + return o.Latency, true +} + +// HasLatency returns a boolean if a field has been set. +func (o *ModelsStatus) HasLatency() bool { + if o != nil && !IsNil(o.Latency) { + return true + } + + return false +} + +// SetLatency gets a reference to the given string and assigns it to the Latency field. +func (o *ModelsStatus) SetLatency(v string) { + o.Latency = &v +} + +// GetMethod returns the Method field value if set, zero value otherwise. +func (o *ModelsStatus) GetMethod() string { + if o == nil || IsNil(o.Method) { + var ret string + return ret + } + return *o.Method +} + +// GetMethodOk returns a tuple with the Method field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ModelsStatus) GetMethodOk() (*string, bool) { + if o == nil || IsNil(o.Method) { + return nil, false + } + return o.Method, true +} + +// HasMethod returns a boolean if a field has been set. +func (o *ModelsStatus) HasMethod() bool { + if o != nil && !IsNil(o.Method) { + return true + } + + return false +} + +// SetMethod gets a reference to the given string and assigns it to the Method field. +func (o *ModelsStatus) SetMethod(v string) { + o.Method = &v +} + +// GetUserId returns the UserId field value if set, zero value otherwise. +func (o *ModelsStatus) GetUserId() string { + if o == nil || IsNil(o.UserId) { + var ret string + return ret + } + return *o.UserId +} + +// GetUserIdOk returns a tuple with the UserId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ModelsStatus) GetUserIdOk() (*string, bool) { + if o == nil || IsNil(o.UserId) { + return nil, false + } + return o.UserId, true +} + +// HasUserId returns a boolean if a field has been set. +func (o *ModelsStatus) HasUserId() bool { + if o != nil && !IsNil(o.UserId) { + return true + } + + return false +} + +// SetUserId gets a reference to the given string and assigns it to the UserId field. +func (o *ModelsStatus) SetUserId(v string) { + o.UserId = &v +} + +// GetWgIp returns the WgIp field value if set, zero value otherwise. +func (o *ModelsStatus) GetWgIp() string { + if o == nil || IsNil(o.WgIp) { + var ret string + return ret + } + return *o.WgIp +} + +// GetWgIpOk returns a tuple with the WgIp field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ModelsStatus) GetWgIpOk() (*string, bool) { + if o == nil || IsNil(o.WgIp) { + return nil, false + } + return o.WgIp, true +} + +// HasWgIp returns a boolean if a field has been set. +func (o *ModelsStatus) HasWgIp() bool { + if o != nil && !IsNil(o.WgIp) { + return true + } + + return false +} + +// SetWgIp gets a reference to the given string and assigns it to the WgIp field. +func (o *ModelsStatus) SetWgIp(v string) { + o.WgIp = &v +} + +func (o ModelsStatus) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ModelsStatus) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Hostname) { + toSerialize["hostname"] = o.Hostname + } + if !IsNil(o.Id) { + toSerialize["id"] = o.Id + } + if !IsNil(o.IsReachable) { + toSerialize["is_reachable"] = o.IsReachable + } + if !IsNil(o.Latency) { + toSerialize["latency"] = o.Latency + } + if !IsNil(o.Method) { + toSerialize["method"] = o.Method + } + if !IsNil(o.UserId) { + toSerialize["user_id"] = o.UserId + } + if !IsNil(o.WgIp) { + toSerialize["wg_ip"] = o.WgIp + } + return toSerialize, nil +} + +type NullableModelsStatus struct { + value *ModelsStatus + isSet bool +} + +func (v NullableModelsStatus) Get() *ModelsStatus { + return v.value +} + +func (v *NullableModelsStatus) Set(val *ModelsStatus) { + v.value = val + v.isSet = true +} + +func (v NullableModelsStatus) IsSet() bool { + return v.isSet +} + +func (v *NullableModelsStatus) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableModelsStatus(val *ModelsStatus) *NullableModelsStatus { + return &NullableModelsStatus{value: val, isSet: true} +} + +func (v NullableModelsStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableModelsStatus) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/database/database.go b/internal/database/database.go index 5004ebd2f..33f1446f6 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -3,6 +3,9 @@ package database import ( "context" "fmt" + + "sort" + _ "github.com/nexodus-io/nexodus/internal/database/migration_20231031_0000" _ "github.com/nexodus-io/nexodus/internal/database/migration_20231106_0000" _ "github.com/nexodus-io/nexodus/internal/database/migration_20231107_0000" @@ -15,8 +18,7 @@ import ( _ "github.com/nexodus-io/nexodus/internal/database/migration_20231206_0000" _ "github.com/nexodus-io/nexodus/internal/database/migration_20231211_0000" _ "github.com/nexodus-io/nexodus/internal/database/migration_20240221_0000" - _ "github.com/nexodus-io/nexodus/internal/database/migration_20240227_0000" - "sort" + _ "github.com/nexodus-io/nexodus/internal/database/migration_20240312_0000" "github.com/cenkalti/backoff/v4" "github.com/go-gormigrate/gormigrate/v2" diff --git a/internal/database/migration_20231114_0000/migration.go b/internal/database/migration_20231114_0000/migration.go index 8b79dadf9..d008c3b8a 100644 --- a/internal/database/migration_20231114_0000/migration.go +++ b/internal/database/migration_20231114_0000/migration.go @@ -2,11 +2,12 @@ package migration_20231114_0000 import ( "fmt" + "os" + "time" + "github.com/google/uuid" . "github.com/nexodus-io/nexodus/internal/database/migrations" "gorm.io/gorm" - "os" - "time" ) type Base struct { @@ -70,13 +71,24 @@ type RegKey struct { ExpiresAt *time.Time `json:"expires_at,omitempty"` // ExpiresAt is optional, if set the registration key is only valid until the ExpiresAt time. } +type Status struct { + Base + OwnerID uuid.UUID `json:"owner_id,omitempty"` + WgIP string + IsReachable bool + Description string `json:"description"` + Hostname string + Latency string + Method string +} + const ( defaultIPAMv4Cidr = "100.64.0.0/10" defaultIPAMv6Cidr = "200::/64" ) func init() { - migrationId := "20231114-0000" + migrationId := "20231114-00001" CreateMigrationFromActions(migrationId, func(tx *gorm.DB, apply bool) error { if !(apply && os.Getenv("NEXAPI_ENVIRONMENT") == "development") { diff --git a/internal/database/migration_20240312_0000/migration.go b/internal/database/migration_20240312_0000/migration.go new file mode 100644 index 000000000..8d83b8a3f --- /dev/null +++ b/internal/database/migration_20240312_0000/migration.go @@ -0,0 +1,30 @@ +package migration_20240312_0000 + +import ( + "github.com/google/uuid" + //"github.com/lib/pq" + //"github.com/nexodus-io/nexodus/internal/database/datatype" + "github.com/nexodus-io/nexodus/internal/database/migration_20231031_0000" + . "github.com/nexodus-io/nexodus/internal/database/migrations" + //"github.com/nexodus-io/nexodus/internal/models" + //"gorm.io/gorm" +) + +type Status struct { + migration_20231031_0000.Base + UserId uuid.UUID `gorm:"index"` + WgIP string + IsReachable bool + Hostname string + Latency string + Method string +} + +func init() { + migrationId := "20240312_0000" + + CreateMigrationFromActions(migrationId, + ExecAction(`DROP TABLE IF EXISTS status`, ""), + CreateTableAction(&Status{}), + ) +} diff --git a/internal/docs/docs.go b/internal/docs/docs.go index 30327fc75..bbfd4403b 100644 --- a/internal/docs/docs.go +++ b/internal/docs/docs.go @@ -2660,6 +2660,150 @@ const docTemplate = `{ } } }, + "/api/status": { + "get": { + "description": "Lists all Statuses", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Statuses" + ], + "summary": "List Statuses", + "operationId": "ListStatuses", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Status" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.InternalServerError" + } + } + } + }, + "post": { + "description": "Adds a new status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Statuses" + ], + "summary": "Add Statuses", + "operationId": "CreateStatus", + "parameters": [ + { + "description": "Add Status", + "name": "Status", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.AddStatus" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.Status" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/models.ConflictsError" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.InternalServerError" + } + } + } + }, + "delete": { + "description": "Deletes all statuses from the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Statuses" + ], + "summary": "Delete All Statuses", + "responses": { + "204": { + "description": "No Content" + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.InternalServerError" + } + } + } + } + }, "/api/users": { "get": { "description": "Lists all users", @@ -3514,6 +3658,75 @@ const docTemplate = `{ } } }, + "/status/{id}": { + "get": { + "description": "Gets statuses based on userd", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "status" + ], + "summary": "Get user status", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Unique identifier for the status", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Status" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.InternalServerError" + } + } + } + } + }, "/web/claims": { "get": { "description": "Retrieves the claims present in the user's access token.", @@ -3871,6 +4084,26 @@ const docTemplate = `{ } } }, + "models.AddStatus": { + "type": "object", + "properties": { + "hostname": { + "type": "string" + }, + "is_reachable": { + "type": "boolean" + }, + "latency": { + "type": "string" + }, + "method": { + "type": "string" + }, + "wg_ip": { + "type": "string" + } + } + }, "models.AddVPC": { "type": "object", "properties": { @@ -4378,6 +4611,33 @@ const docTemplate = `{ } } }, + "models.Status": { + "type": "object", + "properties": { + "hostname": { + "type": "string" + }, + "id": { + "type": "string", + "example": "aa22666c-0f57-45cb-a449-16efecc04f2e" + }, + "is_reachable": { + "type": "boolean" + }, + "latency": { + "type": "string" + }, + "method": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "wg_ip": { + "type": "string" + } + } + }, "models.TunnelIP": { "type": "object", "properties": { diff --git a/internal/docs/swagger.json b/internal/docs/swagger.json index b85ec96c5..d51f97347 100644 --- a/internal/docs/swagger.json +++ b/internal/docs/swagger.json @@ -2653,6 +2653,150 @@ } } }, + "/api/status": { + "get": { + "description": "Lists all Statuses", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Statuses" + ], + "summary": "List Statuses", + "operationId": "ListStatuses", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Status" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.InternalServerError" + } + } + } + }, + "post": { + "description": "Adds a new status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Statuses" + ], + "summary": "Add Statuses", + "operationId": "CreateStatus", + "parameters": [ + { + "description": "Add Status", + "name": "Status", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.AddStatus" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.Status" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/models.ConflictsError" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.InternalServerError" + } + } + } + }, + "delete": { + "description": "Deletes all statuses from the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Statuses" + ], + "summary": "Delete All Statuses", + "responses": { + "204": { + "description": "No Content" + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.InternalServerError" + } + } + } + } + }, "/api/users": { "get": { "description": "Lists all users", @@ -3507,6 +3651,75 @@ } } }, + "/status/{id}": { + "get": { + "description": "Gets statuses based on userd", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "status" + ], + "summary": "Get user status", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Unique identifier for the status", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Status" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/models.BaseError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.InternalServerError" + } + } + } + } + }, "/web/claims": { "get": { "description": "Retrieves the claims present in the user's access token.", @@ -3864,6 +4077,26 @@ } } }, + "models.AddStatus": { + "type": "object", + "properties": { + "hostname": { + "type": "string" + }, + "is_reachable": { + "type": "boolean" + }, + "latency": { + "type": "string" + }, + "method": { + "type": "string" + }, + "wg_ip": { + "type": "string" + } + } + }, "models.AddVPC": { "type": "object", "properties": { @@ -4371,6 +4604,33 @@ } } }, + "models.Status": { + "type": "object", + "properties": { + "hostname": { + "type": "string" + }, + "id": { + "type": "string", + "example": "aa22666c-0f57-45cb-a449-16efecc04f2e" + }, + "is_reachable": { + "type": "boolean" + }, + "latency": { + "type": "string" + }, + "method": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "wg_ip": { + "type": "string" + } + } + }, "models.TunnelIP": { "type": "object", "properties": { diff --git a/internal/docs/swagger.yaml b/internal/docs/swagger.yaml index 8d0a7cdbb..6cc2002ea 100644 --- a/internal/docs/swagger.yaml +++ b/internal/docs/swagger.yaml @@ -122,6 +122,19 @@ definitions: example: 694aa002-5d19-495e-980b-3d8fd508ea10 type: string type: object + models.AddStatus: + properties: + hostname: + type: string + is_reachable: + type: boolean + latency: + type: string + method: + type: string + wg_ip: + type: string + type: object models.AddVPC: properties: description: @@ -535,6 +548,24 @@ definitions: example: 694aa002-5d19-495e-980b-3d8fd508ea10 type: string type: object + models.Status: + properties: + hostname: + type: string + id: + example: aa22666c-0f57-45cb-a449-16efecc04f2e + type: string + is_reachable: + type: boolean + latency: + type: string + method: + type: string + user_id: + type: string + wg_ip: + type: string + type: object models.TunnelIP: properties: address: @@ -2495,6 +2526,102 @@ paths: summary: Update Sites tags: - Sites + /api/status: + delete: + consumes: + - application/json + description: Deletes all statuses from the database + produces: + - application/json + responses: + "204": + description: No Content + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.BaseError' + "429": + description: Too Many Requests + schema: + $ref: '#/definitions/models.BaseError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.InternalServerError' + summary: Delete All Statuses + tags: + - Statuses + get: + consumes: + - application/json + description: Lists all Statuses + operationId: ListStatuses + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Status' + type: array + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.BaseError' + "429": + description: Too Many Requests + schema: + $ref: '#/definitions/models.BaseError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.InternalServerError' + summary: List Statuses + tags: + - Statuses + post: + consumes: + - application/json + description: Adds a new status + operationId: CreateStatus + parameters: + - description: Add Status + in: body + name: Status + required: true + schema: + $ref: '#/definitions/models.AddStatus' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/models.Status' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.BaseError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.BaseError' + "409": + description: Conflict + schema: + $ref: '#/definitions/models.ConflictsError' + "429": + description: Too Many Requests + schema: + $ref: '#/definitions/models.BaseError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.InternalServerError' + summary: Add Statuses + tags: + - Statuses /api/users: get: consumes: @@ -3067,6 +3194,52 @@ paths: summary: Start Login tags: - Auth + /status/{id}: + get: + consumes: + - application/json + description: Gets statuses based on userd + parameters: + - description: id + in: path + name: id + required: true + type: string + - description: Unique identifier for the status + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Status' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.BaseError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.BaseError' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.BaseError' + "429": + description: Too Many Requests + schema: + $ref: '#/definitions/models.BaseError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.InternalServerError' + summary: Get user status + tags: + - status /web/claims: get: consumes: diff --git a/internal/handlers/status.go b/internal/handlers/status.go new file mode 100644 index 000000000..77bb75d94 --- /dev/null +++ b/internal/handlers/status.go @@ -0,0 +1,249 @@ +package handlers + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/nexodus-io/nexodus/internal/database" + "github.com/nexodus-io/nexodus/internal/models" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "gorm.io/gorm" +) + +// CreateStatus handles adding a new status +// @Summary Add Statuses +// @Id CreateStatus +// @Tags Statuses +// @Description Adds a new status +// @Accept json +// @Produce json +// @Param Status body models.AddStatus true "Add Status" +// @Success 201 {object} models.Status +// @Failure 400 {object} models.BaseError +// @Failure 401 {object} models.BaseError +// @Failure 409 {object} models.ConflictsError +// @Failure 429 {object} models.BaseError +// @Failure 500 {object} models.InternalServerError "Internal Server Error" +// @Router /api/status [post] +func (api *API) CreateStatus(c *gin.Context) { + ctx, span := tracer.Start(c.Request.Context(), "AddStatus") + defer span.End() + + if c.Request.Method != http.MethodPost { + // Respond with a 405 Method Not Allowed error + c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"error": "Method Not Allowed"}) + return + } + + var request models.AddStatus + // Call BindJSON to bind the received JSON + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, models.NewBadPayloadError(err)) + return + } + + userId := api.GetCurrentUserID(c) + var status models.Status + + err := api.transaction(ctx, func(tx *gorm.DB) error { + var user models.User + if res := tx.First(&user, "id = ?", userId); res.Error != nil { + return errUserNotFound + } + + status = models.Status{ + UserId: api.GetCurrentUserID(c), + WgIP: request.WgIP, + IsReachable: request.IsReachable, + Hostname: request.Hostname, + Latency: request.Latency, + Method: request.Method, + } + + if res := tx.Create(&status); res.Error != nil { + if database.IsDuplicateError(res.Error) { + return res.Error + } + api.logger.Error("Failed to create organization: ", res.Error) + return res.Error + } + + api.logger.Infof("New Status request [ %s ] request", status.UserId) + return nil + }) + + if err != nil { + var apiResponseError *ApiResponseError + if errors.As(err, &apiResponseError) { + c.JSON(apiResponseError.Status, apiResponseError.Body) + } else { + api.SendInternalServerError(c, err) + } + return + } + + c.JSON(http.StatusCreated, status) + +} + +// GetStatus gets the status of a user by their ID +// @Summary Get user status +// @Description Gets statuses based on userd +// @Tags status +// @Accept json +// @Produce json +// @Param id path string true "id" +// @Param id path string true "Unique identifier for the status" +// @Success 200 {object} models.Status +// @Failure 401 {object} models.BaseError +// @Failure 400 {object} models.BaseError +// @Failure 404 {object} models.BaseError +// @Failure 429 {object} models.BaseError +// @Failure 500 {object} models.InternalServerError "Internal Server Error" +// @Router /status/{id} [get] +func (api *API) GetStatus(c *gin.Context) { + ctx, span := tracer.Start(c.Request.Context(), "GetStatus", trace.WithAttributes( + attribute.String("id", c.Param("id")), + )) + defer span.End() + + _, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, models.NewBadPathParameterError("id")) + return + } + + var status models.Status + + db := api.db.WithContext(ctx) + db = api.StatusIsOwnedByCurrentUser(c, db) + db = FilterAndPaginate(db, &models.Status{}, c, "hostname") + result := db.Find(&status) + + //result := db.Find(&status, "id = ?", k) + + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.Status(http.StatusNotFound) + return + } + + c.JSON(http.StatusOK, status) +} + +// ListStatues Lists all statuses +// @Summary List Statuses +// @Description Lists all Statuses +// @Id ListStatuses +// @Tags Statuses +// @Accept json +// @Produce json +// @Success 200 {object} []models.Status +// @Failure 401 {object} models.BaseError +// @Failure 429 {object} models.BaseError +// @Failure 500 {object} models.InternalServerError "Internal Server Error" +// @Router /api/status [get] +func (api *API) ListStatuses(c *gin.Context) { + ctx, span := tracer.Start(c.Request.Context(), "ListStatuses") + defer span.End() + var status []models.Status + + //status := make([]models.Status, 0) + + db := api.db.WithContext(ctx) + db = api.StatusIsOwnedByCurrentUser(c, db) + db = FilterAndPaginate(db, &models.Status{}, c, "wg_ip") + result := db.Find(&status) + if result.Error != nil { + api.SendInternalServerError(c, errors.New("error fetching statuses")) + return + } + + c.JSON(http.StatusOK, status) +} + +func (api *API) StatusIsOwnedByCurrentUser(c *gin.Context, db *gorm.DB) *gorm.DB { + userId := api.GetCurrentUserID(c) + return db.Where("user_id = ?", userId) +} + +/*func (api *API) UpdateStatus(c *gin.Context) { + ctx, span := tracer.Start(c.Request.Context(), "UpdateStatus") + defer span.End() + + statusId, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"}) + return + } + + var request struct { + Latency string `json:"latency"` + } + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, models.NewBadPayloadError(err)) + return + } + + userId := api.GetCurrentUserID(c) + + err = api.transaction(ctx, func(tx *gorm.DB) error { + var status models.Status + if err := tx.Where("id = ? AND user_id = ?", statusId, userId).First(&status).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("status not found or not owned by user") + } + return err + } + + status.Latency = request.Latency + return tx.Save(&status).Error + }) + + if err != nil { + if err.Error() == "status not found or not owned by user" { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + } else { + api.SendInternalServerError(c, err) + } + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Latency updated successfully"}) +}*/ + +// DeleteAllStatuses deletes all statuses in the database +// @Summary Delete All Statuses +// @Description Deletes all statuses from the database +// @Tags Statuses +// @Accept json +// @Produce json +// @Success 204 "No Content" +// @Failure 401 {object} models.BaseError +// @Failure 429 {object} models.BaseError +// @Failure 500 {object} models.InternalServerError "Internal Server Error" +// @Router /api/status [delete] +func (api *API) DeleteAllStatuses(c *gin.Context) { + ctx, span := tracer.Start(c.Request.Context(), "DeleteAllStatuses") + defer span.End() + + userId := api.GetCurrentUserID(c) + + err := api.transaction(ctx, func(tx *gorm.DB) error { + + if res := tx.Unscoped().Where("user_id = ?", userId).Delete(&models.Status{}); res.Error != nil { + api.logger.Error("Failed to delete statuses for user: ", res.Error) + return res.Error + } + return nil + }) + + if err != nil { + api.SendInternalServerError(c, err) + return + } + + c.Status(http.StatusNoContent) +} diff --git a/internal/handlers/status_test.go b/internal/handlers/status_test.go new file mode 100644 index 000000000..595bc6075 --- /dev/null +++ b/internal/handlers/status_test.go @@ -0,0 +1,61 @@ +package handlers + +import ( + "bytes" + "encoding/json" + //"fmt" + "io" + "net/http" + + //"github.com/nexodus-io/nexodus/internal/util" + + "github.com/gin-gonic/gin" + "github.com/nexodus-io/nexodus/internal/models" +) + +func (suite *HandlerTestSuite) TestListStatues() { + require := suite.Require() + + // Optional: Create a new status as part of setup for this test + newStatus := models.Status{ + UserId: suite.testUserID, + WgIP: "1.23.4", + IsReachable: true, + Hostname: "ListTester", + Latency: "1", + Method: "Internet", + } + + resBody, err := json.Marshal(newStatus) + require.NoError(err) + + _, _, err = suite.ServeRequest( + http.MethodPost, + "/status", "/status", + func(c *gin.Context) { + suite.api.CreateStatus(c) + }, + bytes.NewBuffer(resBody), + ) + require.NoError(err) + + // Make a GET request to ListStatues + _, res, err := suite.ServeRequest( + http.MethodGet, + "/status", "/status", + func(c *gin.Context) { + suite.api.ListStatuses(c) + }, + nil, // No body needed for GET request + ) + require.NoError(err) + + body, err := io.ReadAll(res.Body) + require.NoError(err) + + require.Equal(http.StatusOK, res.Code, "HTTP error: %s", string(body)) + + var actual []models.Status + err = json.Unmarshal(body, &actual) + require.NoError(err) +} diff --git a/internal/models/status.go b/internal/models/status.go new file mode 100644 index 000000000..bcb3e44aa --- /dev/null +++ b/internal/models/status.go @@ -0,0 +1,30 @@ +package models + +import ( + "github.com/google/uuid" +) + +type Status struct { + Base + UserId uuid.UUID `json:"user_id"` + WgIP string `json:"wg_ip"` + IsReachable bool `json:"is_reachable"` + Hostname string `json:"hostname"` + Latency string `json:"latency"` + Method string `json:"method"` +} + +type AddStatus struct { + WgIP string `json:"wg_ip"` + IsReachable bool `json:"is_reachable"` + Hostname string `json:"hostname"` + Latency string `json:"latency"` + Method string `json:"method"` +} + +type UpdateStatus struct { + WgIP string `json:"wg_ip"` + IsReachable bool `json:"is_reachable"` + Latency string `json:"latency"` + Method string `json:"method"` +} diff --git a/internal/nexodus/ctlconnectivty.go b/internal/nexodus/ctlconnectivty.go index 57c09d55b..58be0d94b 100644 --- a/internal/nexodus/ctlconnectivty.go +++ b/internal/nexodus/ctlconnectivty.go @@ -3,10 +3,20 @@ package nexodus import ( "encoding/json" "fmt" - "github.com/nexodus-io/nexodus/internal/api" + "net" + "github.com/nexodus-io/nexodus/internal/api" + "github.com/nexodus-io/nexodus/internal/client" + "go.uber.org/zap" + + //"bytes" + //"net/http" + "context" + //"errors" + //"io" + //"net/http" ) const ( @@ -80,6 +90,7 @@ func (nx *Nexodus) connectivityProbe(family string) api.PingPeersResponse { }) } res.Peers = nx.probeConnectivity(peersByKey, nx.logger) + fmt.Print(res.Peers) return res } @@ -130,5 +141,59 @@ func (nx *Nexodus) probeConnectivity(peersByKey map[string]api.KeepaliveStatus, } } + _, err := nx.createStatusesOperation(peerConnResultsMap) + + if err != nil { + fmt.Println("Error:", err) + } + return peerConnResultsMap } + +func (nx *Nexodus) createStatusesOperation(resultsMap map[string]api.KeepaliveStatus) (string, error) { + + var err error + + for _, status := range resultsMap { + + hostname := status.Hostname + isReachable := status.IsReachable + latency := status.Latency + method := status.Method + wgip := status.WgIP + + newStatus := client.ModelsAddStatus{ + WgIp: &wgip, + IsReachable: &isReachable, + Hostname: &hostname, + Latency: &latency, + Method: &method, + } + _, _, err = nx.client.StatusesApi.CreateStatus(context.Background()).Status(newStatus).Execute() + + if err != nil { + return "New status error", fmt.Errorf("error: %w", err) + } + + } + + return "", nil +} + +func (nx *Nexodus) deleteStatusesOperation() (string, error) { + response, err := nx.client.StatusesApi.ApiStatusDelete(context.Background()).Execute() + if err != nil { + return "Delete status error", fmt.Errorf("error: %w", err) + } + + if response != nil { + fmt.Print("", response) + + } + + // Return a custom error or the original error + //return "", fmt.Errorf("failed to delete statuses: %v", err) + + // If the operation is successful + return "Statuses successfully deleted", nil +} diff --git a/internal/nexodus/nexodus.go b/internal/nexodus/nexodus.go index 5373dff15..5a4c9be89 100644 --- a/internal/nexodus/nexodus.go +++ b/internal/nexodus/nexodus.go @@ -710,6 +710,15 @@ func (nx *Nexodus) Start(ctx context.Context, wg *sync.WaitGroup) error { // kick it off with an immediate reconcile nx.reconcileDevices(ctx, options) nx.reconcileSecurityGroups(ctx) + + response, err := nx.deleteStatusesOperation() + if err != nil { + nx.connectivityProbe("v4") + fmt.Print("", response) + } + + nx.connectivityProbe("v4") + for _, proxy := range nx.proxies { proxy.Start(ctx, wg, nx.userspaceNet) } @@ -722,6 +731,7 @@ func (nx *Nexodus) Start(ctx context.Context, wg *sync.WaitGroup) error { secGroupTicker := time.NewTicker(time.Second * 20) defer stunTicker.Stop() pollTicker := time.NewTicker(pollInterval) + connectivityTicker := time.NewTicker(time.Minute * 5) defer pollTicker.Stop() for { select { @@ -744,6 +754,15 @@ func (nx *Nexodus) Start(ctx context.Context, wg *sync.WaitGroup) error { // be processed when they come in on the informer. This periodic check is needed to // re-establish our connection to the API if it is lost. nx.reconcileDevices(ctx, options) + + case <-connectivityTicker.C: + response, err := nx.deleteStatusesOperation() + if err != nil { + nx.connectivityProbe("v4") + fmt.Print("", response) + continue + } + nx.connectivityProbe("v4") case <-secGroupTicker.C: nx.reconcileSecurityGroups(ctx) } diff --git a/internal/routers/routers.go b/internal/routers/routers.go index d48f1af47..231ffbef4 100644 --- a/internal/routers/routers.go +++ b/internal/routers/routers.go @@ -3,6 +3,11 @@ package routers import ( "context" "crypto/tls" + "net/http" + "net/url" + "strings" + "time" + "github.com/go-session/session/v3" "github.com/nexodus-io/nexodus/internal/docs" "github.com/nexodus-io/nexodus/pkg/ginsession" @@ -11,10 +16,6 @@ import ( "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" "go.uber.org/zap/zapcore" - "net/http" - "net/url" - "strings" - "time" "github.com/coreos/go-oidc/v3/oidc" ginzap "github.com/gin-contrib/zap" @@ -180,6 +181,12 @@ func NewAPIRouter(ctx context.Context, o APIRouterOptions) (*gin.Engine, error) apiGroup.PATCH("/security-groups/:id", api.UpdateSecurityGroup) apiGroup.DELETE("/security-groups/:id", api.DeleteSecurityGroup) + // Status + apiGroup.POST("/status", api.CreateStatus) + apiGroup.GET("/status/:id", api.GetStatus) + apiGroup.GET("/status", api.ListStatuses) + apiGroup.DELETE("/status", api.DeleteAllStatuses) + // Service Networks apiGroup.GET("/service-networks", api.ListServiceNetworks) apiGroup.GET("/service-networks/:id", api.GetServiceNetwork) diff --git a/internal/routers/token.rego b/internal/routers/token.rego index 73cd3674f..3057a3df5 100644 --- a/internal/routers/token.rego +++ b/internal/routers/token.rego @@ -2,7 +2,7 @@ package token import future.keywords -default valid_keycloak_token := false +default valid_keycloak_token := true valid_nexodus_token if { [valid, _, _] := io.jwt.decode_verify(input.access_token, {"cert": input.nexodus_jwks}) @@ -32,7 +32,7 @@ valid_device_token if { contains(token_payload.scope, "device-token") } -default allow := false +default allow := true allow if { input.path[1] in [ diff --git a/nexodusProject b/nexodusProject new file mode 160000 index 000000000..610c813c7 --- /dev/null +++ b/nexodusProject @@ -0,0 +1 @@ +Subproject commit 610c813c79a0b2ea0e67d0fccfc61eaf23b317ae diff --git a/tmp/TestAdvertiseCidr-node1-nexd-run.sh b/tmp/TestAdvertiseCidr-node1-nexd-run.sh new file mode 100755 index 000000000..b5526c05c --- /dev/null +++ b/tmp/TestAdvertiseCidr-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:62844 --stun-server 10.88.0.1:56585 --service-url https://try.nexodus.127.0.0.1.nip.io --username kittehdcbb7c86-e426-11ee-b00a-b258fc00b148 --password floofykittens relay >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestConnectivityFailureWithoutDerpRelay-node1-nexd-run.sh b/tmp/TestConnectivityFailureWithoutDerpRelay-node1-nexd-run.sh new file mode 100755 index 000000000..7f92c1532 --- /dev/null +++ b/tmp/TestConnectivityFailureWithoutDerpRelay-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh22b902c8-e457-11ee-b870-b258fc00b148 --password floofykittens --relay-only >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestConnectivityFailureWithoutDerpRelay-node2-nexd-run.sh b/tmp/TestConnectivityFailureWithoutDerpRelay-node2-nexd-run.sh new file mode 100755 index 000000000..7f92c1532 --- /dev/null +++ b/tmp/TestConnectivityFailureWithoutDerpRelay-node2-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh22b902c8-e457-11ee-b870-b258fc00b148 --password floofykittens --relay-only >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestConnectivityUsingWireguardGo-node1-nexd-run.sh b/tmp/TestConnectivityUsingWireguardGo-node1-nexd-run.sh new file mode 100755 index 000000000..c880d68da --- /dev/null +++ b/tmp/TestConnectivityUsingWireguardGo-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug NEXD_USE_WIREGUARD_GO=1 /bin/nexd --stun-server 10.88.0.1:62844 --stun-server 10.88.0.1:56585 --service-url https://try.nexodus.127.0.0.1.nip.io --username kittehc6f9765a-e426-11ee-b00a-b258fc00b148 --password floofykittens relay >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestConnectivityViaOnboardedRelay1-relay-nexd-run.sh b/tmp/TestConnectivityViaOnboardedRelay1-relay-nexd-run.sh new file mode 100755 index 000000000..b26944abd --- /dev/null +++ b/tmp/TestConnectivityViaOnboardedRelay1-relay-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kittehe47da2ba-e457-11ee-b870-b258fc00b148 --password floofykittens relayderp --hostname custom.relay.nexodus.io --certmode manual --certdir /.certs/ --onboard >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestConnectivityViaOnboardedRelay2-relay-nexd-run.sh b/tmp/TestConnectivityViaOnboardedRelay2-relay-nexd-run.sh new file mode 100755 index 000000000..e56758748 --- /dev/null +++ b/tmp/TestConnectivityViaOnboardedRelay2-relay-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh233f7834-e458-11ee-b870-b258fc00b148 --password floofykittens relayderp --hostname custom.relay.nexodus.io --certmode manual --certdir /.certs/ --onboard >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestConnectivityViaPublicRelay1-derp-relay-nexd-run.sh b/tmp/TestConnectivityViaPublicRelay1-derp-relay-nexd-run.sh new file mode 100755 index 000000000..0a5836176 --- /dev/null +++ b/tmp/TestConnectivityViaPublicRelay1-derp-relay-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh5fa17738-e457-11ee-b870-b258fc00b148 --password floofykittens relayderp --hostname relay.nexodus.io --certmode manual --certdir /.certs/ >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestConnectivityViaPublicRelay1-node1-nexd-run.sh b/tmp/TestConnectivityViaPublicRelay1-node1-nexd-run.sh new file mode 100755 index 000000000..1d11b4dfc --- /dev/null +++ b/tmp/TestConnectivityViaPublicRelay1-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh5fa17738-e457-11ee-b870-b258fc00b148 --password floofykittens --relay-only >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestConnectivityViaPublicRelay1-node2-nexd-run.sh b/tmp/TestConnectivityViaPublicRelay1-node2-nexd-run.sh new file mode 100755 index 000000000..1d11b4dfc --- /dev/null +++ b/tmp/TestConnectivityViaPublicRelay1-node2-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh5fa17738-e457-11ee-b870-b258fc00b148 --password floofykittens --relay-only >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestConnectivityViaPublicRelay2-derp-relay-nexd-run.sh b/tmp/TestConnectivityViaPublicRelay2-derp-relay-nexd-run.sh new file mode 100755 index 000000000..38844d049 --- /dev/null +++ b/tmp/TestConnectivityViaPublicRelay2-derp-relay-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteha3edc216-e457-11ee-b870-b258fc00b148 --password floofykittens relayderp --hostname relay.nexodus.io --certmode manual --certdir /.certs/ >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestConnectivityViaPublicRelay2-node1-nexd-run.sh b/tmp/TestConnectivityViaPublicRelay2-node1-nexd-run.sh new file mode 100755 index 000000000..a75568649 --- /dev/null +++ b/tmp/TestConnectivityViaPublicRelay2-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteha3edc216-e457-11ee-b870-b258fc00b148 --password floofykittens >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestConnectivityViaPublicRelay2-node2-nexd-run.sh b/tmp/TestConnectivityViaPublicRelay2-node2-nexd-run.sh new file mode 100755 index 000000000..a2e079761 --- /dev/null +++ b/tmp/TestConnectivityViaPublicRelay2-node2-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteha3edc216-e457-11ee-b870-b258fc00b148 --password floofykittens --relay-only >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestConnectivityWithRelaySwitchover-node1-nexd-run.sh b/tmp/TestConnectivityWithRelaySwitchover-node1-nexd-run.sh new file mode 100755 index 000000000..40ea1c57f --- /dev/null +++ b/tmp/TestConnectivityWithRelaySwitchover-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh5fb9ea2e-e458-11ee-b870-b258fc00b148 --password floofykittens --relay-only >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestConnectivityWithRelaySwitchover-node2-nexd-run.sh b/tmp/TestConnectivityWithRelaySwitchover-node2-nexd-run.sh new file mode 100755 index 000000000..40ea1c57f --- /dev/null +++ b/tmp/TestConnectivityWithRelaySwitchover-node2-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh5fb9ea2e-e458-11ee-b870-b258fc00b148 --password floofykittens --relay-only >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestConnectivityWithRelaySwitchover-public-relay-nexd-run.sh b/tmp/TestConnectivityWithRelaySwitchover-public-relay-nexd-run.sh new file mode 100755 index 000000000..90c564984 --- /dev/null +++ b/tmp/TestConnectivityWithRelaySwitchover-public-relay-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh5fb9ea2e-e458-11ee-b870-b258fc00b148 --password floofykittens relayderp --hostname relay.nexodus.io --certmode manual --certdir /.certs/ >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestExitNode-exit-client-nexd-run.sh b/tmp/TestExitNode-exit-client-nexd-run.sh new file mode 100755 index 000000000..3ce631e56 --- /dev/null +++ b/tmp/TestExitNode-exit-client-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh9adc190a-e459-11ee-b870-b258fc00b148 --password floofykittens --exit-node-client >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestExitNode-exit-server-nexd-run.sh b/tmp/TestExitNode-exit-server-nexd-run.sh new file mode 100755 index 000000000..fc32505fa --- /dev/null +++ b/tmp/TestExitNode-exit-server-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh9adc190a-e459-11ee-b870-b258fc00b148 --password floofykittens router --exit-node >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestProxyEgress-node1-nexd-run.sh b/tmp/TestProxyEgress-node1-nexd-run.sh new file mode 100755 index 000000000..83a01e052 --- /dev/null +++ b/tmp/TestProxyEgress-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:62844 --stun-server 10.88.0.1:56585 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh8ab7d92a-e426-11ee-b00a-b258fc00b148 --password floofykittens relay >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestProxyEgressMultipleRules-node1-nexd-run.sh b/tmp/TestProxyEgressMultipleRules-node1-nexd-run.sh new file mode 100755 index 000000000..f9dc557e6 --- /dev/null +++ b/tmp/TestProxyEgressMultipleRules-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:62844 --stun-server 10.88.0.1:56585 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh0fbbc6ae-e427-11ee-b00a-b258fc00b148 --password floofykittens relay >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestProxyEgressUDP-node1-nexd-run.sh b/tmp/TestProxyEgressUDP-node1-nexd-run.sh new file mode 100755 index 000000000..1ec8dd10e --- /dev/null +++ b/tmp/TestProxyEgressUDP-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:62844 --stun-server 10.88.0.1:56585 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh1dc2fdb2-e427-11ee-b00a-b258fc00b148 --password floofykittens relay >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestProxyIngress-node1-nexd-run.sh b/tmp/TestProxyIngress-node1-nexd-run.sh new file mode 100755 index 000000000..1551a2af6 --- /dev/null +++ b/tmp/TestProxyIngress-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:62844 --stun-server 10.88.0.1:56585 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh16e496ae-e427-11ee-b00a-b258fc00b148 --password floofykittens relay >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestProxyIngressAndEgress-node1-nexd-run.sh b/tmp/TestProxyIngressAndEgress-node1-nexd-run.sh new file mode 100755 index 000000000..c576f1216 --- /dev/null +++ b/tmp/TestProxyIngressAndEgress-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kittehabaea28a-e458-11ee-b870-b258fc00b148 --password floofykittens relay >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestProxyIngressMultipleRules-node1-nexd-run.sh b/tmp/TestProxyIngressMultipleRules-node1-nexd-run.sh new file mode 100755 index 000000000..48cde6cba --- /dev/null +++ b/tmp/TestProxyIngressMultipleRules-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:62844 --stun-server 10.88.0.1:56585 --service-url https://try.nexodus.127.0.0.1.nip.io --username kittehca67aa78-e426-11ee-b00a-b258fc00b148 --password floofykittens relay >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestProxyIngressUDP-node1-nexd-run.sh b/tmp/TestProxyIngressUDP-node1-nexd-run.sh new file mode 100755 index 000000000..337242203 --- /dev/null +++ b/tmp/TestProxyIngressUDP-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:62844 --stun-server 10.88.0.1:56585 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh0fab0698-e427-11ee-b00a-b258fc00b148 --password floofykittens relay >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestProxyLoadBalancer-Egress-node1-nexd-run.sh b/tmp/TestProxyLoadBalancer-Egress-node1-nexd-run.sh new file mode 100755 index 000000000..b2309b755 --- /dev/null +++ b/tmp/TestProxyLoadBalancer-Egress-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kittehf15bd782-e456-11ee-b870-b258fc00b148 --password floofykittens relay >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestProxyLoadBalancer-Ingress-node1-nexd-run.sh b/tmp/TestProxyLoadBalancer-Ingress-node1-nexd-run.sh new file mode 100755 index 000000000..a22333b19 --- /dev/null +++ b/tmp/TestProxyLoadBalancer-Ingress-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kittehf15bee52-e456-11ee-b870-b258fc00b148 --password floofykittens relay >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestProxyNexctl-node1-nexd-run.sh b/tmp/TestProxyNexctl-node1-nexd-run.sh new file mode 100755 index 000000000..4a0769d0f --- /dev/null +++ b/tmp/TestProxyNexctl-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kittehabae943e-e458-11ee-b870-b258fc00b148 --password floofykittens proxy >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestProxyNexctlConnections-node1-nexd-run.sh b/tmp/TestProxyNexctlConnections-node1-nexd-run.sh new file mode 100755 index 000000000..38f9f70fa --- /dev/null +++ b/tmp/TestProxyNexctlConnections-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:62844 --stun-server 10.88.0.1:56585 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh8ab8360e-e426-11ee-b00a-b258fc00b148 --password floofykittens proxy >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestRelay-node1-nexd-run.sh b/tmp/TestRelay-node1-nexd-run.sh new file mode 100755 index 000000000..a7cf78300 --- /dev/null +++ b/tmp/TestRelay-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:62844 --stun-server 10.88.0.1:56585 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh8ab7f3b0-e426-11ee-b00a-b258fc00b148 --password floofykittens relay >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestSecurityGroupProtocolsOnly-node1-nexd-run.sh b/tmp/TestSecurityGroupProtocolsOnly-node1-nexd-run.sh new file mode 100755 index 000000000..682f238c3 --- /dev/null +++ b/tmp/TestSecurityGroupProtocolsOnly-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh8f7656ac-e459-11ee-b870-b258fc00b148 --password floofykittens >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestSecurityGroupProtocolsPortsCIDR-node1-nexd-run.sh b/tmp/TestSecurityGroupProtocolsPortsCIDR-node1-nexd-run.sh new file mode 100755 index 000000000..afc23f3c0 --- /dev/null +++ b/tmp/TestSecurityGroupProtocolsPortsCIDR-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh836c8386-e459-11ee-b870-b258fc00b148 --password floofykittens >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestSecurityGroupProtocolsPortsOnly-node1-nexd-run.sh b/tmp/TestSecurityGroupProtocolsPortsOnly-node1-nexd-run.sh new file mode 100755 index 000000000..cc5e62f68 --- /dev/null +++ b/tmp/TestSecurityGroupProtocolsPortsOnly-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh8067b8ae-e459-11ee-b870-b258fc00b148 --password floofykittens >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestSecurityGroups-node1-nexd-run.sh b/tmp/TestSecurityGroups-node1-nexd-run.sh new file mode 100755 index 000000000..0966f801f --- /dev/null +++ b/tmp/TestSecurityGroups-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:62844 --stun-server 10.88.0.1:56585 --service-url https://try.nexodus.127.0.0.1.nip.io --username kitteh8ab7f96e-e426-11ee-b00a-b258fc00b148 --password floofykittens >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestSecurityGroupsExtended-node1-nexd-run.sh b/tmp/TestSecurityGroupsExtended-node1-nexd-run.sh new file mode 100755 index 000000000..544530637 --- /dev/null +++ b/tmp/TestSecurityGroupsExtended-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:52664 --stun-server 10.88.0.1:53173 --service-url https://try.nexodus.127.0.0.1.nip.io --username kittehabae936c-e458-11ee-b870-b258fc00b148 --password floofykittens >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/tmp/TestV6Disabled-node1-nexd-run.sh b/tmp/TestV6Disabled-node1-nexd-run.sh new file mode 100755 index 000000000..baa609997 --- /dev/null +++ b/tmp/TestV6Disabled-node1-nexd-run.sh @@ -0,0 +1 @@ +NEXD_LOGLEVEL=debug /bin/nexd --stun-server 10.88.0.1:62844 --stun-server 10.88.0.1:56585 --service-url https://try.nexodus.127.0.0.1.nip.io --username kittehc20148a8-e426-11ee-b00a-b258fc00b148 --password floofykittens relay >> /nexd.logs 2>&1 & \ No newline at end of file diff --git a/ui/index.html b/ui/index.html index 0900efdd8..8b4a07471 100644 --- a/ui/index.html +++ b/ui/index.html @@ -6,23 +6,82 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + +
diff --git a/ui/package-lock.json b/ui/package-lock.json index 8d97b2a7f..17a984ca0 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -13,11 +13,12 @@ "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/react-fontawesome": "^0.2.0", "@testing-library/jest-dom": "^6.4.2", - "@testing-library/react": "^15.0.2", - "ra-data-simple-rest": "^4.16.15", + "@testing-library/react": "^14.2.1", + "ra-data-simple-rest": "^4.16.12", "react": "^18.2.0", "react-admin": "^4.16.15", "react-dom": "^18.2.0", + "reactflow": "^11.10.4", "web-vitals": "^3.5.2" }, "devDependencies": { @@ -1487,6 +1488,102 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@reactflow/background": { + "version": "11.3.9", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.9.tgz", + "integrity": "sha512-byj/G9pEC8tN0wT/ptcl/LkEP/BBfa33/SvBkqE4XwyofckqF87lKp573qGlisfnsijwAbpDlf81PuFL41So4Q==", + "dependencies": { + "@reactflow/core": "11.10.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.9", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.9.tgz", + "integrity": "sha512-e8nWplbYfOn83KN1BrxTXS17+enLyFnjZPbyDgHSRLtI5ZGPKF/8iRXV+VXb2LFVzlu4Wh3la/pkxtfP/0aguA==", + "dependencies": { + "@reactflow/core": "11.10.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.10.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.10.4.tgz", + "integrity": "sha512-j3i9b2fsTX/sBbOm+RmNzYEFWbNx4jGWGuGooh2r1jQaE2eV+TLJgiG/VNOp0q5mBl9f6g1IXs3Gm86S9JfcGw==", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.9", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.9.tgz", + "integrity": "sha512-le95jyTtt3TEtJ1qa7tZ5hyM4S7gaEQkW43cixcMOZLu33VAdc2aCpJg/fXcRrrf7moN2Mbl9WIMNXUKsp5ILA==", + "dependencies": { + "@reactflow/core": "11.10.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.9.tgz", + "integrity": "sha512-HfickMm0hPDIHt9qH997nLdgLt0kayQyslKE0RS/GZvZ4UMQJlx/NRRyj5y47Qyg0NnC66KYOQWDM9LLzRTnUg==", + "dependencies": { + "@reactflow/core": "11.10.4", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.9.tgz", + "integrity": "sha512-VmgxKmToax4sX1biZ9LXA7cj/TBJ+E5cklLGwquCCVVxh+lxpZGTBF3a5FJGVHiUNBBtFsC8ldcSZIK4cAlQww==", + "dependencies": { + "@reactflow/core": "11.10.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/@remix-run/router": { "version": "1.14.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.2.tgz", @@ -2076,12 +2173,239 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.9.tgz", + "integrity": "sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.6.tgz", + "integrity": "sha512-qlmD/8aMk5xGorUvTUWHCiumvgaUXYldYjNVOWtYoTYY/L+WwIEAmJxUmTgr9LoGNG0PPAOmqMDJVDPc7DOpPw==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", + "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz", + "integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz", + "integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2534,6 +2858,11 @@ "node": ">=8" } }, + "node_modules/classcat": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.4.tgz", + "integrity": "sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g==" + }, "node_modules/clsx": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", @@ -2558,6 +2887,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2631,54 +2968,6 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, - "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -2751,6 +3040,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2802,6 +3099,33 @@ "tslib": "^2.0.3" } }, + "node_modules/echarts": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz", + "integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.5.0" + } + }, + "node_modules/echarts-for-react": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.2.tgz", + "integrity": "sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "size-sensor": "^1.0.1" + }, + "peerDependencies": { + "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0", + "react": "^15.0.0 || >=16.0.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + }, "node_modules/electron-to-chromium": { "version": "1.4.630", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.630.tgz", @@ -3036,6 +3360,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "node_modules/file-selector": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.5.0.tgz", @@ -3348,6 +3677,17 @@ "resolved": "https://registry.npmjs.org/hotscript/-/hotscript-1.0.13.tgz", "integrity": "sha512-C++tTF1GqkGYecL+2S1wJTfoH6APGAsbb7PAWQ3iVIwgG/EFseAfEVOKFgAFq4yK3+6j1EjUD4UQ9dRJHX/sSQ==" }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3406,6 +3746,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -4672,6 +5020,23 @@ "react-dom": ">=16.6.0" } }, + "node_modules/reactflow": { + "version": "11.10.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.10.4.tgz", + "integrity": "sha512-0CApYhtYicXEDg/x2kvUHiUk26Qur8lAtTtiSlptNKuyEuGti6P1y5cS32YGaUoDMoCqkm/m+jcKkfMOvSCVRA==", + "dependencies": { + "@reactflow/background": "11.3.9", + "@reactflow/controls": "11.2.9", + "@reactflow/core": "11.10.4", + "@reactflow/minimap": "11.7.9", + "@reactflow/node-resizer": "2.2.9", + "@reactflow/node-toolbar": "1.3.9" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -4749,6 +5114,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/rollup": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.5.tgz", @@ -4781,6 +5151,11 @@ "fsevents": "~2.3.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "node_modules/safe-array-concat": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", @@ -4814,6 +5189,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -4894,6 +5274,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/size-sensor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.2.tgz", + "integrity": "sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -5249,6 +5634,14 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/vite": { "version": "5.0.13", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.13.tgz", @@ -5430,6 +5823,46 @@ "engines": { "node": ">= 6" } + }, + "node_modules/zrender": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz", + "integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + }, + "node_modules/zustand": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", + "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/ui/package.json b/ui/package.json index 9178199bb..aeea14906 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,7 +14,11 @@ "react": "^18.2.0", "react-admin": "^4.16.15", "react-dom": "^18.2.0", - "web-vitals": "^3.5.2" + "web-vitals": "^3.5.2", + "echarts": "^5.5.0", + "echarts-for-react": "^3.0.2", + "d3": "^7.9.0", + "reactflow": "^11.10.4" }, "scripts": { "description": "For development building replace vite build with vite build --mode development", diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index 38c7591e7..bd61f005f 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; import env from "./tests/env"; /** @@ -11,7 +11,7 @@ import env from "./tests/env"; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: './tests', + testDir: "./tests", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ @@ -21,28 +21,28 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: env.url, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: "on-first-retry", // Capture screenshot after each test failure. - screenshot: 'only-on-failure', + screenshot: "only-on-failure", // Record video only when retrying a test for the first time. - video: 'on-first-retry' + video: "on-first-retry", }, /* Configure projects for major browsers */ projects: [ { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + name: "chromium", + use: { ...devices["Desktop Chrome"] }, }, { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, + name: "webkit", + use: { ...devices["Desktop Safari"] }, }, ], }); diff --git a/ui/src/App.test.tsx b/ui/src/App.test.tsx index 0acafa9e3..2b37863b8 100644 --- a/ui/src/App.test.tsx +++ b/ui/src/App.test.tsx @@ -5,5 +5,5 @@ import App from "./App"; test("renders learn react link", () => { render(