diff --git a/backend/services/controller/internal/api/api.go b/backend/services/controller/internal/api/api.go index 404e900..d0b6898 100644 --- a/backend/services/controller/internal/api/api.go +++ b/backend/services/controller/internal/api/api.go @@ -17,37 +17,30 @@ import ( ) type Api struct { - port string - js jetstream.JetStream - nc *nats.Conn - bridge bridge.Bridge - db db.Database - kv jetstream.KeyValue - ctx context.Context - enterpise config.Enterprise + port string + js jetstream.JetStream + nc *nats.Conn + bridge bridge.Bridge + db db.Database + kv jetstream.KeyValue + ctx context.Context } const REQUEST_TIMEOUT = time.Second * 30 func NewApi(c *config.Config, js jetstream.JetStream, nc *nats.Conn, bridge bridge.Bridge, d db.Database, kv jetstream.KeyValue) Api { return Api{ - port: c.RestApi.Port, - js: js, - nc: nc, - ctx: c.RestApi.Ctx, - bridge: bridge, - db: d, - kv: kv, - enterpise: c.Enterprise, + port: c.RestApi.Port, + js: js, + nc: nc, + ctx: c.RestApi.Ctx, + bridge: bridge, + db: d, + kv: kv, } } func (a *Api) StartApi() { - - if a.enterpise.SupportPassword != "" && a.enterpise.SupportEmail != "" { - go registerEnterpriseSupport(a.enterpise.SupportEmail, a.enterpise.SupportPassword, a.db) - } - r := mux.NewRouter() authentication := r.PathPrefix("/api/auth").Subrouter() authentication.HandleFunc("/login", a.generateToken).Methods("PUT") @@ -57,13 +50,6 @@ func (a *Api) StartApi() { authentication.HandleFunc("/password", a.changePassword).Methods("PUT") authentication.HandleFunc("/admin/register", a.registerAdminUser).Methods("POST") authentication.HandleFunc("/admin/exists", a.adminUserExists).Methods("GET") - if a.enterpise.Enable { - mapRoutes := r.PathPrefix("/api/map").Subrouter() - mapRoutes.HandleFunc("", a.devicesLocation).Methods("GET") - mapRoutes.Use(func(handler http.Handler) http.Handler { - return middleware.Middleware(handler) - }) - } iot := r.PathPrefix("/api/device").Subrouter() iot.HandleFunc("/alias", a.setDeviceAlias).Methods("PUT") iot.HandleFunc("/auth", a.deviceAuth).Methods("GET", "POST", "DELETE") @@ -74,7 +60,7 @@ func (a *Api) StartApi() { iot.HandleFunc("/cwmp/{sn}/addObject", a.cwmpAddObjectMsg).Methods("PUT") iot.HandleFunc("/cwmp/{sn}/deleteObject", a.cwmpDeleteObjectMsg).Methods("PUT") iot.HandleFunc("", a.retrieveDevices).Methods("GET") - iot.HandleFunc("/{id}", a.retrieveDevices).Methods("GET") + iot.HandleFunc("/filterOptions", a.filterOptions).Methods("GET") iot.HandleFunc("/{sn}/{mtp}/get", a.deviceGetMsg).Methods("PUT") iot.HandleFunc("/{sn}/{mtp}/add", a.deviceCreateMsg).Methods("PUT") iot.HandleFunc("/{sn}/{mtp}/del", a.deviceDeleteMsg).Methods("PUT") @@ -84,13 +70,6 @@ func (a *Api) StartApi() { iot.HandleFunc("/{sn}/{mtp}/instances", a.deviceGetParameterInstances).Methods("PUT") iot.HandleFunc("/{sn}/{mtp}/operate", a.deviceOperateMsg).Methods("PUT") iot.HandleFunc("/{sn}/{mtp}/fw_update", a.deviceFwUpdate).Methods("PUT") //TODO: put it to work and generalize for usp and cwmp - if a.enterpise.Enable { - iot.HandleFunc("/{sn}/sitesurvey", a.deviceSiteSurvey).Methods("GET") - iot.HandleFunc("/{sn}/connecteddevices", a.deviceConnectedDevices).Methods("GET") - iot.HandleFunc("/{sn}/traceroute", a.deviceTraceRoute).Methods("GET", "PUT") - iot.HandleFunc("/{sn}/speedtest", a.deviceSpeedTest).Methods("PUT") - iot.HandleFunc("/{sn}/ping", a.devicePing).Methods("PUT", "GET") - } iot.HandleFunc("/{sn}/wifi", a.deviceWifi).Methods("PUT", "GET") dash := r.PathPrefix("/api/info").Subrouter() dash.HandleFunc("/vendors", a.vendorsInfo).Methods("GET") @@ -131,32 +110,3 @@ func (a *Api) StartApi() { }() log.Println("Running REST API at port", a.port) } - -func registerEnterpriseSupport(email, password string, d db.Database) { - - user := db.User{ - Email: email, - Password: password, - Name: "Enterprise Support", - Level: db.AdminUser, - } - - for { - if err := user.HashPassword(password); err != nil { - return - } - - err := d.RegisterUser(user) - if err != nil { - if err == db.ErrorUserExists { - log.Println("Enterprise support user already registered.") - return - } - log.Println("Error to register enterprise support user:", err) - time.Sleep(time.Second * 5) - continue - } - log.Println("Enterprise support user registered successfully.") - return - } -} diff --git a/backend/services/controller/internal/api/device.go b/backend/services/controller/internal/api/device.go index 527886c..a24485f 100644 --- a/backend/services/controller/internal/api/device.go +++ b/backend/services/controller/internal/api/device.go @@ -6,9 +6,11 @@ import ( "log" "net/http" "strconv" + "strings" "github.com/leandrofars/oktopus/internal/bridge" "github.com/leandrofars/oktopus/internal/db" + "github.com/leandrofars/oktopus/internal/entity" local "github.com/leandrofars/oktopus/internal/nats" "github.com/leandrofars/oktopus/internal/utils" "github.com/nats-io/nats.go/jetstream" @@ -16,6 +18,33 @@ import ( ) func (a *Api) retrieveDevices(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodDelete { + id := r.URL.Query().Get("id") + if id == "" { + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode("No id provided") + if err != nil { + log.Println(err) + } + return + } + + ids := strings.Split(id, ",") + + msg, err := bridge.NatsReq[int64](local.NATS_ADAPTER_SUBJECT+"devices.delete", utils.Marshall(ids), w, a.nc) + if err != nil { + return + } + + err = json.NewEncoder(w).Encode(map[string]interface{}{ + "number_of_deleted_devices": msg.Msg, + }) + if err != nil { + log.Println(err) + } + return + } + const PAGE_SIZE_LIMIT = 50 const PAGE_SIZE_DEFAULT = 20 @@ -33,9 +62,36 @@ func (a *Api) retrieveDevices(w http.ResponseWriter, r *http.Request) { return } + statusOrderFromUser := r.URL.Query().Get("statusOrder") + var statusOrder int + if statusOrderFromUser != "" { + if statusOrderFromUser == "asc" { + statusOrder = 1 + } else if statusOrderFromUser == "desc" { + statusOrder = -1 + } else { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode("Status order must be 'asc' or 'desc'") + return + } + } else { + statusOrder = 1 + } + + sort := bson.M{} + sort["status"] = statusOrder + + version := r.URL.Query().Get("version") + vendor := r.URL.Query().Get("vendor") + productClass := r.URL.Query().Get("type") + alias := r.URL.Query().Get("alias") + model := r.URL.Query().Get("model") + status := r.URL.Query().Get("status") + // Get devices with pagination page_n := r.URL.Query().Get("page_number") page_s := r.URL.Query().Get("page_size") + var err error var page_number int64 @@ -70,52 +126,69 @@ func (a *Api) retrieveDevices(w http.ResponseWriter, r *http.Request) { page_size = PAGE_SIZE_DEFAULT } - total, err := getDeviceCount(w, a.nc) - if err != nil { - return - } - skip := page_number * (page_size - 1) - if total < page_size { - skip = 0 + + filter := map[string]interface{}{ + "status_order": statusOrder, + "limit": page_size, + "skip": skip, } - //TODO: fix status ordering - statusOrder := r.URL.Query().Get("status") - if statusOrder != "" { - if statusOrder == "asc" { - statusOrder = "1" - } else if statusOrder == "desc" { - statusOrder = "-1" - } else { + if version != "" { + filter["version"] = version + } + if vendor != "" { + filter["vendor"] = vendor + } + if productClass != "" { + filter["productClass"] = productClass + } + if alias != "" { + filter["alias"] = alias + } + if model != "" { + filter["model"] = model + } + if status != "" { + fmtStatus, err := strconv.Atoi(status) + if err != nil { w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode("Status order must be 'asc' or 'desc'") + json.NewEncoder(w).Encode("Status must be an integer") return } - } - - sort := bson.M{} - sort["status"] = 1 - - //TODO: Create filters - - filter := bson.A{ - //bson.M{"$match": filter}, - bson.M{"$sort": sort}, // shows online devices first - bson.M{"$skip": skip}, - bson.M{"$limit": page_size}, + filter["status"] = fmtStatus } devices, err := getDevices(w, filter, a.nc) if err != nil { + log.Println("Error getting devices", err) + return + } + + if devices.Total == 0 { + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode("No devices found") + if err != nil { + log.Println(err) + } + return + } + + if skip >= devices.Total { + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode("Page number is out of range") + if err != nil { + log.Println(err) + } return } err = json.NewEncoder(w).Encode(map[string]interface{}{ - "pages": total / page_size, + "pages": devices.Total / page_size, "page": page_number, "size": page_size, - "devices": devices, + "devices": devices.Devices, + "total": devices.Total, }) if err != nil { log.Println(err) @@ -283,3 +356,14 @@ func (a *Api) setDeviceAlias(w http.ResponseWriter, r *http.Request) { return } } + +func (a *Api) filterOptions(w http.ResponseWriter, r *http.Request) { + + resp, err := bridge.NatsReq[entity.FilterOptions](local.NATS_ADAPTER_SUBJECT+"devices.filterOptions", nil, w, a.nc) + if err != nil { + return + } + + w.WriteHeader(resp.Code) + w.Write(utils.Marshall(resp.Msg)) +} diff --git a/backend/services/controller/internal/api/enterprise.go b/backend/services/controller/internal/api/enterprise.go deleted file mode 100644 index 1e09d44..0000000 --- a/backend/services/controller/internal/api/enterprise.go +++ /dev/null @@ -1,205 +0,0 @@ -package api - -import ( - "io" - "net/http" - - "github.com/gorilla/mux" - "github.com/leandrofars/oktopus/internal/bridge" - "github.com/leandrofars/oktopus/internal/entity" - "github.com/leandrofars/oktopus/internal/utils" -) - -func (a *Api) getEnterpriseResource( - resource string, - action string, - device *entity.Device, - sn string, - w http.ResponseWriter, - body []byte, - protocol, datamodel string, -) error { - model, err := cwmpGetDeviceModel(device, w) - if err != nil { - return err - } - - err = bridge.NatsEnterpriseInteraction("enterprise.v1."+protocol+"."+datamodel+"."+model+"."+sn+"."+resource+"."+action, body, w, a.nc) - return err -} - -func (a *Api) getMapsResource( - action string, - w http.ResponseWriter, - body []byte, -) error { - - err := bridge.NatsEnterpriseInteraction("geolocation.v1."+action, body, w, a.nc) - return err -} - -func (a *Api) devicesLocation(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet { - a.getMapsResource("get", w, []byte{}) - } -} - -func (a *Api) deviceSiteSurvey(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - sn := vars["sn"] - - device, err := getDeviceInfo(w, sn, a.nc) - if err != nil { - return - } - - if r.Method == http.MethodGet { - - if device.Cwmp == entity.Online { - a.getEnterpriseResource("sitesurvey", "get", device, sn, w, []byte{}, "cwmp", "098") - return - } - - if device.Mqtt == entity.Online || device.Stomp == entity.Online || device.Websockets == entity.Online { - w.WriteHeader(http.StatusNotImplemented) - w.Write(utils.Marshall("This feature is only working with CWMP devices")) - return - } - - w.WriteHeader(http.StatusBadRequest) - w.Write(utils.Marshall("Device is Offline")) - } -} - -func (a *Api) deviceConnectedDevices(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - sn := vars["sn"] - - device, err := getDeviceInfo(w, sn, a.nc) - if err != nil { - return - } - - if r.Method == http.MethodGet { - - if device.Cwmp == entity.Online { - a.getEnterpriseResource("connecteddevices", "get", device, sn, w, []byte{}, "cwmp", "098") - return - } - - if device.Mqtt == entity.Online || device.Stomp == entity.Online || device.Websockets == entity.Online { - w.WriteHeader(http.StatusNotImplemented) - w.Write(utils.Marshall("This feature is only working with CWMP devices")) - return - } - - w.WriteHeader(http.StatusBadRequest) - w.Write(utils.Marshall("Device is Offline")) - } -} - -func (a *Api) deviceTraceRoute(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - sn := vars["sn"] - - device, err := getDeviceInfo(w, sn, a.nc) - if err != nil { - return - } - - if r.Method == http.MethodGet { - if device.Cwmp == entity.Online { - w.WriteHeader(http.StatusNotImplemented) - w.Write(utils.Marshall("Get traceroute configuration is not implemented yet")) - } - } - - if r.Method == http.MethodPut { - if device.Cwmp == entity.Online { - a.getEnterpriseResource("traceroute", "set", device, sn, w, []byte{}, "cwmp", "098") - return - } - } - - if device.Mqtt == entity.Online || device.Stomp == entity.Online || device.Websockets == entity.Online { - w.WriteHeader(http.StatusNotImplemented) - w.Write(utils.Marshall("This feature is only working with CWMP devices")) - return - } - - w.WriteHeader(http.StatusBadRequest) - w.Write(utils.Marshall("Device is Offline")) -} - -func (a *Api) deviceSpeedTest(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - sn := vars["sn"] - - device, err := getDeviceInfo(w, sn, a.nc) - if err != nil { - return - } - - payload, err := io.ReadAll(r.Body) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write(utils.Marshall("Error reading request body")) - return - - } - - if device.Cwmp == entity.Online { - a.getEnterpriseResource("speedTest", "set", device, sn, w, payload, "cwmp", "098") - return - } - - if device.Mqtt == entity.Online || device.Stomp == entity.Online || device.Websockets == entity.Online { - w.WriteHeader(http.StatusNotImplemented) - w.Write(utils.Marshall("This feature is only working with CWMP devices")) - return - } - - w.WriteHeader(http.StatusBadRequest) - w.Write(utils.Marshall("Device is Offline")) -} - -func (a *Api) devicePing(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - sn := vars["sn"] - - device, err := getDeviceInfo(w, sn, a.nc) - if err != nil { - return - } - - if device.Cwmp != entity.Online { - w.WriteHeader(http.StatusBadRequest) - w.Write(utils.Marshall("Device is Offline")) - } - - if r.Method == http.MethodGet { - if device.Cwmp == entity.Online { - a.getEnterpriseResource("ping", "get", device, sn, w, []byte{}, "cwmp", "098") - return - } - } else { - payload, err := io.ReadAll(r.Body) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write(utils.Marshall("Error reading request body")) - return - - } - - if device.Cwmp == entity.Online { - a.getEnterpriseResource("ping", "set", device, sn, w, payload, "cwmp", "098") - return - } - } - - if device.Mqtt == entity.Online || device.Stomp == entity.Online || device.Websockets == entity.Online { - w.WriteHeader(http.StatusNotImplemented) - w.Write(utils.Marshall("This feature is only working with CWMP devices")) - return - } -} diff --git a/backend/services/controller/internal/api/user.go b/backend/services/controller/internal/api/user.go index b67d4b5..3e31726 100644 --- a/backend/services/controller/internal/api/user.go +++ b/backend/services/controller/internal/api/user.go @@ -176,7 +176,7 @@ func (a *Api) registerAdminUser(w http.ResponseWriter, r *http.Request) { utils.MarshallEncoder(err, w) } - if !adminUserExists(users, a.enterpise.SupportEmail) { + if !adminUserExists(users) { var user db.User err = json.NewDecoder(r.Body).Decode(&user) if err != nil { @@ -235,14 +235,14 @@ func (a *Api) registerAdminUser(w http.ResponseWriter, r *http.Request) { } } -func adminUserExists(users []map[string]interface{}, supportEmail string) bool { +func adminUserExists(users []map[string]interface{}) bool { if len(users) == 0 { return false } for _, x := range users { - if db.UserLevels(x["level"].(int32)) == db.AdminUser && x["email"].(string) != supportEmail { + if db.UserLevels(x["level"].(int32)) == db.AdminUser { return true } } @@ -257,7 +257,7 @@ func (a *Api) adminUserExists(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) return } - adminExits := adminUserExists(users, a.enterpise.SupportEmail) + adminExits := adminUserExists(users) json.NewEncoder(w).Encode(adminExits) return } diff --git a/backend/services/controller/internal/api/utils.go b/backend/services/controller/internal/api/utils.go index 7a81f88..7df48aa 100644 --- a/backend/services/controller/internal/api/utils.go +++ b/backend/services/controller/internal/api/utils.go @@ -10,7 +10,6 @@ import ( local "github.com/leandrofars/oktopus/internal/nats" "github.com/leandrofars/oktopus/internal/utils" "github.com/nats-io/nats.go" - "go.mongodb.org/mongo-driver/bson/primitive" ) var errInvalidMtp = errors.New("Invalid MTP, valid options are: mqtt, ws, stomp") @@ -105,8 +104,8 @@ func getDeviceCount(w http.ResponseWriter, nc *nats.Conn) (int64, error) { return msg.Msg, err } -func getDevices(w http.ResponseWriter, filter primitive.A, nc *nats.Conn) (*[]entity.Device, error) { - msg, err := bridge.NatsReq[[]entity.Device]( +func getDevices(w http.ResponseWriter, filter map[string]interface{}, nc *nats.Conn) (*entity.DevicesList, error) { + msg, err := bridge.NatsReq[entity.DevicesList]( local.NATS_ADAPTER_SUBJECT+"devices.retrieve", utils.Marshall(filter), w, diff --git a/backend/services/controller/internal/api/wifi.go b/backend/services/controller/internal/api/wifi.go index e6b2213..e1de563 100644 --- a/backend/services/controller/internal/api/wifi.go +++ b/backend/services/controller/internal/api/wifi.go @@ -1,7 +1,6 @@ package api import ( - "io" "log" "net/http" "strings" @@ -213,11 +212,6 @@ func (a *Api) deviceWifi(w http.ResponseWriter, r *http.Request) { if device.Cwmp == entity.Online { - if a.enterpise.Enable { - a.getEnterpriseResource("wifi", "get", device, sn, w, []byte{}, "cwmp", "098") - return - } - var ( NUMBER_OF_WIFI_PARAMS_TO_GET = 5 ) @@ -347,17 +341,6 @@ func (a *Api) deviceWifi(w http.ResponseWriter, r *http.Request) { if device.Cwmp == entity.Online { - if a.enterpise.Enable { - payload, err := io.ReadAll(r.Body) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write(utils.Marshall(err.Error())) - return - } - a.getEnterpriseResource("wifi", "set", device, sn, w, payload, "cwmp", "098") - return - } - var body []WiFi err := utils.MarshallDecoder(&body, r.Body) diff --git a/backend/services/controller/internal/bridge/bridge.go b/backend/services/controller/internal/bridge/bridge.go index 16f1b43..905cb80 100644 --- a/backend/services/controller/internal/bridge/bridge.go +++ b/backend/services/controller/internal/bridge/bridge.go @@ -153,7 +153,6 @@ func NatsReq[T entity.DataType]( err = json.Unmarshal(msg.Data, &answer) if err != nil { - var errMsg *entity.MsgAnswer[*string] err = json.Unmarshal(msg.Data, &errMsg) diff --git a/backend/services/controller/internal/config/config.go b/backend/services/controller/internal/config/config.go index e90dc5e..a67c563 100644 --- a/backend/services/controller/internal/config/config.go +++ b/backend/services/controller/internal/config/config.go @@ -30,17 +30,10 @@ type RestApi struct { Ctx context.Context } -type Enterprise struct { - Enable bool - SupportPassword string - SupportEmail string -} - type Config struct { - RestApi RestApi - Nats Nats - Mongo Mongo - Enterprise Enterprise + RestApi RestApi + Nats Nats + Mongo Mongo } type Tls struct { @@ -62,9 +55,6 @@ func NewConfig() *Config { serverCA := flag.String("server_ca", lookupEnvOrString("SERVER_CA", "rootCA.pem"), "server CA file to TLS connection") flApiPort := flag.String("api_port", lookupEnvOrString("REST_API_PORT", "8000"), "Rest api port") mongoUri := flag.String("mongo_uri", lookupEnvOrString("MONGO_URI", "mongodb://localhost:27017"), "uri for mongodb server") - enterpise := flag.Bool("enterprise", lookupEnvOrBool("ENTERPRISE", false), "enterprise version enable") - enterprise_support_password := flag.String("enterprise_support_password", lookupEnvOrString("ENTERPRISE_SUPPORT_PASSWORD", ""), "enterprise support password") - enterpise_support_email := flag.String("enterprise_support_email", lookupEnvOrString("ENTERPRISE_SUPPORT_EMAIL", ""), "enterprise support email") flHelp := flag.Bool("help", false, "Help") /* @@ -103,11 +93,6 @@ func NewConfig() *Config { Uri: *mongoUri, Ctx: ctx, }, - Enterprise: Enterprise{ - Enable: *enterpise, - SupportPassword: *enterprise_support_password, - SupportEmail: *enterpise_support_email, - }, } } diff --git a/backend/services/controller/internal/entity/device.go b/backend/services/controller/internal/entity/device.go index 12948a7..840d045 100644 --- a/backend/services/controller/internal/entity/device.go +++ b/backend/services/controller/internal/entity/device.go @@ -29,3 +29,15 @@ type StatusCount struct { Status int `bson:"_id" json:"status"` Count int `bson:"count" json:"count"` } + +type DevicesList struct { + Devices []Device `json:"devices" bson:"documents"` + Total int64 `json:"total"` +} + +type FilterOptions struct { + Models []string `json:"models"` + ProductClasses []string `json:"productClasses"` + Vendors []string `json:"vendors"` + Versions []string `json:"versions"` +} diff --git a/backend/services/controller/internal/entity/msg.go b/backend/services/controller/internal/entity/msg.go index 768e4bb..760cfd9 100644 --- a/backend/services/controller/internal/entity/msg.go +++ b/backend/services/controller/internal/entity/msg.go @@ -3,7 +3,7 @@ package entity import "time" type DataType interface { - []map[string]interface{} | *string | Device | int64 | []Device | []VendorsCount | []ProductClassCount | []StatusCount | time.Duration | []byte + []map[string]interface{} | *string | Device | int64 | []Device | []VendorsCount | []ProductClassCount | []StatusCount | time.Duration | []byte | []string | FilterOptions | DevicesList } type MsgAnswer[T DataType] struct { diff --git a/backend/services/mtp/adapter/internal/db/device.go b/backend/services/mtp/adapter/internal/db/device.go index c78ac6a..50ef7e8 100644 --- a/backend/services/mtp/adapter/internal/db/device.go +++ b/backend/services/mtp/adapter/internal/db/device.go @@ -4,6 +4,7 @@ import ( "log" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) @@ -41,6 +42,18 @@ type Device struct { Cwmp Status } +type DevicesList struct { + Devices []Device `json:"devices" bson:"documents"` + Total int64 `json:"total" bson:"totalCount"` +} + +type FilterOptions struct { + Models []string `json:"models"` + ProductClasses []string `json:"productClasses"` + Vendors []string `json:"vendors"` + Versions []string `json:"versions"` +} + func (d *Database) CreateDevice(device Device) error { var result bson.M var deviceExistent Device @@ -100,32 +113,86 @@ func (d *Database) CreateDevice(device Device) error { } return err } -func (d *Database) RetrieveDevices(filter bson.A) ([]Device, error) { +func (d *Database) RetrieveDevices(filter bson.A) (*DevicesList, error) { - var results []Device + var results []DevicesList cursor, err := d.devices.Aggregate(d.ctx, filter) if err != nil { - return results, err + return nil, err } - if cursor.Err() != nil { - return results, cursor.Err() + return nil, cursor.Err() + } + defer cursor.Close(d.ctx) + if err := cursor.All(d.ctx, &results); err != nil { + log.Println(err) + return nil, err } - for cursor.Next(d.ctx) { - var device Device + //log.Printf("results: %++v", results) - err := cursor.Decode(&device) - if err != nil { - log.Println("Error to decode device info fields") - continue - } + return &results[0], err +} - results = append(results, device) +func (d *Database) RetrieveDeviceFilterOptions() (FilterOptions, error) { + filter := bson.A{ + bson.D{ + {"$group", + bson.D{ + {"_id", primitive.Null{}}, + {"vendors", bson.D{{"$addToSet", "$vendor"}}}, + {"versions", bson.D{{"$addToSet", "$version"}}}, + {"productClasses", bson.D{{"$addToSet", "$productclass"}}}, + {"models", bson.D{{"$addToSet", "$model"}}}, + }, + }, + }, + bson.D{ + {"$project", + bson.D{ + {"_id", 0}, + {"vendors", 1}, + {"versions", 1}, + {"productClasses", 1}, + {"models", 1}, + }, + }, + }, } - return results, err + var results []FilterOptions + cursor, err := d.devices.Aggregate(d.ctx, filter) + if err != nil { + log.Println(err) + return FilterOptions{}, err + } + defer cursor.Close(d.ctx) + + if err := cursor.All(d.ctx, &results); err != nil { + log.Println(err) + return FilterOptions{}, err + } + + if len(results) > 0 { + return results[0], nil + } else { + return FilterOptions{ + Models: []string{}, + ProductClasses: []string{}, + Vendors: []string{}, + Versions: []string{}, + }, nil + } +} + +func (d *Database) DeleteDevices(filter bson.D) (int64, error) { + + result, err := d.devices.DeleteMany(d.ctx, filter) + if err != nil { + log.Println(err) + } + return result.DeletedCount, err } func (d *Database) RetrieveDevice(sn string) (Device, error) { diff --git a/backend/services/mtp/adapter/internal/reqs/reqs.go b/backend/services/mtp/adapter/internal/reqs/reqs.go index 3da0fa9..6a0260f 100644 --- a/backend/services/mtp/adapter/internal/reqs/reqs.go +++ b/backend/services/mtp/adapter/internal/reqs/reqs.go @@ -52,18 +52,135 @@ func StartRequestsListener(ctx context.Context, nc *nats.Conn, db db.Database) { nc.QueueSubscribe(local.ADAPTER_SUBJECT+"devices.retrieve", local.ADAPTER_QUEUE, func(msg *nats.Msg) { - var filter bson.A + var criteria map[string]interface{} - err := json.Unmarshal(msg.Data, &filter) + err := json.Unmarshal(msg.Data, &criteria) if err != nil { respondMsg(msg.Respond, 500, err.Error()) } + //log.Println(criteria) + propertiesFilter := bson.D{{}} + + vendorFilter := criteria["vendor"] + if vendorFilter != nil { + log.Println("Vendor filter", vendorFilter) + propertiesFilter = append(propertiesFilter, bson.E{Key: "vendor", Value: vendorFilter}) + } + + versionFilter := criteria["version"] + if versionFilter != nil { + log.Println("Version filter", versionFilter) + propertiesFilter = append(propertiesFilter, bson.E{Key: "version", Value: versionFilter}) + } + + typeFilter := criteria["productClass"] + if typeFilter != nil { + log.Println("Type filter", typeFilter) + propertiesFilter = append(propertiesFilter, bson.E{Key: "productclass", Value: typeFilter}) + } + + aliasFilter := criteria["alias"] + if aliasFilter != nil { + log.Println("Type filter", aliasFilter) + propertiesFilter = append(propertiesFilter, bson.E{Key: "alias", Value: aliasFilter}) + } + + modelFilter := criteria["model"] + if modelFilter != nil { + log.Println("Model filter", modelFilter) + propertiesFilter = append(propertiesFilter, bson.E{Key: "model", Value: modelFilter}) + } + + statusFilter := criteria["status"] + if statusFilter != nil { + log.Println("Status filter", statusFilter) + propertiesFilter = append(propertiesFilter, bson.E{Key: "status", Value: statusFilter}) + } + + filter := bson.A{ + bson.D{ + {"$match", + propertiesFilter, + }, + }, + bson.D{ + {"$facet", + bson.D{ + {"totalCount", + bson.A{ + bson.D{{"$count", "count"}}, + }, + }, + {"documents", + bson.A{ + bson.D{{"$sort", bson.D{{"status", criteria["status_order"]}}}}, + bson.D{{"$skip", criteria["skip"]}}, + bson.D{{"$limit", criteria["limit"]}}, + }, + }, + }, + }, + }, + bson.D{ + {"$project", + bson.D{ + {"totalCount", + bson.D{ + {"$arrayElemAt", + bson.A{ + "$totalCount.count", + 0, + }, + }, + }, + }, + {"documents", 1}, + }, + }, + }, + } + devicesList, err := db.RetrieveDevices(filter) if err != nil { respondMsg(msg.Respond, 500, err.Error()) } - respondMsg(msg.Respond, 200, devicesList) + respondMsg(msg.Respond, 200, &devicesList) + }) + + nc.QueueSubscribe(local.ADAPTER_SUBJECT+"devices.delete", local.ADAPTER_QUEUE, func(msg *nats.Msg) { + + var serialNumbersList []string + + err := json.Unmarshal(msg.Data, &serialNumbersList) + if err != nil { + respondMsg(msg.Respond, 500, err.Error()) + } + + var criteria bson.A + + for _, sn := range serialNumbersList { + criteria = append(criteria, bson.D{{"sn", sn}}) + } + + // Create the filter with the $or operator + filter := bson.D{ + {"$or", criteria}, + } + + deletedCount, err := db.DeleteDevices(filter) + if err != nil { + respondMsg(msg.Respond, 500, err.Error()) + } + respondMsg(msg.Respond, 200, deletedCount) + }) + + nc.QueueSubscribe(local.ADAPTER_SUBJECT+"devices.filterOptions", local.ADAPTER_QUEUE, func(msg *nats.Msg) { + result, err := db.RetrieveDeviceFilterOptions() + if err != nil { + respondMsg(msg.Respond, 500, err.Error()) + } + respondMsg(msg.Respond, 200, result) }) nc.QueueSubscribe(local.ADAPTER_SUBJECT+"devices.class", local.ADAPTER_QUEUE, func(msg *nats.Msg) { diff --git a/frontend/src/pages/devices.js b/frontend/src/pages/devices.js index 45f2732..8b02675 100644 --- a/frontend/src/pages/devices.js +++ b/frontend/src/pages/devices.js @@ -1,38 +1,211 @@ import React, { useState, useEffect } from 'react'; import Head from 'next/head'; -import { - Box, - Container, + +import { + Box, + Container, Unstable_Grid2 as Grid, - Card, + Card, OutlinedInput, InputAdornment, SvgIcon, Stack, Pagination, - CircularProgress + CircularProgress, + Button, + Menu, + MenuItem, + Checkbox, + ListItemText, + SpeedDial, + SpeedDialAction, + SpeedDialIcon, + CardHeader, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + FormControl, + Tooltip, + Chip, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + InputLabel, + Input, + TextField, + Select, + DialogContentText, + TableContainer, + TablePagination } from '@mui/material'; + +import ViewColumnsIcon from '@heroicons/react/24/outline/ViewColumnsIcon'; +import ArrowTopRightOnSquareIcon from '@heroicons/react/24/solid/ArrowTopRightOnSquareIcon'; +import FunnelIcon from "@heroicons/react/24/outline/FunnelIcon"; +import PencilIcon from '@heroicons/react/24/outline/PencilIcon'; +import TagIcon from '@heroicons/react/24/outline/TagIcon'; +import ShareIcon from '@heroicons/react/24/outline/ShareIcon'; +import CommandLineIcon from '@heroicons/react/24/outline/CommandLineIcon'; +import ChevronUpIcon from '@heroicons/react/24/outline/ChevronUpIcon'; +import ChevronDownIcon from '@heroicons/react/24/outline/ChevronDownIcon'; import MagnifyingGlassIcon from '@heroicons/react/24/solid/MagnifyingGlassIcon'; +import TrashIcon from '@heroicons/react/24/outline/TrashIcon'; + +import { Scrollbar } from 'src/components/scrollbar'; import { Layout as DashboardLayout } from 'src/layouts/dashboard/layout'; -import { OverviewLatestOrders } from 'src/sections/overview/overview-latest-orders'; import { useAuth } from 'src/hooks/use-auth'; import { useRouter } from 'next/router'; +import { useTheme } from '@emotion/react'; const Page = () => { + + const theme = useTheme(); const router = useRouter() const auth = useAuth(); + const [devices, setDevices] = useState([]); + const [total, setTotal] = useState(null); const [deviceFound, setDeviceFound] = useState(true) const [pages, setPages] = useState(0); - const [page, setPage] = useState(null); + const [page, setPage] = useState(0); const [Loading, setLoading] = useState(true); + const [anchorEl, setAnchorEl] = useState(null); + const [statusOrder, setStatusOrder] = useState("desc"); + const [filterOptions, setFilterOptions] = useState(null); + const defaultFiltersList = { + alias: "", + model: "", + vendor: "", + version: "", + status: "", + type: "", + } + const [filtersList, setFiltersList] = useState(defaultFiltersList); + const [newFiltersList, setNewFiltersList] = useState(defaultFiltersList); + + const cleanFilters = () => { + setFiltersList(defaultFiltersList) + } + + const rowsPerPageOptions = [20,30,40]; + const [rowsPerPage, setRowsPerPage] = useState(20); + + const [showSetDeviceAlias, setShowSetDeviceAlias] = useState(false); + const [deviceAlias, setDeviceAlias] = useState(null); + const [deviceToBeChanged, setDeviceToBeChanged] = useState(null); + const [showFilter, setShowFilter] = useState(false); + const [selected, setSelected] = useState([]); + const [selectAll, setSelectAll] = useState(false); + + + const [columns, setColumns] = useState({ + version: true, + sn: true, + alias: true, + model: true, + vendor: true, + status: true, + actions: true, + label: false + }); + + const [showSpeedDial, setShowSpeedDial] = useState(false); + + const getColumns = () => { + localStorage.getItem("columns") ? setColumns(JSON.parse(localStorage.getItem("columns"))) : setColumns({ + version: true, + sn: true, + alias: false, + model: true, + vendor: true, + status: true, + actions: true, + label: false + }) + } + + const changeColumn = (column) => { + console.log("columns old:", columns) + setColumns({ ...columns, [column]: !columns[column] }) + localStorage.setItem("columns", JSON.stringify({ ...columns, [column]: !columns[column] })) + } + + function objsEqual(obj1,obj2){ + return JSON.stringify(obj1)===JSON.stringify(obj2); + } useEffect(() => { + if (selected.length > 0) { + let speedDial = false + selected.map((s) => { + if (s == true) { + speedDial = true + } else if (s == false) { + setSelectAll(false) + } + }) + setShowSpeedDial(speedDial) + return + } + }, [selected]) + + const statusMap = { + 1: 'warning', + 2: 'success', + 0: 'error' + }; + + const status = (s) => { + if (s == 0) { + return "Offline" + } else if (s == 1) { + return "Associating" + } else if (s == 2) { + return "Online" + } else { + return "Unknown" + } + } + + const getDeviceProtocol = (order) => { + if (order.Cwmp != null) { + return "cwmp" + } else { + return "usp" + } + } + + const open = Boolean(anchorEl); + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + + const actions = [ + /*{ icon: , name: 'Alias', onClickEvent: () => { + console.log("edit device alias") + // setDeviceToBeChanged(selected.indexOf(true)) + // setDeviceAlias(orders[selected.indexOf(true)].Alias) + // setShowSetDeviceAlias(true) + } },*/ + //{ icon: , name: 'Label' }, + //{ icon: , name: 'Share' }, + { icon: , name: 'Remove' }, + { icon: , name: 'Action' }, + ]; + + useEffect(() => { + getColumns() setLoading(true) if (auth.user.token) { console.log("auth.user.token =", auth.user.token) - }else{ + } else { auth.user.token = localStorage.getItem("token") } @@ -46,32 +219,102 @@ const Page = () => { redirect: 'follow' } - fetch(`${process.env.NEXT_PUBLIC_REST_ENDPOINT || ""}/api/device`, requestOptions) + let status; + + fetch(`${process.env.NEXT_PUBLIC_REST_ENDPOINT || ""}/api/device?statusOrder=${statusOrder}&page_number=${page}&page_size=${rowsPerPage}&vendor=${filtersList["vendor"]}&version=${filtersList["version"]}&alias=${filtersList["alias"]}&type=${filtersList["type"]}&status=${filtersList["status"]}&model=${filtersList["model"]}`, requestOptions) .then(response => { - if (response.status === 401) + if (response.status === 401){ router.push("/auth/login") + } + status = response.status return response.json() }) .then(json => { + if (status == 404) { + console.log("device not found") + setLoading(false) + setDeviceFound(false) + return + } + console.log("Status:", status) setPages(json.pages + 1) - setPage(json.page +1) + setPage(json.page + 1) + setTotal(json.total) setDevices(json.devices) + setSelected(new Array(json.devices.length).fill(false)) setLoading(false) return setDeviceFound(true) }) .catch(error => { return console.error('Error:', error) }); + + fetch(`${process.env.NEXT_PUBLIC_REST_ENDPOINT || ""}/api/device/filterOptions`, requestOptions) + .then(response => { + if (response.status === 401) + router.push("/auth/login") + return response.json() + }) + .then(json => { + return setFilterOptions(json) + }) + .catch(error => { + return console.error('Error:', error) + }); + }, [auth.user]); - const handleChangePage = (event, value) => { - console.log("new page: ", value) - setPage(value) - fetchDevicePerPage(value) + const setNewDeviceAlias = async (alias, sn) => { + var myHeaders = new Headers(); + myHeaders.append("Content-Type", "application/json"); + myHeaders.append("Authorization", localStorage.getItem("token")); + + var requestOptions = { + method: 'PUT', + headers: myHeaders, + body: alias, + redirect: 'follow' + }; + + let result = await fetch(`${process.env.NEXT_PUBLIC_REST_ENDPOINT || ""}/api/device/alias?id=${sn}`, requestOptions) + console.log("result:", result) + if (result.status === 401) { + router.push("/auth/login") + } else if (result.status != 200) { + console.log("Status:", result.status) + let content = await result.json() + console.log("Message:", content) + setShowSetDeviceAlias(false) + setDeviceAlias(null) + setDeviceToBeChanged(null) + } else { + let content = await result.json() + console.log("set alias result:", content) + setShowSetDeviceAlias(false) + setDeviceAlias(null) + orders[deviceToBeChanged].Alias = alias + setDeviceToBeChanged(null) + } + // .then(response => { + // if (response.status === 401) { + // router.push("/auth/login") + // } + // return response.json() + // }) + // .then(result => { + // console.log("alias result:", result) + // setShowSetDeviceAlias(false) + // setDeviceAlias(null) + // }) + // .catch(error => { + // console.log('error:', error) + // setShowSetDeviceAlias(false) + // setDeviceAlias(null) + // }) } - const fetchDevicePerPage = async (p) => { - setLoading(true) + const fetchDevicePerPage = async (p, s, localFilterList, page_size) => { + //setLoading(true) var myHeaders = new Headers(); myHeaders.append("Content-Type", "application/json"); @@ -83,23 +326,34 @@ const Page = () => { redirect: 'follow' } + if (localFilterList == undefined) { + localFilterList = filtersList + } + + if (page_size == undefined) { + page_size = rowsPerPage + } + p = p - 1 p = p.toString() - fetch(`${process.env.NEXT_PUBLIC_REST_ENDPOINT || ""}/api/device?page_number=+${p}`, requestOptions) - .then(response => { - if (response.status === 401) - router.push("/auth/login") - return response.json() - }) - .then(json => { - setDevices(json.devices) - setLoading(false) - return - }) - .catch(error => { - return console.error('Error:', error) - }); + fetch(`${process.env.NEXT_PUBLIC_REST_ENDPOINT || ""}/api/device?page_number=${p}&page_size=${page_size}&statusOrder=${s}&vendor=${localFilterList["vendor"]}&version=${localFilterList["version"]}&alias=${localFilterList["alias"]}&type=${localFilterList["type"]}&status=${localFilterList["status"]}&model=${localFilterList["model"]}`, requestOptions) + .then(response => { + if (response.status === 401) + router.push("/auth/login") + return response.json() + }) + .then(json => { + setTotal(json.total) + setDevices(json.devices) + setPages(json.pages + 1) + setPage(json.page + 1) + //setLoading(false) + return + }) + .catch(error => { + return console.error('Error:', error) + }); } const fetchDevicePerId = async (id) => { @@ -115,41 +369,43 @@ const Page = () => { redirect: 'follow' } - if (id == ""){ - return fetch(`${process.env.NEXT_PUBLIC_REST_ENDPOINT || ""}/api/device`, requestOptions) - .then(response => { - if (response.status === 401) - router.push("/auth/login") - return response.json() - }) - .then(json => { - setPages(json.pages + 1) - setPage(json.page) - setDevices(json.devices) - setLoading(false) - return setDeviceFound(true) - }) - .catch(error => { - return console.error('Error:', error) - }); + if (id == "") { + return fetch(`${process.env.NEXT_PUBLIC_REST_ENDPOINT || ""}/api/device?vendor=${filtersList["vendor"]}&version=${filtersList["version"]}&alias=${filtersList["alias"]}&type=${filtersList["type"]}&status=${filtersList["status"]}&model=${filtersList["model"]}`, requestOptions) + .then(response => { + if (response.status === 401) + router.push("/auth/login") + return response.json() + }) + .then(json => { + setPages(json.pages + 1) + setPage(json.page + 1) + setTotal(json.total) + setDevices(json.devices) + setLoading(false) + return setDeviceFound(true) + }) + .catch(error => { + return console.error('Error:', error) + }); } let response = await fetch(`${process.env.NEXT_PUBLIC_REST_ENDPOINT || ""}/api/device?id=${id}`, requestOptions) if (response.status === 401) router.push("/auth/login") let json = await response.json() - if (json.SN != undefined){ + if (json.SN != undefined) { setDevices([json]) + setTotal(1) setDeviceFound(true) setLoading(false) setPages(1) setPage(1) - }else{ + } else { setDeviceFound(false) - setDevices([]) - setPages(1) - setPage(1) - setLoading(false) + setDevices(null) + setTotal(null) + setPages(null) + setPage(null) } } @@ -161,86 +417,555 @@ const Page = () => { Oktopus | TR-369 - + - - - - - { - if (e.key === 'Enter') { - console.log("Fetch devices per id: ", e.target.value) - fetchDevicePerId(e.target.value) - } - }} - startAdornment={( - - - + + + + { + if (e.key === 'Enter') { + console.log("Fetch devices per id: ", e.target.value) + fetchDevicePerId(e.target.value) + } + }} + startAdornment={( + + + + + + )} + sx={{ maxWidth: 500 }} + /> + + + + {Object.keys(filtersList).map((key) => ( + (filtersList[key] && + { + setFiltersList({ ...filtersList, [key]: "" }) + setNewFiltersList({ ...newFiltersList, [key]: ""}) + fetchDevicePerPage(1, statusOrder, { ...filtersList, [key]: "" }) + }}/>) + ))} + + +
+ + + changeColumn("sn")}>changeColumn("sn")}*/ /> + changeColumn("alias")}>changeColumn("alias")}*/ /> + changeColumn("model")}> changeColumn("model")}*/ /> + changeColumn("vendor")}> changeColumn("vendor")}*/ /> + changeColumn("version")}> changeColumn("version")}*/ /> + changeColumn("status")}> changeColumn("status")}*/ /> + changeColumn("actions")}> changeColumn("actions")}*/ /> + {/* changeColumn("label")}> */} + +
+
+ + + + + + + + + {/* + { + setSelected(new Array(devices.length).fill(e.target.checked)) + //console.log("selected:", selected) + setSelectAll(e.target.checked) + }} + checked={selectAll} + /> + */} + {columns["sn"] && + Serial Number + } + {columns["alias"] && + Alias + } + { + columns["label"] && + Labels + + } + {columns["model"] && + Model + } + {columns["vendor"] && + Vendor + } + {columns["version"] && + Version + } + {columns["status"] && + + {/*//TODO: create function to fetch devices by status order*/} + + { + if (statusOrder == "asc") { + setStatusOrder("desc") + fetchDevicePerPage(page, "desc") + } else { + setStatusOrder("asc") + fetchDevicePerPage(page, "asc") + } + }}>Status ↑↓ + + {/* + + + + + + */} + } + {columns["actions"] && + Actions + } + + + {!Loading ? + {devices && devices.map((order, index) => { + return ( + + {/* + + { + let newData = [...selected] + newData.splice(index, 1, e.target.checked); + console.log("newData:", newData) + setSelected(newData) + }} + checked={selected[index]} + /> + + */} + {columns["sn"] && + {order.SN} + } + {columns["alias"] && + {order.Alias} + } + { + columns["label"] && + + + + + } + {columns["model"] && + {order.Model || order.ProductClass} + } + {columns["vendor"] && + {order.Vendor} + } + {columns["version"] && + {order.Version} + } + {columns["status"] && + {/* + {status(order.Status)} + */} + + } + {columns["actions"] && + {order.Status == 2 && + + + } + {/* + + + + + */} + } + + ); + })} + : + + + + { + deviceFound ? : "No device found" + } + + + + } +
+
+ {(pages.lenght > 0) && total && { + setPage(newPage+1) + fetchDevicePerPage(newPage+1, statusOrder, filtersList, rowsPerPage) + }} + onRowsPerPageChange={(e)=>{ + setRowsPerPage(e.target.value) + setPage(1) + fetchDevicePerPage(1, statusOrder, filtersList, e.target.value) + }} + />} +
+
+
+ {showSetDeviceAlias && + + + Device Alias + { setDeviceAlias(e.target.value) }} + onKeyUp={e => { + if (e.key === 'Enter') { + setNewDeviceAlias(deviceAlias, orders[deviceToBeChanged].SN) + } + }}> + + + + + + + } +
+ {/* / : + // + //

Device Not Found

+ //
+ // } + // */} + {/* {pages ? : null} */} + {/* //TODO: show loading */} + {/* */} +
+ + + + + + + Filter + + {filterOptions && + + + setNewFiltersList({ ...newFiltersList, "alias": e.target.value })} + /> + + Type + + + {/* */} + + + + Vendor + + + + Version + + + + + + Status + + + {/* + Label + + */} + + Model + + + + {/* + + Type + + + */} + + } + + + + + ) } Page.getLayout = (page) => ( - - {page} - + + {page} + ); export default Page;