add new lights, add login and role to it, add services page

This commit is contained in:
Adrian Zuercher
2025-08-09 18:00:36 +02:00
parent 7d2ab814da
commit 1697a4dcfd
56 changed files with 1337 additions and 290 deletions

View File

@@ -1,6 +1,7 @@
package dbRequest
import (
"backend/models"
"fmt"
"net/http"
@@ -10,25 +11,22 @@ import (
var DBCreate string = `CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
role TEXT NOT NULL,
password TEXT NOT NULL
);`
var DBNewUser string = `INSERT INTO users (username, password) VALUES (?, ?)`
var DBQueryPassword string = `SELECT password FROM users WHERE username = ?`
var DBNewUser string = `INSERT INTO users (username, role, password) VALUES (?, ?, ?)`
var DBQueryPassword string = `SELECT role, password FROM users WHERE username = ?`
var DBUserLookup string = `SELECT EXISTS(SELECT 1 FROM users WHERE username = ?)`
var DBRemoveUser string = `DELETE FROM users WHERE username = $1`
func CheckDBError(c *gin.Context, username string, err error) bool {
if err != nil {
if err.Error() == "sql: no rows in result set" {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("no user '%s' found", username),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorMessageResponse(fmt.Sprintf("no user '%s' found", username)))
return true
}
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return true
}
return false

Binary file not shown.

View File

@@ -0,0 +1,28 @@
{
"drivers":[{
"name":"ArtNet",
"description":"ArtNet Driver DMX over UDP/IP",
"executablePath":"bin/tecamino-driver-artNet-linux-arm64",
"workingDirectory":"bin",
"arguments":[
"-cfg './cfg'",
"-serverIp '127.0.0.1'",
"-serverPort 8100",
"-port 8200"
]
},
{
"name":"OSC Driver",
"description":"OSC Driver Music Mixer over UDP/IP",
"executablePath":"bin/tecamino-driver-osc-linux-arm64",
"workingDirectory":"bin",
"arguments":[
"-cfg './cfg'",
"-serverIp '127.0.0.1'",
"-serverPort 8100",
"-port 8300"
]
}
]
}

View File

@@ -0,0 +1,36 @@
package drivers
import (
"backend/models"
"encoding/json"
"net/http"
"os"
"github.com/gin-gonic/gin"
)
type DriverHandler struct {
driversConfig string
}
func NewDriverHandler(cfgDir string) *DriverHandler {
return &DriverHandler{driversConfig: cfgDir}
}
func (dH *DriverHandler) GetDriverList(c *gin.Context) {
content, err := os.ReadFile(dH.driversConfig)
if err != nil {
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
var data struct {
Drivers []models.Drivers `json:"drivers"`
}
err = json.Unmarshal(content, &data)
if err != nil {
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
c.JSON(http.StatusOK, data.Drivers)
}

View File

@@ -19,25 +19,19 @@ import (
func (lm *LoginManager) AddUser(c *gin.Context) {
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
user := models.User{}
err = json.Unmarshal(body, &user)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
if !user.IsValid() {
c.JSON(http.StatusBadRequest, gin.H{
"error": "user empty",
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorMessageResponse("user empty"))
return
}
@@ -54,20 +48,16 @@ func (lm *LoginManager) AddUser(c *gin.Context) {
}
if exists {
c.JSON(http.StatusOK, gin.H{
"error": fmt.Sprintf("user '%s' exists already", user.Name),
})
c.JSON(http.StatusOK, models.NewJsonErrorMessageResponse(fmt.Sprintf("user '%s' exists already", user.Name)))
return
}
hash, err := utils.HashPassword(user.Password)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
if _, err := db.Exec(dbRequest.DBNewUser, user.Name, hash); dbRequest.CheckDBError(c, user.Name, err) {
if _, err := db.Exec(dbRequest.DBNewUser, user.Role, user.Name, hash); dbRequest.CheckDBError(c, user.Name, err) {
return
}
@@ -79,25 +69,19 @@ func (lm *LoginManager) AddUser(c *gin.Context) {
func (lm *LoginManager) RemoveUser(c *gin.Context) {
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
user := models.User{}
err = json.Unmarshal(body, &user)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
if !user.IsValid() {
c.JSON(http.StatusBadRequest, gin.H{
"error": "user empty",
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorMessageResponse("user empty"))
return
}
@@ -108,14 +92,12 @@ func (lm *LoginManager) RemoveUser(c *gin.Context) {
defer db.Close()
var storedPassword string
if err := db.QueryRow(dbRequest.DBQueryPassword, user.Name).Scan(&storedPassword); dbRequest.CheckDBError(c, user.Name, err) {
if err := db.QueryRow(dbRequest.DBQueryPassword, user.Name).Scan(&storedPassword, &user.Role); dbRequest.CheckDBError(c, user.Name, err) {
return
}
if !utils.CheckPassword(user.Password, storedPassword) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "wrong password",
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorMessageResponse("wrong password"))
return
}
@@ -131,27 +113,19 @@ func (lm *LoginManager) RemoveUser(c *gin.Context) {
func (lm *LoginManager) Login(c *gin.Context) {
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
user := models.User{}
err = json.Unmarshal(body, &user)
if err != nil {
fmt.Println(2)
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
if !user.IsValid() {
c.JSON(http.StatusBadRequest, gin.H{
"error": "user empty",
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorMessageResponse("user empty"))
return
}
@@ -162,41 +136,36 @@ func (lm *LoginManager) Login(c *gin.Context) {
defer db.Close()
var storedPassword string
if err := db.QueryRow(dbRequest.DBQueryPassword, user.Name).Scan(&storedPassword); dbRequest.CheckDBError(c, user.Name, err) {
if err := db.QueryRow(dbRequest.DBQueryPassword, user.Name).Scan(&user.Role, &storedPassword); dbRequest.CheckDBError(c, user.Name, err) {
return
}
if !utils.CheckPassword(user.Password, storedPassword) {
fmt.Println(2, user.Password)
c.JSON(http.StatusBadRequest, gin.H{
"error": "wrong password",
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorMessageResponse("wrong password"))
return
}
// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"username": user.Name,
"exp": time.Now().Add(time.Hour * 72).Unix(), // expires in 72h
"role": user.Role,
"exp": time.Now().Add(time.Minute * 60).Unix(), // expires in 72h
})
secret, err := utils.GenerateJWTSecret(32) // 32 bytes = 256 bits
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "error generate jwt token"})
c.JSON(http.StatusInternalServerError, models.NewJsonErrorMessageResponse("error generate jwt token"))
return
}
// Sign and get the complete encoded token as a string
tokenString, err := token.SignedString(secret)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Could not generate token"})
c.JSON(http.StatusInternalServerError, models.NewJsonErrorMessageResponse("Could not generate token"))
return
}
c.JSON(http.StatusOK, models.User{
Name: user.Name,
Token: tokenString,
})
}

View File

@@ -37,7 +37,7 @@ func NewLoginManager(dir string) (*LoginManager, error) {
if err != nil {
return nil, err
}
_, err = db.Exec(dbRequest.DBNewUser, "admin", hash)
_, err = db.Exec(dbRequest.DBNewUser, "admin", "admin", hash)
if err != nil {
return nil, err
}

View File

@@ -1,6 +1,7 @@
package main
import (
"backend/drivers"
"backend/login"
"backend/models"
secenes "backend/scenes"
@@ -27,6 +28,8 @@ func main() {
flag.Var(&allowOrigins, "allowOrigin", "Allowed origin (can repeat this flag)")
spa := flag.String("spa", "./dist/spa", "quasar spa files")
cfgDir := flag.String("cfg", "./cfg", "config dir")
driversCfg := flag.String("drivers", "defaultConfigurations.json", "drivers config file name")
workingDir := flag.String("workingDirectory", ".", "quasar spa files")
ip := flag.String("ip", "0.0.0.0", "server listening ip")
port := flag.Uint("port", 9500, "server listening port")
@@ -69,6 +72,9 @@ func main() {
//new scenes handler
scenesHandler := secenes.NewScenesHandler("")
//new scenes handler
driversHandler := drivers.NewDriverHandler(filepath.Join(*cfgDir, *driversCfg))
// new server
s := server.NewServer()
@@ -92,6 +98,7 @@ func main() {
api := s.Routes.Group("/api")
//set routes
api.GET("/loadScenes", scenesHandler.LoadScenes)
api.GET("/allDrivers", driversHandler.GetDriverList)
api.POST("/login", loginManager.Login)
api.POST("/user/add", loginManager.AddUser)
api.POST("/saveScene", scenesHandler.SaveScene)
@@ -104,7 +111,7 @@ func main() {
s.Routes.NoRoute(func(c *gin.Context) {
// Disallow fallback for /api paths
if strings.HasPrefix(c.Request.URL.Path, "/api") {
c.JSON(http.StatusNotFound, gin.H{"error": "API endpoint not found"})
c.JSON(http.StatusNotFound, models.NewJsonErrorMessageResponse("API endpoint not found"))
return
}
// Try to serve file from SPA directory

View File

@@ -0,0 +1,9 @@
package models
type Drivers struct {
Name string `json:"name"`
Description string `json:"description"`
ExecutablePath string `json:"executablePath,omitempty"`
WorkingDirectory string `json:"workingDirectory,omitempty"`
Arguments []string `json:"arguments,omitempty"`
}

View File

@@ -0,0 +1,20 @@
package models
type JsonResponse struct {
Error bool `json:"error,omitempty"`
Message string `json:"message,omitempty"`
}
func NewJsonErrorMessageResponse(msg string) JsonResponse {
return JsonResponse{
Error: true,
Message: msg,
}
}
func NewJsonErrorResponse(err error) JsonResponse {
return JsonResponse{
Error: true,
Message: err.Error(),
}
}

View File

@@ -1,3 +0,0 @@
package models
type LightBar []Value

View File

@@ -1,3 +0,0 @@
package models
type MovingHead []Value

View File

@@ -2,6 +2,7 @@ package models
type User struct {
Name string `json:"user"`
Role string `json:"role"`
Password string `json:"password,omitempty"`
Token string `json:"token,omitempty"`
}

View File

@@ -1,7 +1,9 @@
package models
type Values struct {
MovingHead *MovingHead `json:"movingHead"`
LightBar *LightBar `json:"lightBar"`
Value any `json:"value"`
Stagelights []Value `json:"stageLights,omitempty"`
LightBar []Value `json:"lightBar,omitempty"`
FloogLights []Value `json:"floodLights,omitempty"`
MovingHead []Value `json:"movingHead,omitempty"`
Value any `json:"value"`
}

View File

@@ -31,45 +31,35 @@ func NewScenesHandler(dir string) *ScenesHandler {
func (sh *ScenesHandler) SaveScene(c *gin.Context) {
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
var scene models.Scene
err = json.Unmarshal(body, &scene)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
if _, err := os.Stat(path.Join(sh.dir)); err != nil {
err := os.MkdirAll(sh.dir, 0755)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
}
f, err := os.OpenFile(path.Join(sh.dir, scene.Name+".scene"), os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0644)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
defer f.Close()
_, err = f.Write(body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
@@ -81,26 +71,20 @@ func (sh *ScenesHandler) SaveScene(c *gin.Context) {
func (sh *ScenesHandler) DeleteScene(c *gin.Context) {
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
var scene models.Scene
err = json.Unmarshal(body, &scene)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
err = os.Remove(path.Join(sh.dir, scene.Name+".scene"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
@@ -114,26 +98,20 @@ func (sh *ScenesHandler) LoadScenes(c *gin.Context) {
files, err := utils.FindAllFiles("./scenes", ".scene")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
for _, f := range files {
content, err := os.ReadFile(f)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
var scene models.Scene
err = json.Unmarshal(content, &scene)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
sceneMap[scene.Name] = scene
@@ -160,9 +138,7 @@ func (sh *ScenesHandler) LoadScenes(c *gin.Context) {
func (sh *ScenesHandler) LoadScene(c *gin.Context) {
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
@@ -170,17 +146,13 @@ func (sh *ScenesHandler) LoadScene(c *gin.Context) {
err = json.Unmarshal(body, &scene)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
files, err := utils.FindAllFiles("./scenes", ".scene")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
@@ -190,24 +162,18 @@ func (sh *ScenesHandler) LoadScene(c *gin.Context) {
}
content, err := os.ReadFile(f)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
err = json.Unmarshal(content, &scene)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
c.JSON(http.StatusOK, scene)
return
}
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Errorf("scene '%s' not found", scene.Name),
})
c.JSON(http.StatusBadRequest, models.NewJsonErrorMessageResponse(fmt.Sprintf("scene '%s' not found", scene.Name)))
}

Binary file not shown.