commit
28738ed0f3
1
backend/services/controller/.env
Normal file
1
backend/services/controller/.env
Normal file
|
|
@ -0,0 +1 @@
|
|||
SECRET_API_KEY=""
|
||||
2
backend/services/controller/.gitignore
vendored
Normal file
2
backend/services/controller/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/.env.local
|
||||
run.prod.sh
|
||||
|
|
@ -5,6 +5,7 @@ package main
|
|||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/leandrofars/oktopus/internal/api"
|
||||
"github.com/leandrofars/oktopus/internal/db"
|
||||
usp_msg "github.com/leandrofars/oktopus/internal/usp_message"
|
||||
|
|
@ -23,6 +24,20 @@ const VERSION = "0.0.1"
|
|||
func main() {
|
||||
done := make(chan os.Signal, 1)
|
||||
|
||||
err := godotenv.Load()
|
||||
|
||||
localEnv := ".env.local"
|
||||
if _, err := os.Stat(localEnv); err == nil {
|
||||
_ = godotenv.Overload(localEnv)
|
||||
log.Println("Loaded variables from '.env.local'")
|
||||
} else {
|
||||
log.Println("Loaded variables from '.env'")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Println("Error to load environment variables:", err)
|
||||
}
|
||||
|
||||
// Locks app running until it receives a stop command as Ctrl+C.
|
||||
signal.Notify(done, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,19 +3,22 @@ module github.com/leandrofars/oktopus
|
|||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/eclipse/paho.golang v0.10.0
|
||||
go.mongodb.org/mongo-driver v1.11.3
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
|
||||
google.golang.org/protobuf v1.28.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/klauspost/compress v1.13.6 // indirect
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rs/cors v1.9.0 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.1 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.3 // indirect
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
|||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
|
|
@ -29,6 +31,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE=
|
||||
github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
|
|
@ -47,6 +51,7 @@ go.mongodb.org/mongo-driver v1.11.3 h1:Ql6K6qYHEzB6xvu4+AU0BoRoqf9vFPcc4o7MUIdPW
|
|||
go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/leandrofars/oktopus/internal/api/auth"
|
||||
"github.com/leandrofars/oktopus/internal/api/cors"
|
||||
"github.com/leandrofars/oktopus/internal/api/middleware"
|
||||
"github.com/leandrofars/oktopus/internal/db"
|
||||
"github.com/leandrofars/oktopus/internal/mtp"
|
||||
|
|
@ -39,9 +40,9 @@ func StartApi(a Api) {
|
|||
r := mux.NewRouter()
|
||||
authentication := r.PathPrefix("/auth").Subrouter()
|
||||
authentication.HandleFunc("/login", a.generateToken).Methods("PUT")
|
||||
authentication.HandleFunc("/register", a.registerUser).Methods("POST")
|
||||
//authentication.HandleFunc("/register", a.registerUser).Methods("POST")
|
||||
iot := r.PathPrefix("/device").Subrouter()
|
||||
iot.HandleFunc("/", a.retrieveDevices).Methods("GET")
|
||||
iot.HandleFunc("", a.retrieveDevices).Methods("GET")
|
||||
iot.HandleFunc("/{sn}/get", a.deviceGetMsg).Methods("PUT")
|
||||
iot.HandleFunc("/{sn}/add", a.deviceCreateMsg).Methods("PUT")
|
||||
iot.HandleFunc("/{sn}/del", a.deviceDeleteMsg).Methods("PUT")
|
||||
|
|
@ -49,17 +50,21 @@ func StartApi(a Api) {
|
|||
//TODO: Create operation action handler
|
||||
iot.HandleFunc("/device/{sn}/act", a.deviceUpdateMsg).Methods("PUT")
|
||||
|
||||
// Middleware for requests which requires user to be authenticated
|
||||
iot.Use(func(handler http.Handler) http.Handler {
|
||||
return middleware.Middleware(handler)
|
||||
})
|
||||
|
||||
// Verifies CORS configs for requests
|
||||
corsOpts := cors.GetCorsConfig()
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: "0.0.0.0:" + a.Port,
|
||||
// Good practice to set timeouts to avoid Slowloris attacks.
|
||||
WriteTimeout: time.Second * 15,
|
||||
ReadTimeout: time.Second * 15,
|
||||
IdleTimeout: time.Second * 60,
|
||||
Handler: r, // Pass our instance of gorilla/mux in.
|
||||
Handler: corsOpts.Handler(r), // Pass our instance of gorilla/mux in.
|
||||
}
|
||||
|
||||
// Run our server in a goroutine so that it doesn't block.
|
||||
|
|
@ -68,6 +73,7 @@ func StartApi(a Api) {
|
|||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
log.Println("Running Api at port", a.Port)
|
||||
}
|
||||
|
||||
func (a *Api) retrieveDevices(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -92,7 +98,7 @@ func (a *Api) deviceCreateMsg(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
var receiver usp_msg.Add
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(receiver)
|
||||
err := json.NewDecoder(r.Body).Decode(&receiver)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
|
|
|||
|
|
@ -3,10 +3,17 @@ package auth
|
|||
import (
|
||||
"errors"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var jwtKey = []byte("supersecretkey")
|
||||
func getJwtKey() []byte {
|
||||
jwtKey, ok := os.LookupEnv("SECRET_API_KEY")
|
||||
if !ok || jwtKey == "" {
|
||||
return []byte("supersecretkey")
|
||||
}
|
||||
return []byte(jwtKey)
|
||||
}
|
||||
|
||||
type JWTClaim struct {
|
||||
Username string `json:"username"`
|
||||
|
|
@ -15,7 +22,7 @@ type JWTClaim struct {
|
|||
}
|
||||
|
||||
func GenerateJWT(email string, username string) (tokenString string, err error) {
|
||||
expirationTime := time.Now().Add(1 * time.Hour)
|
||||
expirationTime := time.Now().Add(4 * time.Hour)
|
||||
claims := &JWTClaim{
|
||||
Email: email,
|
||||
Username: username,
|
||||
|
|
@ -24,15 +31,16 @@ func GenerateJWT(email string, username string) (tokenString string, err error)
|
|||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err = token.SignedString(jwtKey)
|
||||
tokenString, err = token.SignedString(getJwtKey())
|
||||
return
|
||||
}
|
||||
|
||||
func ValidateToken(signedToken string) (email string, err error) {
|
||||
token, err := jwt.ParseWithClaims(
|
||||
signedToken,
|
||||
&JWTClaim{},
|
||||
func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(jwtKey), nil
|
||||
return getJwtKey(), nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
|
|
|||
27
backend/services/controller/internal/api/cors/cors.go
Normal file
27
backend/services/controller/internal/api/cors/cors.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package cors
|
||||
|
||||
import (
|
||||
"github.com/rs/cors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GetCorsConfig() cors.Cors {
|
||||
return *cors.New(cors.Options{
|
||||
AllowedOrigins: []string{
|
||||
"http://localhost:3000",
|
||||
},
|
||||
AllowedMethods: []string{
|
||||
http.MethodGet,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch,
|
||||
http.MethodDelete,
|
||||
http.MethodOptions,
|
||||
http.MethodHead,
|
||||
},
|
||||
|
||||
AllowedHeaders: []string{
|
||||
"*", //or you can your header key values which you are using in your application
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ func NewUspRecord(p []byte, toId string) usp_record.Record {
|
|||
return usp_record.Record{
|
||||
Version: "0.1",
|
||||
ToId: toId,
|
||||
FromId: "leleco",
|
||||
FromId: "oktopusController",
|
||||
PayloadSecurity: usp_record.Record_PLAINTEXT,
|
||||
RecordType: &usp_record.Record_NoSessionContext{
|
||||
NoSessionContext: &usp_record.NoSessionContextRecord{
|
||||
|
|
|
|||
1
backend/services/controller/run.sh
Normal file
1
backend/services/controller/run.sh
Normal file
|
|
@ -0,0 +1 @@
|
|||
go run cmd/oktopus/main.go -u root -P root -mongo mongodb://172.16.238.3:27017/
|
||||
2
backend/services/mochi/.gitignore
vendored
2
backend/services/mochi/.gitignore
vendored
|
|
@ -1,3 +1,5 @@
|
|||
cmd/mqtt
|
||||
.DS_Store
|
||||
*.db
|
||||
auth.prod.json
|
||||
run.prod.sh
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
This broker is an implementation of mochi. I had to fork it to customize CONNACK packet userProperty.
|
||||
This broker is an implementation of mochi. I've to forked it to customize CONNACK packet userProperty, although mochi lib might have a better approach to do it.
|
||||
|
||||
To run this project you might have Go compiler in your machine, and inside cmd folder there is a run.sh script, which runs the project with the right arguments; also inside the same folder is the auth.json file, that carries configs of RBAC.
|
||||
|
||||

|
||||
|
|
@ -6,9 +6,11 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"github.com/mochi-co/mqtt/v2/packets"
|
||||
"github.com/rs/zerolog"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
|
@ -22,24 +24,59 @@ import (
|
|||
"github.com/mochi-co/mqtt/v2/listeners"
|
||||
)
|
||||
|
||||
var server = mqtt.New(&mqtt.Options{
|
||||
//Capabilities: &mqtt.Capabilities{
|
||||
// ServerKeepAlive: 10000,
|
||||
// ReceiveMaximum: math.MaxUint16,
|
||||
// MaximumMessageExpiryInterval: math.MaxUint32,
|
||||
// MaximumSessionExpiryInterval: math.MaxUint32, // maximum number of seconds to keep disconnected sessions
|
||||
// MaximumClientWritesPending: 65536,
|
||||
// MaximumPacketSize: 0,
|
||||
// MaximumQos: 2,
|
||||
//},
|
||||
})
|
||||
var (
|
||||
// testCertificate = []byte(`-----BEGIN CERTIFICATE-----
|
||||
//MIIB/zCCAWgCCQDm3jV+lSF1AzANBgkqhkiG9w0BAQsFADBEMQswCQYDVQQGEwJB
|
||||
//VTETMBEGA1UECAwKU29tZS1TdGF0ZTERMA8GA1UECgwITW9jaGkgQ28xDTALBgNV
|
||||
//BAsMBE1RVFQwHhcNMjAwMTA0MjAzMzQyWhcNMjEwMTAzMjAzMzQyWjBEMQswCQYD
|
||||
//VQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTERMA8GA1UECgwITW9jaGkgQ28x
|
||||
//DTALBgNVBAsMBE1RVFQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKz2bUz3
|
||||
//AOymssVLuvSOEbQ/sF8C/Ill8nRTd7sX9WBIxHJZf+gVn8lQ4BTQ0NchLDRIlpbi
|
||||
//OuZgktpd6ba8sIfVM4jbVprctky5tGsyHRFwL/GAycCtKwvuXkvcwSwLvB8b29EI
|
||||
//MLQ/3vNnYuC3eZ4qqxlODJgRsfQ7mUNB8zkLAgMBAAEwDQYJKoZIhvcNAQELBQAD
|
||||
//gYEAiMoKnQaD0F/J332arGvcmtbHmF2XZp/rGy3dooPug8+OPUSAJY9vTfxJwOsQ
|
||||
//qN1EcI+kIgrGxzA3VRfVYV8gr7IX+fUYfVCaPGcDCfPvo/Ihu757afJRVvpafWgy
|
||||
//zSpDZYu6C62h3KSzMJxffDjy7/2t8oYbTzkLSamsHJJjLZw=
|
||||
//-----END CERTIFICATE-----`)
|
||||
//
|
||||
// testPrivateKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
//MIICXAIBAAKBgQCs9m1M9wDsprLFS7r0jhG0P7BfAvyJZfJ0U3e7F/VgSMRyWX/o
|
||||
//FZ/JUOAU0NDXISw0SJaW4jrmYJLaXem2vLCH1TOI21aa3LZMubRrMh0RcC/xgMnA
|
||||
//rSsL7l5L3MEsC7wfG9vRCDC0P97zZ2Lgt3meKqsZTgyYEbH0O5lDQfM5CwIDAQAB
|
||||
//AoGBAKlmVVirFqmw/qhDaqD4wBg0xI3Zw/Lh+Vu7ICoK5hVeT6DbTW3GOBAY+M8K
|
||||
//UXBSGhQ+/9ZZTmyyK0JZ9nw2RAG3lONU6wS41pZhB7F4siatZfP/JJfU6p+ohe8m
|
||||
//n22hTw4brY/8E/tjuki9T5e2GeiUPBhjbdECkkVXMYBPKDZhAkEA5h/b/HBcsIZZ
|
||||
//mL2d3dyWkXR/IxngQa4NH3124M8MfBqCYXPLgD7RDI+3oT/uVe+N0vu6+7CSMVx6
|
||||
//INM67CuE0QJBAMBpKW54cfMsMya3CM1BfdPEBzDT5kTMqxJ7ez164PHv9CJCnL0Z
|
||||
//AuWgM/p2WNbAF1yHNxw1eEfNbUWwVX2yhxsCQEtnMQvcPWLSAtWbe/jQaL2scGQt
|
||||
///F9JCp/A2oz7Cto3TXVlHc8dxh3ZkY/ShOO/pLb3KOODjcOCy7mpvOrZr6ECQH32
|
||||
//WoFPqImhrfryaHi3H0C7XFnC30S7GGOJIy0kfI7mn9St9x50eUkKj/yv7YjpSGHy
|
||||
//w0lcV9npyleNEOqxLXECQBL3VRGCfZfhfFpL8z+5+HPKXw6FxWr+p5h8o3CZ6Yi3
|
||||
//OJVN3Mfo6mbz34wswrEdMXn25MzAwbhFQvCVpPZrFwc=
|
||||
//-----END RSA PRIVATE KEY-----`)
|
||||
|
||||
server = mqtt.New(&mqtt.Options{
|
||||
//Capabilities: &mqtt.Capabilities{
|
||||
// ServerKeepAlive: 10000,
|
||||
// ReceiveMaximum: math.MaxUint16,
|
||||
// MaximumMessageExpiryInterval: math.MaxUint32,
|
||||
// MaximumSessionExpiryInterval: math.MaxUint32, // maximum number of seconds to keep disconnected sessions
|
||||
// MaximumClientWritesPending: 65536,
|
||||
// MaximumPacketSize: 0,
|
||||
// MaximumQos: 2,
|
||||
//},
|
||||
})
|
||||
)
|
||||
|
||||
func main() {
|
||||
tcpAddr := flag.String("tcp", ":1883", "network address for TCP listener")
|
||||
redisAddr := flag.String("redis", "172.17.0.2:6379", "host address of redis db")
|
||||
wsAddr := flag.String("ws", "", "network address for Websocket listener")
|
||||
infoAddr := flag.String("info", "", "network address for web info dashboard listener")
|
||||
infoAddr := flag.String("info", ":8080", "network address for web info dashboard listener")
|
||||
path := flag.String("path", "", "path to data auth file")
|
||||
fullchain := flag.String("full_chain_path", "", "path to fullchain.pem certificate")
|
||||
privkey := flag.String("private_key_path", "", "path to privkey.pem certificate")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
|
|
@ -72,12 +109,50 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
if *tcpAddr != "" {
|
||||
tcp := listeners.NewTCP("t1", *tcpAddr, nil)
|
||||
err := server.AddListener(tcp)
|
||||
if *fullchain != "" && *privkey != "" {
|
||||
chain, err := ioutil.ReadFile(*fullchain)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
priv, err := ioutil.ReadFile(*fullchain)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair(chain, priv)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
//Basic TLS Config
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
|
||||
if *tcpAddr != "" {
|
||||
tcp := listeners.NewTCP("t1", *tcpAddr, &listeners.Config{
|
||||
TLSConfig: tlsConfig,
|
||||
})
|
||||
err := server.AddListener(tcp)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Mqtt Broker is running with TLS")
|
||||
} else {
|
||||
if *tcpAddr != "" {
|
||||
//tcp := listeners.NewTCP("t1", *tcpAddr, &listeners.Config{
|
||||
// TLSConfig: tlsConfig,
|
||||
//})
|
||||
tcp := listeners.NewTCP("t1", *tcpAddr, nil)
|
||||
err := server.AddListener(tcp)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
log.Println("Mqtt Broker is running without TLS, (it's dangerous)")
|
||||
}
|
||||
|
||||
if *wsAddr != "" {
|
||||
|
|
@ -146,11 +221,6 @@ func (h *MyHook) Init(config any) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (h *MyHook) Red(config any) error {
|
||||
h.Log.Info().Msg("initialised")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MyHook) OnDisconnect(cl *mqtt.Client, err error, expire bool) {
|
||||
var clUser string
|
||||
if len(cl.Properties.Props.User) > 0 {
|
||||
|
|
@ -158,8 +228,7 @@ func (h *MyHook) OnDisconnect(cl *mqtt.Client, err error, expire bool) {
|
|||
}
|
||||
|
||||
if clUser != "" {
|
||||
sn := strings.Split(clUser, "-")
|
||||
err := server.Publish("oktopus/disconnect", []byte(sn[1]), false, 1)
|
||||
err := server.Publish("oktopus/disconnect", []byte(clUser), false, 1)
|
||||
if err != nil {
|
||||
log.Println("server publish error: ", err)
|
||||
}
|
||||
|
|
|
|||
1
backend/services/mochi/cmd/run.sh
Normal file
1
backend/services/mochi/cmd/run.sh
Normal file
|
|
@ -0,0 +1 @@
|
|||
go run . -path auth.json
|
||||
|
|
@ -507,6 +507,8 @@ func (s *Server) sendConnack(cl *Client, reason packets.Code, present bool) erro
|
|||
ServerKeepAliveFlag: true,
|
||||
ReceiveMaximum: s.Options.Capabilities.ReceiveMaximum, // 3.2.2.3.3 Receive Maximum
|
||||
User: []packets.UserProperty{{Key: "subscribe-topic", Val: "oktopus/v1/agent/" + clUser}},
|
||||
//AssignedClientID: clUser,
|
||||
//Device doesn't recognize assigned client id in an attempt of reconnection
|
||||
}
|
||||
|
||||
if reason.Code >= packets.ErrUnspecifiedError.Code {
|
||||
|
|
|
|||
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
|
|
@ -17,7 +17,7 @@ out
|
|||
.DS_Store
|
||||
.eslintcache
|
||||
.idea
|
||||
/.env
|
||||
.env
|
||||
/.env.local
|
||||
/.env.development.local
|
||||
/.env.test.local
|
||||
|
|
|
|||
3257
frontend/package-lock.json
generated
3257
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
BIN
frontend/public/assets/avatars/default-avatar.png
Normal file
BIN
frontend/public/assets/avatars/default-avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
|
|
@ -83,9 +83,9 @@ export const AuthProvider = (props) => {
|
|||
if (isAuthenticated) {
|
||||
const user = {
|
||||
id: '5e86809283e28b96d2d38537',
|
||||
avatar: '/assets/avatars/avatar-anika-visser.png',
|
||||
avatar: '/assets/avatars/default-avatar.png',
|
||||
name: 'Anika Visser',
|
||||
email: 'anika.visser@devias.io'
|
||||
email: 'anika.visser@devias.io',
|
||||
};
|
||||
|
||||
dispatch({
|
||||
|
|
@ -116,9 +116,9 @@ export const AuthProvider = (props) => {
|
|||
|
||||
const user = {
|
||||
id: '5e86809283e28b96d2d38537',
|
||||
avatar: '/assets/avatars/avatar-anika-visser.png',
|
||||
avatar: '/assets/avatars/default-avatar.png',
|
||||
name: 'Anika Visser',
|
||||
email: 'anika.visser@devias.io'
|
||||
email: 'anika.visser@devias.io',
|
||||
};
|
||||
|
||||
dispatch({
|
||||
|
|
@ -128,10 +128,31 @@ export const AuthProvider = (props) => {
|
|||
};
|
||||
|
||||
const signIn = async (email, password) => {
|
||||
if (email !== 'demo@oktopus.io' || password !== 'Password123!') {
|
||||
|
||||
var myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/json");
|
||||
|
||||
var raw = JSON.stringify({
|
||||
"email": email,
|
||||
"password": password
|
||||
});
|
||||
|
||||
var requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: myHeaders,
|
||||
body: raw,
|
||||
redirect: 'follow'
|
||||
};
|
||||
|
||||
let result = await fetch(process.env.NEXT_PUBLIC_REST_ENPOINT+"/auth/login", requestOptions)
|
||||
|
||||
if (result.status != 200) {
|
||||
throw new Error('Please check your email and password');
|
||||
}
|
||||
|
||||
const token = await result.json()
|
||||
|
||||
|
||||
try {
|
||||
window.sessionStorage.setItem('authenticated', 'true');
|
||||
} catch (err) {
|
||||
|
|
@ -140,11 +161,14 @@ export const AuthProvider = (props) => {
|
|||
|
||||
const user = {
|
||||
id: '5e86809283e28b96d2d38537',
|
||||
avatar: '/assets/avatars/avatar-anika-visser.png',
|
||||
avatar: '/assets/avatars/default-avatar.png',
|
||||
name: 'Anika Visser',
|
||||
email: 'anika.visser@devias.io'
|
||||
email: 'anika.visser@devias.io',
|
||||
token: token
|
||||
};
|
||||
|
||||
localStorage.setItem("token", token)
|
||||
|
||||
dispatch({
|
||||
type: HANDLERS.SIGN_IN,
|
||||
payload: user
|
||||
|
|
@ -168,7 +192,7 @@ export const AuthProvider = (props) => {
|
|||
skip,
|
||||
signIn,
|
||||
signUp,
|
||||
signOut
|
||||
signOut,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const AccountPopover = (props) => {
|
|||
const router = useRouter();
|
||||
const auth = useAuth();
|
||||
|
||||
|
||||
const handleSignOut = useCallback(
|
||||
() => {
|
||||
onClose?.();
|
||||
|
|
@ -42,7 +43,7 @@ export const AccountPopover = (props) => {
|
|||
color="text.secondary"
|
||||
variant="body2"
|
||||
>
|
||||
Anika Visser
|
||||
{auth.user.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider />
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { alpha } from '@mui/material/styles';
|
||||
import { usePopover } from 'src/hooks/use-popover';
|
||||
import { AccountPopover } from './account-popover';
|
||||
import { useAuth } from 'src/hooks/use-auth';
|
||||
|
||||
const SIDE_NAV_WIDTH = 280;
|
||||
const TOP_NAV_HEIGHT = 64;
|
||||
|
|
@ -24,8 +25,9 @@ export const TopNav = (props) => {
|
|||
const { onNavOpen } = props;
|
||||
const lgUp = useMediaQuery((theme) => theme.breakpoints.up('lg'));
|
||||
const accountPopover = usePopover();
|
||||
const auth = useAuth();
|
||||
|
||||
return (
|
||||
return ( auth.user &&
|
||||
<>
|
||||
<Box
|
||||
component="header"
|
||||
|
|
@ -106,7 +108,7 @@ export const TopNav = (props) => {
|
|||
height: 40,
|
||||
width: 40
|
||||
}}
|
||||
src="/assets/avatars/avatar-anika-visser.png"
|
||||
src={auth.user.avatar}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ const Page = () => {
|
|||
const [method, setMethod] = useState('email');
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
email: 'demo@oktopus.io',
|
||||
password: 'Password123!',
|
||||
email: '',
|
||||
password: '',
|
||||
submit: null
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
|
|
@ -99,7 +99,7 @@ const Page = () => {
|
|||
<Typography variant="h4">
|
||||
Login
|
||||
</Typography>
|
||||
<Typography
|
||||
{/* <Typography
|
||||
color="text.secondary"
|
||||
variant="body2"
|
||||
>
|
||||
|
|
@ -113,7 +113,7 @@ const Page = () => {
|
|||
>
|
||||
Register
|
||||
</Link>
|
||||
</Typography>
|
||||
</Typography> */}
|
||||
</Stack>
|
||||
{/*<Tabs
|
||||
onChange={handleMethodChange}
|
||||
|
|
@ -158,9 +158,9 @@ const Page = () => {
|
|||
value={formik.values.password}
|
||||
/>
|
||||
</Stack>
|
||||
<FormHelperText sx={{ mt: 1 }}>
|
||||
{/* <FormHelperText sx={{ mt: 1 }}>
|
||||
Optionally you can skip.
|
||||
</FormHelperText>
|
||||
</FormHelperText> */}
|
||||
{formik.errors.submit && (
|
||||
<Typography
|
||||
color="error"
|
||||
|
|
@ -179,21 +179,25 @@ const Page = () => {
|
|||
>
|
||||
Continue
|
||||
</Button>
|
||||
<Button
|
||||
{/* <Button
|
||||
fullWidth
|
||||
size="large"
|
||||
sx={{ mt: 3 }}
|
||||
onClick={handleSkip}
|
||||
>
|
||||
Skip authentication
|
||||
</Button>
|
||||
</Button> */}
|
||||
<Alert
|
||||
color="primary"
|
||||
severity="info"
|
||||
sx={{ mt: 3 }}
|
||||
>
|
||||
<div>
|
||||
You can use <b>demo@oktopus.io</b> and password <b>Password123!</b>
|
||||
Don't have an account? ask one for us at <Link
|
||||
href="https://github.com/leandrofars/oktopus"
|
||||
underline="hover">
|
||||
Github
|
||||
</Link>
|
||||
</div>
|
||||
</Alert>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,108 +1,67 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { subDays, subHours } from 'date-fns';
|
||||
import { Box, Container, Unstable_Grid2 as Grid } from '@mui/material';
|
||||
import { Layout as DashboardLayout } from 'src/layouts/dashboard/layout';
|
||||
import { OverviewBudget } from 'src/sections/overview/overview-budget';
|
||||
import { OverviewLatestOrders } from 'src/sections/overview/overview-latest-orders';
|
||||
import { OverviewLatestProducts } from 'src/sections/overview/overview-latest-products';
|
||||
import { OverviewSales } from 'src/sections/overview/overview-sales';
|
||||
import { OverviewTasksProgress } from 'src/sections/overview/overview-tasks-progress';
|
||||
import { OverviewTotalCustomers } from 'src/sections/overview/overview-total-customers';
|
||||
import { OverviewTotalProfit } from 'src/sections/overview/overview-total-profit';
|
||||
import { OverviewTraffic } from 'src/sections/overview/overview-traffic';
|
||||
import { useAuth } from 'src/hooks/use-auth';
|
||||
|
||||
const now = new Date();
|
||||
const Page = () => {
|
||||
const auth = useAuth();
|
||||
const [devices, setDevices] = useState([]);
|
||||
|
||||
const Page = () => (
|
||||
useEffect(() => {
|
||||
|
||||
if (auth.user.token) {
|
||||
console.log("auth.user.token =", auth.user.token)
|
||||
}else{
|
||||
auth.user.token = localStorage.getItem("token")
|
||||
}
|
||||
|
||||
var myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/json");
|
||||
myHeaders.append("Authorization", auth.user.token);
|
||||
|
||||
var requestOptions = {
|
||||
method: 'GET',
|
||||
headers: myHeaders,
|
||||
redirect: 'follow'
|
||||
}
|
||||
|
||||
fetch(process.env.NEXT_PUBLIC_REST_ENPOINT+'/device', requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(json => setDevices(json))
|
||||
.catch(error => console.error('Error:', error));
|
||||
}, []);
|
||||
|
||||
return (devices &&
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
Oktopus | TR-369
|
||||
</title>
|
||||
</Head>
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
py: 8,
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="xl" >
|
||||
<Grid
|
||||
container
|
||||
spacing={3}
|
||||
>
|
||||
</Grid>
|
||||
<OverviewLatestOrders
|
||||
orders={[
|
||||
{
|
||||
id: 'f69f88012978187a6c12897f',
|
||||
ref: 'DEV1049',
|
||||
amount: 30.5,
|
||||
customer: {
|
||||
name: 'Ekaterina Tankova'
|
||||
},
|
||||
createdAt: 1555016400000,
|
||||
status: 'Associating'
|
||||
},
|
||||
{
|
||||
id: '9eaa1c7dd4433f413c308ce2',
|
||||
ref: 'DEV1048',
|
||||
amount: 25.1,
|
||||
customer: {
|
||||
name: 'Cao Yu'
|
||||
},
|
||||
createdAt: 1555016400000,
|
||||
status: 'Online'
|
||||
},
|
||||
{
|
||||
id: '01a5230c811bd04996ce7c13',
|
||||
ref: 'DEV1047',
|
||||
amount: 10.99,
|
||||
customer: {
|
||||
name: 'Alexa Richardson'
|
||||
},
|
||||
createdAt: 1554930000000,
|
||||
status: 'Offline'
|
||||
},
|
||||
{
|
||||
id: '1f4e1bd0a87cea23cdb83d18',
|
||||
ref: 'DEV1046',
|
||||
amount: 96.43,
|
||||
customer: {
|
||||
name: 'Anje Keizer'
|
||||
},
|
||||
createdAt: 1554757200000,
|
||||
status: 'Associating'
|
||||
},
|
||||
{
|
||||
id: '9f974f239d29ede969367103',
|
||||
ref: 'DEV1045',
|
||||
amount: 32.54,
|
||||
customer: {
|
||||
name: 'Clarke Gillebert'
|
||||
},
|
||||
createdAt: 1554670800000,
|
||||
status: 'Online'
|
||||
},
|
||||
{
|
||||
id: 'ffc83c1560ec2f66a1c05596',
|
||||
ref: 'DEV1044',
|
||||
amount: 16.76,
|
||||
customer: {
|
||||
name: 'Adam Denisov'
|
||||
},
|
||||
createdAt: 1554670800000,
|
||||
status: 'Online'
|
||||
}
|
||||
]}
|
||||
sx={{ height: '100%' }}
|
||||
/>
|
||||
</Container>
|
||||
</Box>
|
||||
<Head>
|
||||
<title>
|
||||
Oktopus | TR-369
|
||||
</title>
|
||||
</Head>
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
py: 8,
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="xl" >
|
||||
<Grid
|
||||
container
|
||||
spacing={3}
|
||||
>
|
||||
</Grid>
|
||||
<OverviewLatestOrders
|
||||
orders={devices}
|
||||
sx={{ height: '100%' }}
|
||||
/>
|
||||
</Container>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
)
|
||||
}
|
||||
Page.getLayout = (page) => (
|
||||
<DashboardLayout>
|
||||
{page}
|
||||
|
|
@ -110,125 +69,3 @@ Page.getLayout = (page) => (
|
|||
);
|
||||
|
||||
export default Page;
|
||||
|
||||
/*
|
||||
<OverviewSales
|
||||
chartSeries={[
|
||||
{
|
||||
name: 'This year',
|
||||
data: [18, 16, 5, 8, 3, 14, 14, 16, 17, 19, 18, 20]
|
||||
},
|
||||
{
|
||||
name: 'Last year',
|
||||
data: [12, 11, 4, 6, 2, 9, 9, 10, 11, 12, 13, 13]
|
||||
}
|
||||
]}
|
||||
sx={{ height: '100%' }}
|
||||
/>
|
||||
<OverviewLatestProducts
|
||||
products={[
|
||||
{
|
||||
id: '5ece2c077e39da27658aa8a9',
|
||||
image: '/assets/products/product-1.png',
|
||||
name: 'Healthcare Erbology',
|
||||
updatedAt: subHours(now, 6).getTime()
|
||||
},
|
||||
{
|
||||
id: '5ece2c0d16f70bff2cf86cd8',
|
||||
image: '/assets/products/product-2.png',
|
||||
name: 'Makeup Lancome Rouge',
|
||||
updatedAt: subDays(subHours(now, 8), 2).getTime()
|
||||
},
|
||||
{
|
||||
id: 'b393ce1b09c1254c3a92c827',
|
||||
image: '/assets/products/product-5.png',
|
||||
name: 'Skincare Soja CO',
|
||||
updatedAt: subDays(subHours(now, 1), 1).getTime()
|
||||
},
|
||||
{
|
||||
id: 'a6ede15670da63f49f752c89',
|
||||
image: '/assets/products/product-6.png',
|
||||
name: 'Makeup Lipstick',
|
||||
updatedAt: subDays(subHours(now, 3), 3).getTime()
|
||||
},
|
||||
{
|
||||
id: 'bcad5524fe3a2f8f8620ceda',
|
||||
image: '/assets/products/product-7.png',
|
||||
name: 'Healthcare Ritual',
|
||||
updatedAt: subDays(subHours(now, 5), 6).getTime()
|
||||
}
|
||||
]}
|
||||
sx={{ height: '100%' }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid
|
||||
xs={12}
|
||||
md={12}
|
||||
lg={8}
|
||||
>
|
||||
<OverviewLatestOrders
|
||||
orders={[
|
||||
{
|
||||
id: 'f69f88012978187a6c12897f',
|
||||
ref: 'DEV1049',
|
||||
amount: 30.5,
|
||||
customer: {
|
||||
name: 'Ekaterina Tankova'
|
||||
},
|
||||
createdAt: 1555016400000,
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: '9eaa1c7dd4433f413c308ce2',
|
||||
ref: 'DEV1048',
|
||||
amount: 25.1,
|
||||
customer: {
|
||||
name: 'Cao Yu'
|
||||
},
|
||||
createdAt: 1555016400000,
|
||||
status: 'Online'
|
||||
},
|
||||
{
|
||||
id: '01a5230c811bd04996ce7c13',
|
||||
ref: 'DEV1047',
|
||||
amount: 10.99,
|
||||
customer: {
|
||||
name: 'Alexa Richardson'
|
||||
},
|
||||
createdAt: 1554930000000,
|
||||
status: 'refunded'
|
||||
},
|
||||
{
|
||||
id: '1f4e1bd0a87cea23cdb83d18',
|
||||
ref: 'DEV1046',
|
||||
amount: 96.43,
|
||||
customer: {
|
||||
name: 'Anje Keizer'
|
||||
},
|
||||
createdAt: 1554757200000,
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: '9f974f239d29ede969367103',
|
||||
ref: 'DEV1045',
|
||||
amount: 32.54,
|
||||
customer: {
|
||||
name: 'Clarke Gillebert'
|
||||
},
|
||||
createdAt: 1554670800000,
|
||||
status: 'Online'
|
||||
},
|
||||
{
|
||||
id: 'ffc83c1560ec2f66a1c05596',
|
||||
ref: 'DEV1044',
|
||||
amount: 16.76,
|
||||
customer: {
|
||||
name: 'Adam Denisov'
|
||||
},
|
||||
createdAt: 1554670800000,
|
||||
status: 'Online'
|
||||
}
|
||||
]}
|
||||
sx={{ height: '100%' }}
|
||||
/>
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -21,11 +21,23 @@ import { SeverityPill } from 'src/components/severity-pill';
|
|||
import { useRouter } from 'next/router';
|
||||
|
||||
const statusMap = {
|
||||
Associating: 'warning',
|
||||
Online: 'success',
|
||||
Offline: 'error'
|
||||
1: 'warning',
|
||||
0: 'success',
|
||||
2: 'error'
|
||||
};
|
||||
|
||||
const status = (s)=>{
|
||||
if (s == 0){
|
||||
return "Online"
|
||||
} else if (s == 1){
|
||||
return "Associating"
|
||||
}else if (s==2){
|
||||
return "Offline"
|
||||
}else {
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
export const OverviewLatestOrders = (props) => {
|
||||
const { orders = [], sx } = props;
|
||||
|
||||
|
|
@ -39,15 +51,18 @@ export const OverviewLatestOrders = (props) => {
|
|||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell align="center">
|
||||
Serial Number
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
Model
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
Customer
|
||||
</TableCell>
|
||||
<TableCell sortDirection="desc">
|
||||
Vendor
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
Version
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
Status
|
||||
</TableCell>
|
||||
|
|
@ -58,26 +73,28 @@ export const OverviewLatestOrders = (props) => {
|
|||
</TableHead>
|
||||
<TableBody>
|
||||
{orders.map((order) => {
|
||||
const createdAt = format(order.createdAt, 'dd/MM/yyyy');
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
hover
|
||||
key={order.id}
|
||||
key={order.SN}
|
||||
>
|
||||
<TableCell>
|
||||
{order.ref}
|
||||
<TableCell TableCell align="center">
|
||||
{order.SN}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{order.customer.name}
|
||||
{order.Model}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{createdAt}
|
||||
{order.Vendor}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<SeverityPill color={statusMap[order.status]}>
|
||||
{order.status}
|
||||
</SeverityPill>
|
||||
{order.Version}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<SeverityPill color={statusMap[order.Status]}>
|
||||
{status(order.Status)}
|
||||
</SeverityPill>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<SvgIcon
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user