diff --git a/backend/services/controller/go.mod b/backend/services/controller/go.mod
index 5507d84..4504b6f 100755
--- a/backend/services/controller/go.mod
+++ b/backend/services/controller/go.mod
@@ -9,6 +9,7 @@ require (
)
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
diff --git a/backend/services/controller/go.sum b/backend/services/controller/go.sum
index 5d46421..f365626 100644
--- a/backend/services/controller/go.sum
+++ b/backend/services/controller/go.sum
@@ -1,6 +1,8 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/eclipse/paho.golang v0.10.0 h1:oUGPjRwWcZQRgDD9wVDV7y7i7yBSxts3vcvcNJo8B4Q=
github.com/eclipse/paho.golang v0.10.0/go.mod h1:rhrV37IEwauUyx8FHrvmXOKo+QRKng5ncoN1vJiJMcs=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
diff --git a/backend/services/controller/internal/api/api.go b/backend/services/controller/internal/api/api.go
index 14f9d7f..09cbb52 100644
--- a/backend/services/controller/internal/api/api.go
+++ b/backend/services/controller/internal/api/api.go
@@ -3,6 +3,7 @@ package api
import (
"encoding/json"
"github.com/gorilla/mux"
+ "github.com/leandrofars/oktopus/internal/api/auth"
"github.com/leandrofars/oktopus/internal/api/middleware"
"github.com/leandrofars/oktopus/internal/db"
"github.com/leandrofars/oktopus/internal/mtp"
@@ -36,17 +37,19 @@ func NewApi(port string, db db.Database, b mtp.Broker, msgQueue map[string](chan
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")
+ iot := r.PathPrefix("/device").Subrouter()
+ 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")
+ iot.HandleFunc("/{sn}/set", a.deviceUpdateMsg).Methods("PUT")
+ //TODO: Create operation action handler
+ iot.HandleFunc("/device/{sn}/act", a.deviceUpdateMsg).Methods("PUT")
- r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- return
- })
- r.HandleFunc("/devices", a.retrieveDevices).Methods("GET")
- r.HandleFunc("/device/{sn}/get", a.deviceGetMsg).Methods("PUT")
- r.HandleFunc("/device/{sn}/add", a.deviceCreateMsg).Methods("PUT")
- r.HandleFunc("/device/{sn}/del", a.deviceDeleteMsg).Methods("PUT")
- r.HandleFunc("/device/{sn}/set", a.deviceUpdateMsg).Methods("PUT")
-
- r.Use(func(handler http.Handler) http.Handler {
+ iot.Use(func(handler http.Handler) http.Handler {
return middleware.Middleware(handler)
})
@@ -274,3 +277,61 @@ func (a *Api) deviceExists(sn string, w http.ResponseWriter) {
return
}
}
+
+func (a *Api) registerUser(w http.ResponseWriter, r *http.Request) {
+ var user db.User
+ err := json.NewDecoder(r.Body).Decode(&user)
+ if err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ if err := user.HashPassword(user.Password); err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ if err := a.Db.RegisterUser(user); err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+}
+
+type TokenRequest struct {
+ Email string `json:"email"`
+ Password string `json:"password"`
+}
+
+func (a *Api) generateToken(w http.ResponseWriter, r *http.Request) {
+ var tokenReq TokenRequest
+
+ err := json.NewDecoder(r.Body).Decode(&tokenReq)
+ if err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ user, err := a.Db.FindUser(tokenReq.Email)
+ if err != nil {
+ w.WriteHeader(http.StatusUnauthorized)
+ json.NewEncoder(w).Encode("Invalid Credentials")
+ return
+ }
+
+ credentialError := user.CheckPassword(tokenReq.Password)
+ if credentialError != nil {
+ w.WriteHeader(http.StatusUnauthorized)
+ json.NewEncoder(w).Encode("Invalid Credentials")
+ return
+ }
+
+ token, err := auth.GenerateJWT(user.Email, user.Name)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Add("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(token)
+ return
+}
diff --git a/backend/services/controller/internal/api/auth/auth.go b/backend/services/controller/internal/api/auth/auth.go
new file mode 100644
index 0000000..af2a12c
--- /dev/null
+++ b/backend/services/controller/internal/api/auth/auth.go
@@ -0,0 +1,54 @@
+package auth
+
+import (
+ "errors"
+ "github.com/dgrijalva/jwt-go"
+ "time"
+)
+
+var jwtKey = []byte("supersecretkey")
+
+type JWTClaim struct {
+ Username string `json:"username"`
+ Email string `json:"email"`
+ jwt.StandardClaims
+}
+
+func GenerateJWT(email string, username string) (tokenString string, err error) {
+ expirationTime := time.Now().Add(1 * time.Hour)
+ claims := &JWTClaim{
+ Email: email,
+ Username: username,
+ StandardClaims: jwt.StandardClaims{
+ ExpiresAt: expirationTime.Unix(),
+ },
+ }
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ tokenString, err = token.SignedString(jwtKey)
+ 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
+ },
+ )
+ if err != nil {
+ return
+ }
+ claims, ok := token.Claims.(*JWTClaim)
+ if !ok {
+ err = errors.New("couldn't parse claims")
+ return
+ }
+ if claims.ExpiresAt < time.Now().Local().Unix() {
+ err = errors.New("token expired")
+ return
+ }
+
+ email = claims.Email
+
+ return
+}
diff --git a/backend/services/controller/internal/api/middleware/middleware.go b/backend/services/controller/internal/api/middleware/middleware.go
index 97edbe4..fad3a59 100644
--- a/backend/services/controller/internal/api/middleware/middleware.go
+++ b/backend/services/controller/internal/api/middleware/middleware.go
@@ -1,12 +1,28 @@
package middleware
-import "net/http"
+import (
+ "github.com/leandrofars/oktopus/internal/api/auth"
+ "golang.org/x/net/context"
+ "net/http"
+)
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
- next.ServeHTTP(w, r)
+
+ tokenString := r.Header.Get("Authorization")
+ if tokenString == "" {
+ w.WriteHeader(http.StatusUnauthorized)
+ return
+ }
+ email, err := auth.ValidateToken(tokenString)
+ if err != nil {
+ w.WriteHeader(http.StatusUnauthorized)
+ return
+ }
+ ctx := context.WithValue(r.Context(), "email", email)
+ next.ServeHTTP(w, r.WithContext(ctx))
},
)
}
diff --git a/backend/services/controller/internal/db/db.go b/backend/services/controller/internal/db/db.go
index 5f217f7..7ff27ba 100644
--- a/backend/services/controller/internal/db/db.go
+++ b/backend/services/controller/internal/db/db.go
@@ -9,6 +9,7 @@ import (
type Database struct {
devices *mongo.Collection
+ users *mongo.Collection
ctx context.Context
}
@@ -22,7 +23,9 @@ func NewDatabase(ctx context.Context, mongoUri string) Database {
log.Println("Connected to MongoDB-->", mongoUri)
devices := client.Database("oktopus").Collection("devices")
+ users := client.Database("oktopus").Collection("users")
db.devices = devices
+ db.users = users
db.ctx = ctx
return db
}
diff --git a/backend/services/controller/internal/db/user.go b/backend/services/controller/internal/db/user.go
new file mode 100644
index 0000000..1552f0d
--- /dev/null
+++ b/backend/services/controller/internal/db/user.go
@@ -0,0 +1,49 @@
+package db
+
+import (
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "golang.org/x/crypto/bcrypt"
+ "log"
+)
+
+type User struct {
+ Email string `json:"email"`
+ Name string `json:"name"`
+ Password string `json:"password"`
+}
+
+func (d *Database) RegisterUser(user User) error {
+ err := d.users.FindOne(d.ctx, bson.D{{"email", user.Email}}).Err()
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ _, err = d.users.InsertOne(d.ctx, user)
+ return err
+ }
+ log.Println(err)
+ }
+ return err
+}
+
+func (d *Database) FindUser(email string) (User, error) {
+ var result User
+ err := d.users.FindOne(d.ctx, bson.D{{"email", email}}).Decode(&result)
+ return result, err
+}
+
+func (user *User) HashPassword(password string) error {
+ bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
+ if err != nil {
+ return err
+ }
+ user.Password = string(bytes)
+ return nil
+}
+
+func (user *User) CheckPassword(providedPassword string) error {
+ err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(providedPassword))
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/backend/services/controller/internal/mqtt/mqtt.go b/backend/services/controller/internal/mqtt/mqtt.go
index b1b3fa0..7a53378 100644
--- a/backend/services/controller/internal/mqtt/mqtt.go
+++ b/backend/services/controller/internal/mqtt/mqtt.go
@@ -252,7 +252,6 @@ func (m *Mqtt) handleApiRequest(api []byte) {
log.Println(err)
}
- //TODO: verify record operation type
var msg usp_msg.Msg
err = proto.Unmarshal(record.GetNoSessionContext().Payload, &msg)
if err != nil {
diff --git a/frontend/src/sections/settings/color-theme.js b/frontend/src/sections/settings/color-theme.js
new file mode 100644
index 0000000..1a5bff5
--- /dev/null
+++ b/frontend/src/sections/settings/color-theme.js
@@ -0,0 +1,62 @@
+import * as React from 'react';
+import IconButton from '@mui/material/IconButton';
+import Box from '@mui/material/Box';
+import { useTheme, ThemeProvider, createTheme } from '@mui/material/styles';
+import Brightness4Icon from '@mui/icons-material/Brightness4';
+import Brightness7Icon from '@mui/icons-material/Brightness7';
+
+const ColorModeContext = React.createContext({ toggleColorMode: () => {} });
+
+function MyApp() {
+ const theme = useTheme();
+ const colorMode = React.useContext(ColorModeContext);
+ return (
+
+ {theme.palette.mode} mode
+
+ {theme.palette.mode === 'dark' ? : }
+
+
+ );
+}
+
+export default function ToggleColorMode() {
+ const [mode, setMode] = React.useState<'light' | 'dark'>('light');
+ const colorMode = React.useMemo(
+ () => ({
+ toggleColorMode: () => {
+ setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light'));
+ },
+ }),
+ [],
+ );
+
+ const theme = React.useMemo(
+ () =>
+ createTheme({
+ palette: {
+ mode,
+ },
+ }),
+ [mode],
+ );
+
+ return (
+
+
+
+
+
+ );
+}
\ No newline at end of file