diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml
index bfc25c5..29878b4 100644
--- a/.gitea/workflows/build.yml
+++ b/.gitea/workflows/build.yml
@@ -104,7 +104,7 @@ jobs:
export GOMODCACHE=/data/gomodcache
mkdir -p $GOCACHE $GOMODCACHE
- OUTPUT="../server-${{ matrix.goos }}-${{ matrix.arch }}${{ matrix.ext }}"
+ OUTPUT="../memberApp-${{ matrix.goos }}-${{ matrix.arch }}${{ matrix.ext }}"
if [ "${{ matrix.arch }}" = "arm" ]; then
GOARM=${{ matrix.arm_version }}
fi
diff --git a/.gitignore b/.gitignore
index a44f7fe..50f974d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,3 +42,4 @@ yarn-error.log*
# golang quasar websever executable
backend/server-linux-arm64
+backend/cert
diff --git a/backend/bin/dist/allowOrigins.json b/backend/bin/dist/allowOrigins.json
deleted file mode 100644
index 152ffd2..0000000
--- a/backend/bin/dist/allowOrigins.json
+++ /dev/null
@@ -1 +0,0 @@
-["http://localhost:9000", "http://localhost:9500", "http://127.0.0.1:9500"]
\ No newline at end of file
diff --git a/backend/dbRequest/dbRequest.go b/backend/dbRequest/dbRequest.go
index ce0a72f..76afc8f 100644
--- a/backend/dbRequest/dbRequest.go
+++ b/backend/dbRequest/dbRequest.go
@@ -8,19 +8,29 @@ import (
"github.com/gin-gonic/gin"
)
-var DBCreate string = `CREATE TABLE IF NOT EXISTS users (
+var CreateUserTable string = `CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
+ email TEXT NOT NULL,
role TEXT NOT NULL,
password TEXT NOT NULL,
settings TEXT NOT NULL
);`
-var DBNewUser string = `INSERT INTO users (username, role, password, settings) VALUES (?, ?, ?, ?)`
-var DBQueryPassword string = `SELECT role, password, settings FROM users WHERE username = ?`
+var CreateRoleTable string = `CREATE TABLE IF NOT EXISTS roles (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ role TEXT NOT NULL,
+ rights TEXT NOT NULL
+ );`
+
+var NewUser string = `INSERT INTO users (username, email, role, password, settings) VALUES (?, ?, ?, ?, ?)`
+var NewRole = `INSERT INTO roles (role, rights) VALUES (?, ?)`
+var DBQueryPassword string = `SELECT id, role, password, settings FROM users WHERE username = ?`
var DBUserLookup string = `SELECT EXISTS(SELECT 1 FROM users WHERE username = ?)`
+var DBRoleLookup string = `SELECT EXISTS(SELECT 1 FROM roles WHERE role = ?)`
var DBRemoveUser string = `DELETE FROM users WHERE username = $1`
var DBUpdateSettings string = `UPDATE users SET settings = ? WHERE username = ?`
+var DBUpdateRole string = `UPDATE roles SET rights = ? WHERE role = ?`
func CheckDBError(c *gin.Context, username string, err error) bool {
if err != nil {
diff --git a/backend/main.go b/backend/main.go
index 9933c1b..b40d8ae 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -14,6 +14,8 @@ import (
"strings"
"time"
+ "gitea.tecamino.com/paadi/tecamino-dbm/cert"
+
dbApi "gitea.tecamino.com/paadi/memberDB/api"
"gitea.tecamino.com/paadi/tecamino-logger/logging"
"github.com/gin-contrib/cors"
@@ -28,13 +30,16 @@ func main() {
spa := flag.String("spa", "./dist/spa", "quasar spa files")
workingDir := flag.String("workingDirectory", ".", "quasar spa files")
ip := flag.String("ip", "0.0.0.0", "server listening ip")
+ organization := flag.String("organization", "", "self signed ciertificate organization")
port := flag.Uint("port", 9500, "server listening port")
+ https := flag.Bool("https", false, "serves as https needs flag -cert and -chain")
+ sslCert := flag.String("cert", "", "ssl certificate path")
+ sslChain := flag.String("chain", "", "ssl chain path")
debug := flag.Bool("debug", false, "log debug")
flag.Parse()
//change working directory only if value is given
if *workingDir != "." && *workingDir != "" {
- fmt.Println(1, *workingDir)
os.Chdir(*workingDir)
}
@@ -72,13 +77,18 @@ func main() {
dbHandler := dbApi.NewAPIHandler()
//get local ip
- allowOrigins = append(allowOrigins, "http://localhost:9000", "http://localhost:9500", "http://127.0.0.1:9500")
+ httpString := "http://"
+ if *https {
+ httpString = "https://"
+
+ }
+ allowOrigins = append(allowOrigins, httpString+"localhost:9000", httpString+"localhost:9500", httpString+"127.0.0.1:9500")
localIP, err := utils.GetLocalIP()
if err != nil {
logger.Error("main", fmt.Sprintf("get local ip : %s", err.Error()))
} else {
- allowOrigins = append(allowOrigins, fmt.Sprintf("http://%s:9000", localIP), fmt.Sprintf("http://%s:9500", localIP))
+ allowOrigins = append(allowOrigins, fmt.Sprintf("%s%s:9000", httpString, localIP), fmt.Sprintf("%s%s:9500", httpString, localIP))
}
s.Routes.Use(cors.New(cors.Config{
@@ -102,18 +112,26 @@ func main() {
//private
auth := api.Group("/secure", user.AuthMiddleware())
+ auth.GET("/users", userManager.GetUserById)
auth.GET("/members", dbHandler.GetMemberById)
+ auth.GET("/roles", userManager.GetRoleById)
auth.POST("database/open", dbHandler.OpenDatabase)
auth.POST("/members/add", dbHandler.AddNewMember)
auth.POST("/members/edit", dbHandler.EditMember)
auth.POST("/members/delete", dbHandler.DeleteMember)
auth.POST("/members/import/csv", dbHandler.ImportCSV)
- auth.POST("/settings/update", userManager.UpdateSettings)
- auth.POST("/user/add", userManager.AddUser)
- auth.POST("/login/refresh", userManager.Refresh)
- auth.DELETE("/user", userManager.RemoveUser)
+ auth.POST("/settings/update", userManager.UpdateSettings)
+
+ auth.POST("/roles/add", userManager.AddRole)
+ auth.POST("/roles/update", userManager.UpdateRole)
+ auth.POST("/roles/delete", userManager.DeleteRole)
+
+ auth.POST("/users/add", userManager.AddUser)
+ auth.POST("/users/delete", userManager.DeleteUser)
+
+ auth.POST("/login/refresh", userManager.Refresh)
// Serve static files
s.Routes.StaticFS("/assets", gin.Dir(filepath.Join(*spa, "assets"), true))
@@ -136,10 +154,29 @@ func main() {
go func() {
time.Sleep(500 * time.Millisecond)
- if err := utils.OpenBrowser(fmt.Sprintf("http://localhost:%d", *port), logger); err != nil {
+ if err := utils.OpenBrowser(fmt.Sprintf("%slocalhost:%d", httpString, *port), logger); err != nil {
logger.Error("main", fmt.Sprintf("starting browser error : %s", err.Error()))
}
}()
+
+ if *https {
+ if *sslCert == "" {
+ logger.Error("ssl certificate", "-cert flag not given for https server")
+ log.Fatal("-cert flag not given for https server")
+ }
+ if *sslChain == "" {
+ logger.Error("ssl key", "-chain flag not given for https server")
+ log.Fatal("-chain flag not given for https server")
+ }
+
+ // start https server
+ logger.Info("main", fmt.Sprintf("https listen on ip: %s port: %d", *ip, *port))
+ if err := s.ServeHttps(*ip, *port, cert.Cert{Organization: *organization, CertFile: *sslCert, KeyFile: *sslChain}); err != nil {
+ logger.Error("main", "error https server "+err.Error())
+ }
+ return
+ }
+
// start http server
logger.Info("main", fmt.Sprintf("http listen on ip: %s port: %d", *ip, *port))
if err := s.ServeHttp(*ip, *port); err != nil {
diff --git a/backend/members.dba b/backend/members.dba
index 7861e82..eb0c588 100644
Binary files a/backend/members.dba and b/backend/members.dba differ
diff --git a/backend/models/rights.go b/backend/models/rights.go
new file mode 100644
index 0000000..70fecfa
--- /dev/null
+++ b/backend/models/rights.go
@@ -0,0 +1,8 @@
+package models
+
+type Rights struct {
+ Name string `json:"name,omitempty"`
+ Read bool `json:"read,omitempty"`
+ Write bool `json:"write,omitempty"`
+ Delete bool `json:"delete,omitempty"`
+}
diff --git a/backend/models/role.go b/backend/models/role.go
new file mode 100644
index 0000000..7a99145
--- /dev/null
+++ b/backend/models/role.go
@@ -0,0 +1,11 @@
+package models
+
+type Role struct {
+ Id int `json:"id"`
+ Role string `json:"role"`
+ Rights []Rights `json:"rights"`
+}
+
+func (r *Role) IsValid() bool {
+ return r.Role != ""
+}
diff --git a/backend/models/user.go b/backend/models/user.go
index 621e819..54a52cc 100644
--- a/backend/models/user.go
+++ b/backend/models/user.go
@@ -1,7 +1,9 @@
package models
type User struct {
+ Id int `json:"id"`
Name string `json:"user"`
+ Email string `json:"email"`
Role string `json:"role"`
Password string `json:"password,omitempty"`
Settings Settings `json:"settings"`
diff --git a/backend/server/server.go b/backend/server/server.go
index 5595c5e..8c46c3a 100644
--- a/backend/server/server.go
+++ b/backend/server/server.go
@@ -29,10 +29,10 @@ func (s *Server) ServeHttp(ip string, port uint) error {
}
// serve dbm as http
-func (s *Server) ServeHttps(port uint, cert cert.Cert) error {
+func (s *Server) ServeHttps(url string, port uint, cert cert.Cert) error {
// generate self signed tls certificate
if err := cert.GenerateSelfSignedCert(); err != nil {
return err
}
- return s.Routes.RunTLS(fmt.Sprintf(":%d", port), cert.CertFile, cert.KeyFile)
+ return s.Routes.RunTLS(fmt.Sprintf("%s:%d", url, port), cert.CertFile, cert.KeyFile)
}
diff --git a/backend/undefined b/backend/undefined
deleted file mode 100644
index d2d7528..0000000
Binary files a/backend/undefined and /dev/null differ
diff --git a/backend/user/login.go b/backend/user/login.go
index 60befa5..99f096a 100644
--- a/backend/user/login.go
+++ b/backend/user/login.go
@@ -4,9 +4,7 @@ import (
"backend/dbRequest"
"backend/models"
"backend/utils"
- "database/sql"
"encoding/json"
- "fmt"
"io"
"net/http"
"time"
@@ -21,101 +19,11 @@ var DOMAIN = "localhost"
var ACCESS_TOKEN_TIME = 15 * time.Minute
var REFRESH_TOKEN_TIME = 72 * time.Hour
-func (um *UserManager) AddUser(c *gin.Context) {
- body, err := io.ReadAll(c.Request.Body)
- if err != nil {
- c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
- return
- }
-
- user := models.User{}
- err = json.Unmarshal(body, &user)
- if err != nil {
- c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
- return
- }
-
- if !user.IsValid() {
- c.JSON(http.StatusBadRequest, models.NewJsonErrorMessageResponse("user empty"))
- return
- }
-
- db, err := sql.Open(um.dbType, um.dbFile)
- if dbRequest.CheckDBError(c, user.Name, err) {
- return
- }
- defer db.Close()
-
- var exists bool
-
- if err := db.QueryRow(dbRequest.DBUserLookup, user.Name).Scan(&exists); dbRequest.CheckDBError(c, user.Name, err) {
- return
- }
-
- if exists {
- 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, models.NewJsonErrorResponse(err))
- return
- }
- if _, err := db.Exec(dbRequest.DBNewUser, user.Role, user.Name, hash, "{}"); dbRequest.CheckDBError(c, user.Name, err) {
- return
- }
-
- c.JSON(http.StatusOK, gin.H{
- "message": fmt.Sprintf("user '%s' successfully added", user.Name),
- })
-}
-
-func (um *UserManager) RemoveUser(c *gin.Context) {
- body, err := io.ReadAll(c.Request.Body)
- if err != nil {
- c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
- return
- }
-
- user := models.User{}
- err = json.Unmarshal(body, &user)
- if err != nil {
- c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
- return
- }
-
- if !user.IsValid() {
- c.JSON(http.StatusBadRequest, models.NewJsonErrorMessageResponse("user empty"))
- return
- }
-
- db, err := sql.Open(um.dbType, um.dbFile)
- if dbRequest.CheckDBError(c, user.Name, err) {
- return
- }
- defer db.Close()
-
- var storedPassword string
- 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, models.NewJsonErrorMessageResponse("wrong password"))
- return
- }
-
- if _, err := db.Exec(dbRequest.DBRemoveUser, user.Name); dbRequest.CheckDBError(c, user.Name, err) {
- return
- }
-
- c.JSON(http.StatusOK, gin.H{
- "message": fmt.Sprintf("user '%s' successfully removed", user.Name),
- })
-}
-
func (um *UserManager) Login(c *gin.Context) {
+ if !um.databaseOpened(c) {
+ return
+ }
+
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
@@ -134,14 +42,8 @@ func (um *UserManager) Login(c *gin.Context) {
return
}
- db, err := sql.Open(um.dbType, um.dbFile)
- if dbRequest.CheckDBError(c, user.Name, err) {
- return
- }
- defer db.Close()
-
var storedPassword, settingsJsonString string
- if err := db.QueryRow(dbRequest.DBQueryPassword, user.Name).Scan(&user.Role, &storedPassword, &settingsJsonString); dbRequest.CheckDBError(c, user.Name, err) {
+ if err := um.database.QueryRow(dbRequest.DBQueryPassword, user.Name).Scan(&user.Id, &user.Role, &storedPassword, &settingsJsonString); dbRequest.CheckDBError(c, user.Name, err) {
return
}
@@ -162,6 +64,7 @@ func (um *UserManager) Login(c *gin.Context) {
// Create token
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+ "id": user.Id,
"username": user.Name,
"role": user.Role,
"type": "access",
@@ -169,6 +72,7 @@ func (um *UserManager) Login(c *gin.Context) {
})
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+ "id": user.Id,
"username": user.Name,
"role": user.Role,
"type": "refresh",
@@ -194,9 +98,9 @@ func (um *UserManager) Login(c *gin.Context) {
c.SetCookie("refresh_token", refreshString, int(time.Until(refreshTokenExp).Seconds()),
"/", "", secure, true)
- fmt.Println(22, user.Settings)
c.JSON(http.StatusOK, gin.H{
"message": "login successful",
+ "id": user.Id,
"user": user.Name,
"role": user.Role,
"settings": user.Settings,
@@ -225,11 +129,13 @@ func (um *UserManager) Refresh(c *gin.Context) {
}
username := claims["username"].(string)
+ id := claims["id"].(float64)
role := claims["role"].(string)
// new access token
accessExp := time.Now().Add(ACCESS_TOKEN_TIME)
newAccess := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+ "id": id,
"username": username,
"role": role,
"exp": accessExp.Unix(),
@@ -259,6 +165,7 @@ func (um *UserManager) Me(c *gin.Context) {
claims := token.Claims.(jwt.MapClaims)
c.JSON(http.StatusOK, gin.H{
+ "id": claims["id"],
"user": claims["username"],
"role": claims["role"],
})
diff --git a/backend/user/manager.go b/backend/user/manager.go
index e966998..c653d30 100644
--- a/backend/user/manager.go
+++ b/backend/user/manager.go
@@ -2,15 +2,23 @@ package user
import (
"backend/dbRequest"
+ "backend/models"
"backend/utils"
"database/sql"
+ "encoding/json"
+ "errors"
"fmt"
+ "io"
+ "net/http"
"os"
+ "strconv"
+ "strings"
+
+ "github.com/gin-gonic/gin"
)
type UserManager struct {
- dbType string
- dbFile string
+ database *sql.DB
}
func NewUserManager(dir string) (*UserManager, error) {
@@ -18,17 +26,17 @@ func NewUserManager(dir string) (*UserManager, error) {
dir = "."
}
- var typ string = "sqlite"
- var file string = fmt.Sprintf("%s/user.db", dir)
+ var err error
+ var um UserManager
+ file := fmt.Sprintf("%s/user.db", dir)
+
+ um.database, err = sql.Open("sqlite", file)
+ if err != nil {
+ return nil, err
+ }
if _, err := os.Stat(file); err != nil {
- db, err := sql.Open(typ, file)
- if err != nil {
- return nil, err
- }
- defer db.Close()
-
- _, err = db.Exec(dbRequest.DBCreate)
+ _, err = um.database.Exec(dbRequest.CreateUserTable)
if err != nil {
return nil, err
}
@@ -37,13 +45,217 @@ func NewUserManager(dir string) (*UserManager, error) {
if err != nil {
return nil, err
}
- _, err = db.Exec(dbRequest.DBNewUser, "admin", "admin", hash, "{}")
+ _, err = um.database.Exec(dbRequest.NewUser, "admin", "", "admin", hash, `{"databaseName":"members.dba","primaryColor":"#1976d2", "secondaryColor":"#26a69a"}`)
if err != nil {
return nil, err
}
}
- return &UserManager{
- dbType: typ,
- dbFile: file,
- }, nil
+ return &um, nil
+}
+
+func (um *UserManager) databaseOpened(c *gin.Context) bool {
+ if um.database == nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": "no database opened",
+ })
+ return false
+ }
+ return true
+}
+
+func (um *UserManager) AddUser(c *gin.Context) {
+ if !um.databaseOpened(c) {
+ return
+ }
+
+ body, err := io.ReadAll(c.Request.Body)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
+ return
+ }
+
+ user := models.User{}
+ err = json.Unmarshal(body, &user)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
+ return
+ }
+
+ if !user.IsValid() {
+ c.JSON(http.StatusBadRequest, models.NewJsonErrorMessageResponse("user empty"))
+ return
+ }
+
+ var exists bool
+
+ if err := um.database.QueryRow(dbRequest.DBUserLookup, user.Name).Scan(&exists); dbRequest.CheckDBError(c, user.Name, err) {
+ return
+ }
+
+ if exists {
+ 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, models.NewJsonErrorResponse(err))
+ return
+ }
+
+ if !utils.IsValidEmail(user.Email) {
+ c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(errors.New("not valid email address")))
+ return
+ }
+
+ if _, err := um.database.Exec(dbRequest.NewUser, user.Name, user.Email, user.Role, hash, "{}"); dbRequest.CheckDBError(c, user.Name, err) {
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": fmt.Sprintf("user '%s' successfully added", user.Name),
+ })
+}
+
+func (um *UserManager) GetUserById(c *gin.Context) {
+ if !um.databaseOpened(c) {
+ return
+ }
+
+ var i int
+ var err error
+
+ id := c.Query("id")
+ if id != "" {
+ i, err = strconv.Atoi(id)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": err.Error(),
+ })
+ return
+ }
+ }
+
+ query := `SELECT id, username, email, role, settings FROM users`
+ var args any
+ if i > 0 {
+ query = `
+ SELECT id, username, email, role, settings FROM users
+ WHERE id = ?
+ `
+ args = i
+ }
+
+ rows, err := um.database.Query(query, args)
+ if err != nil {
+ return
+ }
+ defer rows.Close()
+
+ var users []models.User
+
+ for rows.Next() {
+ var id int
+ var name, email, role, settingsString string
+ if err = rows.Scan(&id, &name, &email, &role, &settingsString); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": err.Error(),
+ })
+ return
+ }
+
+ var settings models.Settings
+ err := json.Unmarshal([]byte(settingsString), &settings)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": err.Error(),
+ })
+ return
+ }
+
+ users = append(users, models.User{
+ Id: id,
+ Name: name,
+ Email: email,
+ Role: role,
+ Settings: settings,
+ })
+ }
+
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": err.Error(),
+ })
+ return
+ }
+ c.JSON(http.StatusOK, users)
+}
+
+func (um *UserManager) DeleteUser(c *gin.Context) {
+ if !um.databaseOpened(c) {
+ return
+ }
+
+ queryId := c.Query("id")
+
+ if queryId == "" || queryId == "null" || queryId == "undefined" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": "id query missing or wrong value: " + queryId,
+ })
+ return
+ }
+
+ var request struct {
+ Ids []int `json:"ids"`
+ }
+
+ err := c.BindJSON(&request)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": err.Error(),
+ })
+ return
+ }
+
+ if len(request.Ids) == 0 {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": "no ids given to be deleted",
+ })
+ return
+ }
+
+ var ownId string
+ placeholders := make([]string, len(request.Ids))
+ args := make([]any, len(request.Ids))
+ for i, id := range request.Ids {
+ if queryId == fmt.Sprint(id) {
+ ownId = queryId
+ continue
+ }
+
+ placeholders[i] = "?"
+ args[i] = id
+ }
+
+ query := fmt.Sprintf("DELETE FROM users WHERE id IN (%s)", strings.Join(placeholders, ","))
+
+ _, err = um.database.Exec(query, args...)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": err.Error(),
+ })
+ return
+ }
+
+ if ownId != "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": "can not delete logged in member id: " + queryId,
+ "id": queryId,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "member(s) deleted",
+ })
}
diff --git a/backend/user/roles.go b/backend/user/roles.go
new file mode 100644
index 0000000..c677f4d
--- /dev/null
+++ b/backend/user/roles.go
@@ -0,0 +1,245 @@
+package user
+
+import (
+ "backend/dbRequest"
+ "backend/models"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+)
+
+func (um *UserManager) AddRole(c *gin.Context) {
+ if !um.databaseOpened(c) {
+ return
+ }
+
+ body, err := io.ReadAll(c.Request.Body)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
+ return
+ }
+
+ role := models.Role{}
+ err = json.Unmarshal(body, &role)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
+ return
+ }
+
+ if !role.IsValid() {
+ c.JSON(http.StatusBadRequest, models.NewJsonErrorMessageResponse("user empty"))
+ return
+ }
+
+ var exists bool
+
+ if err := um.database.QueryRow(dbRequest.DBRoleLookup, role.Role).Scan(&exists); dbRequest.CheckDBError(c, role.Role, err) {
+ return
+ }
+
+ if exists {
+ c.JSON(http.StatusOK, models.NewJsonErrorMessageResponse(fmt.Sprintf("role '%s' exists already", role.Role)))
+ return
+ }
+
+ jsonBytes, err := json.Marshal(role.Rights)
+ if err != nil {
+ c.JSON(http.StatusOK, models.NewJsonErrorMessageResponse(err.Error()))
+ return
+ }
+
+ if _, err := um.database.Exec(dbRequest.NewRole, role.Role, string(jsonBytes)); dbRequest.CheckDBError(c, role.Role, err) {
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": fmt.Sprintf("role '%s' successfully added", role.Role),
+ })
+}
+
+func (um *UserManager) GetRoleById(c *gin.Context) {
+ if !um.databaseOpened(c) {
+ return
+ }
+
+ if _, err := um.database.Exec(dbRequest.CreateRoleTable); err != nil {
+ c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
+ return
+ }
+
+ var i int
+ var err error
+
+ id := c.Query("id")
+ if id != "" {
+ i, err = strconv.Atoi(id)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": err.Error(),
+ })
+ return
+ }
+ }
+
+ query := `SELECT id, role, rights FROM roles`
+ var args any
+ if i > 0 {
+ query = `
+ SELECT id, role, rights FROM users
+ WHERE id = ?
+ `
+ args = i
+ }
+
+ rows, err := um.database.Query(query, args)
+ if err != nil {
+ return
+ }
+ defer rows.Close()
+
+ var roles []models.Role
+
+ for rows.Next() {
+ var id int
+ var role, rightsString string
+ if err = rows.Scan(&id, &role, &rightsString); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": err.Error(),
+ })
+ return
+ }
+
+ var data struct {
+ Rights []models.Rights `json:"rights"`
+ }
+ err := json.Unmarshal([]byte(rightsString), &data)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": err.Error(),
+ })
+ return
+ }
+
+ roles = append(roles, models.Role{
+ Id: id,
+ Role: role,
+ Rights: data.Rights,
+ })
+ }
+
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": err.Error(),
+ })
+ return
+ }
+ c.JSON(http.StatusOK, roles)
+}
+
+func (um *UserManager) UpdateRole(c *gin.Context) {
+ if !um.databaseOpened(c) {
+ return
+ }
+
+ body, err := io.ReadAll(c.Request.Body)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
+ return
+ }
+
+ role := models.Role{}
+ err = json.Unmarshal(body, &role)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
+ return
+ }
+
+ jsonBytes, err := json.Marshal(role)
+ if err != nil {
+ c.JSON(http.StatusOK, models.NewJsonErrorMessageResponse(err.Error()))
+ return
+ }
+
+ if _, err := um.database.Exec(dbRequest.DBUpdateRole, string(jsonBytes), role.Role); err != nil {
+ c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": fmt.Sprintf("role rights '%s' successfully updated", role.Role),
+ })
+}
+
+func (um *UserManager) DeleteRole(c *gin.Context) {
+ if !um.databaseOpened(c) {
+ return
+ }
+
+ queryRole := c.Query("role")
+
+ if queryRole == "" || queryRole == "null" || queryRole == "undefined" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": "role query missing or wrong value: " + queryRole,
+ })
+ return
+ }
+
+ var request struct {
+ Roles []string `json:"roles"`
+ }
+
+ err := c.BindJSON(&request)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": err.Error(),
+ })
+ return
+ }
+
+ if len(request.Roles) == 0 {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": "no roles given to be deleted",
+ })
+ return
+ }
+
+ var ownRole string
+ placeholders := make([]string, len(request.Roles))
+ args := make([]any, len(request.Roles))
+ for i, role := range request.Roles {
+ if ownRole == role {
+ ownRole = queryRole
+ continue
+ }
+
+ placeholders[i] = "?"
+ args[i] = role
+ }
+
+ query := fmt.Sprintf("DELETE FROM roles WHERE role IN (%s)", strings.Join(placeholders, ","))
+
+ _, err = um.database.Exec(query, args...)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": err.Error(),
+ })
+ return
+ }
+
+ if ownRole != "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "message": "can not delete logged in role id: " + ownRole,
+ "role": ownRole,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "role(s) deleted",
+ })
+}
diff --git a/backend/user/settings.go b/backend/user/settings.go
index 3c7cce1..ee20561 100644
--- a/backend/user/settings.go
+++ b/backend/user/settings.go
@@ -3,7 +3,6 @@ package user
import (
"backend/dbRequest"
"backend/models"
- "database/sql"
"encoding/json"
"fmt"
"io"
@@ -13,6 +12,10 @@ import (
)
func (um *UserManager) UpdateSettings(c *gin.Context) {
+ if !um.databaseOpened(c) {
+ return
+ }
+
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
@@ -26,19 +29,13 @@ func (um *UserManager) UpdateSettings(c *gin.Context) {
return
}
- db, err := sql.Open(um.dbType, um.dbFile)
- if dbRequest.CheckDBError(c, user.Name, err) {
- return
- }
- defer db.Close()
-
jsonBytes, err := json.Marshal(user.Settings)
if err != nil {
c.JSON(http.StatusOK, models.NewJsonErrorMessageResponse(err.Error()))
return
}
- if _, err := db.Exec(dbRequest.DBUpdateSettings, string(jsonBytes), user.Name); err != nil {
+ if _, err := um.database.Exec(dbRequest.DBUpdateSettings, string(jsonBytes), user.Name); err != nil {
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return
}
diff --git a/backend/utils/utils.go b/backend/utils/utils.go
index 8acad74..21370a1 100644
--- a/backend/utils/utils.go
+++ b/backend/utils/utils.go
@@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"path/filepath"
+ "regexp"
"runtime"
"gitea.tecamino.com/paadi/tecamino-logger/logging"
@@ -63,3 +64,8 @@ func FindAllFiles(rootDir, fileExtention string) (files []string, err error) {
})
return
}
+
+func IsValidEmail(email string) bool {
+ re := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
+ return re.MatchString(email)
+}
diff --git a/eslint.config.js b/eslint.config.js
index 20e7947..3830a34 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,9 +1,9 @@
-import js from '@eslint/js'
-import globals from 'globals'
-import pluginVue from 'eslint-plugin-vue'
-import pluginQuasar from '@quasar/app-vite/eslint'
-import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
-import prettierSkipFormatting from '@vue/eslint-config-prettier/skip-formatting'
+import js from '@eslint/js';
+import globals from 'globals';
+import pluginVue from 'eslint-plugin-vue';
+import pluginQuasar from '@quasar/app-vite/eslint';
+import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript';
+import prettierSkipFormatting from '@vue/eslint-config-prettier/skip-formatting';
export default defineConfigWithVueTs(
{
@@ -33,16 +33,13 @@ export default defineConfigWithVueTs(
* pluginVue.configs["flat/recommended"]
* -> Above, plus rules to enforce subjective community defaults to ensure consistency.
*/
- pluginVue.configs[ 'flat/essential' ],
+ pluginVue.configs['flat/essential'],
{
- files: ['**/*.ts', '**/*.vue'],
+ files: ['**/*.ts', '**/*.d.ts', '**/*.vue'],
rules: {
- '@typescript-eslint/consistent-type-imports': [
- 'error',
- { prefer: 'type-imports' }
- ],
- }
+ '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
+ },
},
// https://github.com/vuejs/eslint-config-typescript
vueTsConfigs.recommendedTypeChecked,
@@ -60,8 +57,8 @@ export default defineConfigWithVueTs(
cordova: 'readonly',
Capacitor: 'readonly',
chrome: 'readonly', // BEX related
- browser: 'readonly' // BEX related
- }
+ browser: 'readonly', // BEX related
+ },
},
// add your custom rules here
@@ -69,18 +66,18 @@ export default defineConfigWithVueTs(
'prefer-promise-reject-errors': 'off',
// allow debugger during development only
- 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
- }
+ 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+ },
},
{
- files: [ 'src-pwa/custom-service-worker.ts' ],
+ files: ['src-pwa/custom-service-worker.ts'],
languageOptions: {
globals: {
- ...globals.serviceworker
- }
- }
+ ...globals.serviceworker,
+ },
+ },
},
- prettierSkipFormatting
-)
+ prettierSkipFormatting,
+);
diff --git a/package-lock.json b/package-lock.json
index 2bfc4b2..fb8d593 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "lightcontrol",
- "version": "0.1.0",
+ "version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lightcontrol",
- "version": "0.1.0",
+ "version": "1.0.0",
"hasInstallScript": true,
"dependencies": {
"@capacitor-community/sqlite": "^7.0.1",
@@ -18,7 +18,8 @@
"quasar": "^2.16.0",
"vue": "^3.4.18",
"vue-i18n": "^11.1.12",
- "vue-router": "^4.0.12"
+ "vue-router": "^4.0.12",
+ "vuedraggable": "^4.1.0"
},
"devDependencies": {
"@eslint/js": "^9.14.0",
@@ -7280,6 +7281,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/sortablejs": {
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
+ "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
+ "license": "MIT"
+ },
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -8215,6 +8222,18 @@
"typescript": ">=5.0.0"
}
},
+ "node_modules/vuedraggable": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
+ "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
+ "license": "MIT",
+ "dependencies": {
+ "sortablejs": "1.14.0"
+ },
+ "peerDependencies": {
+ "vue": "^3.0.1"
+ }
+ },
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
diff --git a/package.json b/package.json
index 8d94c2a..7f7a5b9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "lightcontrol",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "A Tecamino App",
"productName": "Member Database",
"author": "A. Zuercher",
@@ -24,7 +24,8 @@
"quasar": "^2.16.0",
"vue": "^3.4.18",
"vue-i18n": "^11.1.12",
- "vue-router": "^4.0.12"
+ "vue-router": "^4.0.12",
+ "vuedraggable": "^4.1.0"
},
"devDependencies": {
"@eslint/js": "^9.14.0",
diff --git a/quasar.config.ts b/quasar.config.ts
index 1fdbf16..e445758 100644
--- a/quasar.config.ts
+++ b/quasar.config.ts
@@ -81,7 +81,7 @@ export default defineConfig((/* ctx */) => {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver
devServer: {
open: true, // opens browser window automatically
- https: false, // or true if you want HTTPS
+ https: true, // or true if you want HTTPS
port: 9000, // your custom port
// host: '0.0.0.0', // allows external access (not just localhost)
// allowedHosts: ['members.tecamino.com'],
diff --git a/src/assets/lang/de-CH.yaml b/src/assets/lang/de-CH.yaml
index 895102b..eff15da 100644
--- a/src/assets/lang/de-CH.yaml
+++ b/src/assets/lang/de-CH.yaml
@@ -1,6 +1,6 @@
language: Sprach
prename: Vorname
-lastname: Nachname
+lastName: Nachname
birthday: Geburtstag
email: Email
group: Gruppe
@@ -46,3 +46,17 @@ resetColors: Farbe zrügsetze
save: Spichere
users: Benutzer
roles: Rollen
+name: Name
+role: Rolle
+addNewUser: Füeg neue Benutzer hinzue
+expires: Ablouf
+selectUserOptions: Wähle Benutzer Optione
+prenameIsRequired: Vorname ist erforderlich
+lastNameIsRequired: Nachname ist erforderlich
+birthdayIsRequired: Geburtstag ist erforderlich
+userIsRequired: Benutzer ist erforderlich
+emailIsRequired: Email ist erforderlich
+roleIsRequired: Rolle ist erforderlich
+rights: Recht
+selectRoleOptions: Wähle Roue Optione
+addNewRole: Füeg neui Roue hinzue
diff --git a/src/assets/lang/de-DE.yaml b/src/assets/lang/de-DE.yaml
index 5b93069..59e2f1e 100644
--- a/src/assets/lang/de-DE.yaml
+++ b/src/assets/lang/de-DE.yaml
@@ -1,6 +1,6 @@
language: Sprache
prename: Vorname
-lastname: Nachname
+lastName: Nachname
birthday: Geburtstag
email: Email
group: Gruppe
@@ -46,3 +46,17 @@ resetColors: Farben zurücksetzen
save: Speichern
users: Benutzer
roles: Rollen
+name: Name
+role: Rolle
+addNewUser: Füge neuen Benutzer hinzu
+expires: Ablauf
+selectUserOptions: Wähle Benutzer Optionen
+prenameIsRequired: Vorname ist erforderlich
+lastNameIsRequired: Nachname ist erforderlich
+birthdayIsRequired: Geburtstag ist erforderlich
+userIsRequired: Benutzer ist erforderlich
+emailIsRequired: Email ist erforderlich
+roleIsRequired: Rolle ist erforderlich
+rights: Rechte
+selectRoleOptions: Wähle Rollen Option
+addNewRole: Füge neue Rolle hinzu
diff --git a/src/assets/lang/en-US.yaml b/src/assets/lang/en-US.yaml
index 42c1181..9dfca59 100644
--- a/src/assets/lang/en-US.yaml
+++ b/src/assets/lang/en-US.yaml
@@ -1,6 +1,6 @@
language: Language
prename: Prename
-lastname: Name
+lastName: Name
birthday: Birthday
email: Email
group: Group
@@ -46,3 +46,17 @@ resetColors: Reset Colors
save: Save
users: Users
roles: Roles
+name: Name
+role: Role
+addNewUser: Add new User
+expires: Expires
+selectUserOptions: Select User Options
+prenameIsRequired: Fist Name is required
+lastNameIsRequired: Last Name is required
+birthdayIsRequired: Birthday is required
+userIsRequired: User is required
+emailIsRequired: Email is required
+roleIsRequired: Role is required
+rights: Rights
+selectRoleOptions: Select Role Options
+addNewRole: Add new Role
diff --git a/src/boot/auth.ts b/src/boot/auth.ts
index 6891582..e864967 100644
--- a/src/boot/auth.ts
+++ b/src/boot/auth.ts
@@ -14,7 +14,7 @@ export default boot(async ({ app }) => {
await appApi
.get('/login/me')
.then((resp) => {
- useStore.setUser({ username: resp.data.username, role: resp.data.role });
+ useStore.setUser({ id: resp.data.id, username: resp.data.username, role: resp.data.role });
login.refresh().catch((err) => console.error(err));
})
.catch(() => {
diff --git a/src/boot/axios.ts b/src/boot/axios.ts
index 063fa5d..fe58472 100644
--- a/src/boot/axios.ts
+++ b/src/boot/axios.ts
@@ -8,7 +8,7 @@ export const portApp = 9500;
// Create axios instance
export const appApi: AxiosInstance = axios.create({
- baseURL: `http://${host}:${portApp}/api`,
+ baseURL: `https://${host}:${portApp}/api`,
timeout: 10000,
withCredentials: true,
});
diff --git a/src/boot/quasar-global.ts b/src/boot/quasar-global.ts
index b4e574f..249af1f 100644
--- a/src/boot/quasar-global.ts
+++ b/src/boot/quasar-global.ts
@@ -9,11 +9,17 @@ export default boot(({ app, router }) => {
const $q = app.config.globalProperties.$q;
setQuasarInstance($q);
- Logo.value = localStorage.getItem('icon') ?? '';
- databaseName.value = localStorage.getItem('databaseName') ?? '';
- document.documentElement.style.setProperty('--q-primary', localStorage.getItem('primaryColor'));
- document.documentElement.style.setProperty(
- '--q-secondary',
- localStorage.getItem('secondaryColor'),
- );
+ Logo.value = localStorage.getItem('icon') ?? Logo.value;
+ databaseName.value = localStorage.getItem('databaseName') ?? databaseName.value;
+ let primaryColor = localStorage.getItem('primaryColor');
+ if (primaryColor == null || primaryColor === 'undefined' || primaryColor.trim() === '') {
+ primaryColor = null;
+ }
+ let secondaryColor = localStorage.getItem('secondaryColor');
+ if (secondaryColor == null || secondaryColor === 'undefined' || secondaryColor.trim() === '') {
+ secondaryColor = null;
+ }
+
+ document.documentElement.style.setProperty('--q-primary', primaryColor ?? '#1976d2');
+ document.documentElement.style.setProperty('--q-secondary', secondaryColor ?? '#26a69a');
});
diff --git a/src/components/EditAllDialog.vue b/src/components/EditAllDialog.vue
deleted file mode 100644
index 27bb67c..0000000
--- a/src/components/EditAllDialog.vue
+++ /dev/null
@@ -1,139 +0,0 @@
-
-