Compare commits

5 Commits

Author SHA1 Message Date
fdc8525abe backend/main.go aktualisiert
fix wrong order and description of ssl certificate
2025-10-14 21:29:42 +02:00
8075d1fe1f src/components/UploadDialog.vue aktualisiert 2025-10-14 21:23:31 +02:00
8709989fbe src/components/UploadDialog.vue aktualisiert
Fix absolute url to relative url
2025-10-14 21:22:16 +02:00
Adrian Zürcher
b0d6bb5512 fix binary name
All checks were successful
Build Quasar SPA and Go Backend for memberApp / build-spa (push) Successful in 2m18s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, .exe, windows) (push) Successful in 5m15s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, , linux) (push) Successful in 5m18s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm64, , linux) (push) Successful in 5m18s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm, 6, , linux) (push) Successful in 5m20s
2025-10-14 16:52:04 +02:00
Adrian Zürcher
690b7f4034 add new user and role table in app (in progress)
All checks were successful
Build Quasar SPA and Go Backend for memberApp / build-spa (push) Successful in 2m12s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, , linux) (push) Successful in 5m8s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, .exe, windows) (push) Successful in 5m8s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm, 6, , linux) (push) Successful in 4m57s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm64, , linux) (push) Successful in 5m7s
2025-10-14 16:41:20 +02:00
45 changed files with 1428 additions and 630 deletions

View File

@@ -104,7 +104,7 @@ jobs:
export GOMODCACHE=/data/gomodcache export GOMODCACHE=/data/gomodcache
mkdir -p $GOCACHE $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 if [ "${{ matrix.arch }}" = "arm" ]; then
GOARM=${{ matrix.arm_version }} GOARM=${{ matrix.arm_version }}
fi fi
@@ -117,4 +117,4 @@ jobs:
name: memberApp-${{ matrix.goos }}-${{ matrix.arch }} name: memberApp-${{ matrix.goos }}-${{ matrix.arch }}
path: | path: |
./dist/spa ./dist/spa
server-${{ matrix.goos }}-${{ matrix.arch }}${{ matrix.ext }} memberApp-${{ matrix.goos }}-${{ matrix.arch }}${{ matrix.ext }}

1
.gitignore vendored
View File

@@ -42,3 +42,4 @@ yarn-error.log*
# golang quasar websever executable # golang quasar websever executable
backend/server-linux-arm64 backend/server-linux-arm64
backend/cert

View File

@@ -1 +0,0 @@
["http://localhost:9000", "http://localhost:9500", "http://127.0.0.1:9500"]

View File

@@ -8,19 +8,29 @@ import (
"github.com/gin-gonic/gin" "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, id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL, username TEXT NOT NULL,
email TEXT NOT NULL,
role TEXT NOT NULL, role TEXT NOT NULL,
password TEXT NOT NULL, password TEXT NOT NULL,
settings TEXT NOT NULL settings TEXT NOT NULL
);` );`
var DBNewUser string = `INSERT INTO users (username, role, password, settings) VALUES (?, ?, ?, ?)` var CreateRoleTable string = `CREATE TABLE IF NOT EXISTS roles (
var DBQueryPassword string = `SELECT role, password, settings FROM users WHERE username = ?` 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 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 DBRemoveUser string = `DELETE FROM users WHERE username = $1`
var DBUpdateSettings string = `UPDATE users SET settings = ? WHERE username = ?` 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 { func CheckDBError(c *gin.Context, username string, err error) bool {
if err != nil { if err != nil {

View File

@@ -14,6 +14,8 @@ import (
"strings" "strings"
"time" "time"
"gitea.tecamino.com/paadi/tecamino-dbm/cert"
dbApi "gitea.tecamino.com/paadi/memberDB/api" dbApi "gitea.tecamino.com/paadi/memberDB/api"
"gitea.tecamino.com/paadi/tecamino-logger/logging" "gitea.tecamino.com/paadi/tecamino-logger/logging"
"github.com/gin-contrib/cors" "github.com/gin-contrib/cors"
@@ -28,13 +30,16 @@ func main() {
spa := flag.String("spa", "./dist/spa", "quasar spa files") spa := flag.String("spa", "./dist/spa", "quasar spa files")
workingDir := flag.String("workingDirectory", ".", "quasar spa files") workingDir := flag.String("workingDirectory", ".", "quasar spa files")
ip := flag.String("ip", "0.0.0.0", "server listening ip") 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") port := flag.Uint("port", 9500, "server listening port")
https := flag.Bool("https", false, "serves as https needs flag -cert and -chain")
sslKey := flag.String("privkey", "", "ssl private key path")
sslFullchain := flag.String("fullchain", "", "ssl fullchain path")
debug := flag.Bool("debug", false, "log debug") debug := flag.Bool("debug", false, "log debug")
flag.Parse() flag.Parse()
//change working directory only if value is given //change working directory only if value is given
if *workingDir != "." && *workingDir != "" { if *workingDir != "." && *workingDir != "" {
fmt.Println(1, *workingDir)
os.Chdir(*workingDir) os.Chdir(*workingDir)
} }
@@ -72,13 +77,18 @@ func main() {
dbHandler := dbApi.NewAPIHandler() dbHandler := dbApi.NewAPIHandler()
//get local ip //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() localIP, err := utils.GetLocalIP()
if err != nil { if err != nil {
logger.Error("main", fmt.Sprintf("get local ip : %s", err.Error())) logger.Error("main", fmt.Sprintf("get local ip : %s", err.Error()))
} else { } 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{ s.Routes.Use(cors.New(cors.Config{
@@ -102,18 +112,26 @@ func main() {
//private //private
auth := api.Group("/secure", user.AuthMiddleware()) auth := api.Group("/secure", user.AuthMiddleware())
auth.GET("/users", userManager.GetUserById)
auth.GET("/members", dbHandler.GetMemberById) auth.GET("/members", dbHandler.GetMemberById)
auth.GET("/roles", userManager.GetRoleById)
auth.POST("database/open", dbHandler.OpenDatabase) auth.POST("database/open", dbHandler.OpenDatabase)
auth.POST("/members/add", dbHandler.AddNewMember) auth.POST("/members/add", dbHandler.AddNewMember)
auth.POST("/members/edit", dbHandler.EditMember) auth.POST("/members/edit", dbHandler.EditMember)
auth.POST("/members/delete", dbHandler.DeleteMember) auth.POST("/members/delete", dbHandler.DeleteMember)
auth.POST("/members/import/csv", dbHandler.ImportCSV) 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 // Serve static files
s.Routes.StaticFS("/assets", gin.Dir(filepath.Join(*spa, "assets"), true)) s.Routes.StaticFS("/assets", gin.Dir(filepath.Join(*spa, "assets"), true))
@@ -136,10 +154,29 @@ func main() {
go func() { go func() {
time.Sleep(500 * time.Millisecond) 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())) 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: *sslFullchain, KeyFile: *sslKey}); err != nil {
logger.Error("main", "error https server "+err.Error())
}
return
}
// start http server // start http server
logger.Info("main", fmt.Sprintf("http listen on ip: %s port: %d", *ip, *port)) logger.Info("main", fmt.Sprintf("http listen on ip: %s port: %d", *ip, *port))
if err := s.ServeHttp(*ip, *port); err != nil { if err := s.ServeHttp(*ip, *port); err != nil {

Binary file not shown.

8
backend/models/rights.go Normal file
View File

@@ -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"`
}

11
backend/models/role.go Normal file
View File

@@ -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 != ""
}

View File

@@ -1,7 +1,9 @@
package models package models
type User struct { type User struct {
Id int `json:"id"`
Name string `json:"user"` Name string `json:"user"`
Email string `json:"email"`
Role string `json:"role"` Role string `json:"role"`
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
Settings Settings `json:"settings"` Settings Settings `json:"settings"`

View File

@@ -29,10 +29,10 @@ func (s *Server) ServeHttp(ip string, port uint) error {
} }
// serve dbm as http // 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 // generate self signed tls certificate
if err := cert.GenerateSelfSignedCert(); err != nil { if err := cert.GenerateSelfSignedCert(); err != nil {
return err 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)
} }

Binary file not shown.

View File

@@ -4,9 +4,7 @@ import (
"backend/dbRequest" "backend/dbRequest"
"backend/models" "backend/models"
"backend/utils" "backend/utils"
"database/sql"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"time" "time"
@@ -21,101 +19,11 @@ var DOMAIN = "localhost"
var ACCESS_TOKEN_TIME = 15 * time.Minute var ACCESS_TOKEN_TIME = 15 * time.Minute
var REFRESH_TOKEN_TIME = 72 * time.Hour 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) { func (um *UserManager) Login(c *gin.Context) {
if !um.databaseOpened(c) {
return
}
body, err := io.ReadAll(c.Request.Body) body, err := io.ReadAll(c.Request.Body)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err)) c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
@@ -134,14 +42,8 @@ func (um *UserManager) Login(c *gin.Context) {
return return
} }
db, err := sql.Open(um.dbType, um.dbFile)
if dbRequest.CheckDBError(c, user.Name, err) {
return
}
defer db.Close()
var storedPassword, settingsJsonString string 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 return
} }
@@ -162,6 +64,7 @@ func (um *UserManager) Login(c *gin.Context) {
// Create token // Create token
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": user.Id,
"username": user.Name, "username": user.Name,
"role": user.Role, "role": user.Role,
"type": "access", "type": "access",
@@ -169,6 +72,7 @@ func (um *UserManager) Login(c *gin.Context) {
}) })
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": user.Id,
"username": user.Name, "username": user.Name,
"role": user.Role, "role": user.Role,
"type": "refresh", "type": "refresh",
@@ -194,9 +98,9 @@ func (um *UserManager) Login(c *gin.Context) {
c.SetCookie("refresh_token", refreshString, int(time.Until(refreshTokenExp).Seconds()), c.SetCookie("refresh_token", refreshString, int(time.Until(refreshTokenExp).Seconds()),
"/", "", secure, true) "/", "", secure, true)
fmt.Println(22, user.Settings)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"message": "login successful", "message": "login successful",
"id": user.Id,
"user": user.Name, "user": user.Name,
"role": user.Role, "role": user.Role,
"settings": user.Settings, "settings": user.Settings,
@@ -225,11 +129,13 @@ func (um *UserManager) Refresh(c *gin.Context) {
} }
username := claims["username"].(string) username := claims["username"].(string)
id := claims["id"].(float64)
role := claims["role"].(string) role := claims["role"].(string)
// new access token // new access token
accessExp := time.Now().Add(ACCESS_TOKEN_TIME) accessExp := time.Now().Add(ACCESS_TOKEN_TIME)
newAccess := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ newAccess := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": id,
"username": username, "username": username,
"role": role, "role": role,
"exp": accessExp.Unix(), "exp": accessExp.Unix(),
@@ -259,6 +165,7 @@ func (um *UserManager) Me(c *gin.Context) {
claims := token.Claims.(jwt.MapClaims) claims := token.Claims.(jwt.MapClaims)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"id": claims["id"],
"user": claims["username"], "user": claims["username"],
"role": claims["role"], "role": claims["role"],
}) })

View File

@@ -2,15 +2,23 @@ package user
import ( import (
"backend/dbRequest" "backend/dbRequest"
"backend/models"
"backend/utils" "backend/utils"
"database/sql" "database/sql"
"encoding/json"
"errors"
"fmt" "fmt"
"io"
"net/http"
"os" "os"
"strconv"
"strings"
"github.com/gin-gonic/gin"
) )
type UserManager struct { type UserManager struct {
dbType string database *sql.DB
dbFile string
} }
func NewUserManager(dir string) (*UserManager, error) { func NewUserManager(dir string) (*UserManager, error) {
@@ -18,17 +26,17 @@ func NewUserManager(dir string) (*UserManager, error) {
dir = "." dir = "."
} }
var typ string = "sqlite" var err error
var file string = fmt.Sprintf("%s/user.db", dir) var um UserManager
file := fmt.Sprintf("%s/user.db", dir)
if _, err := os.Stat(file); err != nil { um.database, err = sql.Open("sqlite", file)
db, err := sql.Open(typ, file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer db.Close()
_, err = db.Exec(dbRequest.DBCreate) if _, err := os.Stat(file); err != nil {
_, err = um.database.Exec(dbRequest.CreateUserTable)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -37,13 +45,217 @@ func NewUserManager(dir string) (*UserManager, error) {
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
} }
return &UserManager{ return &um, nil
dbType: typ, }
dbFile: file,
}, 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",
})
} }

245
backend/user/roles.go Normal file
View File

@@ -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",
})
}

View File

@@ -3,7 +3,6 @@ package user
import ( import (
"backend/dbRequest" "backend/dbRequest"
"backend/models" "backend/models"
"database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -13,6 +12,10 @@ import (
) )
func (um *UserManager) UpdateSettings(c *gin.Context) { func (um *UserManager) UpdateSettings(c *gin.Context) {
if !um.databaseOpened(c) {
return
}
body, err := io.ReadAll(c.Request.Body) body, err := io.ReadAll(c.Request.Body)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err)) c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
@@ -26,19 +29,13 @@ func (um *UserManager) UpdateSettings(c *gin.Context) {
return 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) jsonBytes, err := json.Marshal(user.Settings)
if err != nil { if err != nil {
c.JSON(http.StatusOK, models.NewJsonErrorMessageResponse(err.Error())) c.JSON(http.StatusOK, models.NewJsonErrorMessageResponse(err.Error()))
return 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)) c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err))
return return
} }

View File

@@ -6,6 +6,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp"
"runtime" "runtime"
"gitea.tecamino.com/paadi/tecamino-logger/logging" "gitea.tecamino.com/paadi/tecamino-logger/logging"
@@ -63,3 +64,8 @@ func FindAllFiles(rootDir, fileExtention string) (files []string, err error) {
}) })
return 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)
}

View File

@@ -1,9 +1,9 @@
import js from '@eslint/js' import js from '@eslint/js';
import globals from 'globals' import globals from 'globals';
import pluginVue from 'eslint-plugin-vue' import pluginVue from 'eslint-plugin-vue';
import pluginQuasar from '@quasar/app-vite/eslint' import pluginQuasar from '@quasar/app-vite/eslint';
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript';
import prettierSkipFormatting from '@vue/eslint-config-prettier/skip-formatting' import prettierSkipFormatting from '@vue/eslint-config-prettier/skip-formatting';
export default defineConfigWithVueTs( export default defineConfigWithVueTs(
{ {
@@ -33,16 +33,13 @@ export default defineConfigWithVueTs(
* pluginVue.configs["flat/recommended"] * pluginVue.configs["flat/recommended"]
* -> Above, plus rules to enforce subjective community defaults to ensure consistency. * -> 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: { rules: {
'@typescript-eslint/consistent-type-imports': [ '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
'error', },
{ prefer: 'type-imports' }
],
}
}, },
// https://github.com/vuejs/eslint-config-typescript // https://github.com/vuejs/eslint-config-typescript
vueTsConfigs.recommendedTypeChecked, vueTsConfigs.recommendedTypeChecked,
@@ -60,8 +57,8 @@ export default defineConfigWithVueTs(
cordova: 'readonly', cordova: 'readonly',
Capacitor: 'readonly', Capacitor: 'readonly',
chrome: 'readonly', // BEX related chrome: 'readonly', // BEX related
browser: 'readonly' // BEX related browser: 'readonly', // BEX related
} },
}, },
// add your custom rules here // add your custom rules here
@@ -69,18 +66,18 @@ export default defineConfigWithVueTs(
'prefer-promise-reject-errors': 'off', 'prefer-promise-reject-errors': 'off',
// allow debugger during development only // 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: { languageOptions: {
globals: { globals: {
...globals.serviceworker ...globals.serviceworker,
} },
} },
}, },
prettierSkipFormatting prettierSkipFormatting,
) );

25
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "lightcontrol", "name": "lightcontrol",
"version": "0.1.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "lightcontrol", "name": "lightcontrol",
"version": "0.1.0", "version": "1.0.0",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@capacitor-community/sqlite": "^7.0.1", "@capacitor-community/sqlite": "^7.0.1",
@@ -18,7 +18,8 @@
"quasar": "^2.16.0", "quasar": "^2.16.0",
"vue": "^3.4.18", "vue": "^3.4.18",
"vue-i18n": "^11.1.12", "vue-i18n": "^11.1.12",
"vue-router": "^4.0.12" "vue-router": "^4.0.12",
"vuedraggable": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.14.0", "@eslint/js": "^9.14.0",
@@ -7280,6 +7281,12 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -8215,6 +8222,18 @@
"typescript": ">=5.0.0" "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": { "node_modules/wcwidth": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "lightcontrol", "name": "lightcontrol",
"version": "1.0.0", "version": "1.0.1",
"description": "A Tecamino App", "description": "A Tecamino App",
"productName": "Member Database", "productName": "Member Database",
"author": "A. Zuercher", "author": "A. Zuercher",
@@ -24,7 +24,8 @@
"quasar": "^2.16.0", "quasar": "^2.16.0",
"vue": "^3.4.18", "vue": "^3.4.18",
"vue-i18n": "^11.1.12", "vue-i18n": "^11.1.12",
"vue-router": "^4.0.12" "vue-router": "^4.0.12",
"vuedraggable": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.14.0", "@eslint/js": "^9.14.0",

View File

@@ -81,7 +81,7 @@ export default defineConfig((/* ctx */) => {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver
devServer: { devServer: {
open: true, // opens browser window automatically 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 port: 9000, // your custom port
// host: '0.0.0.0', // allows external access (not just localhost) // host: '0.0.0.0', // allows external access (not just localhost)
// allowedHosts: ['members.tecamino.com'], // allowedHosts: ['members.tecamino.com'],

View File

@@ -1,6 +1,6 @@
language: Sprach language: Sprach
prename: Vorname prename: Vorname
lastname: Nachname lastName: Nachname
birthday: Geburtstag birthday: Geburtstag
email: Email email: Email
group: Gruppe group: Gruppe
@@ -46,3 +46,17 @@ resetColors: Farbe zrügsetze
save: Spichere save: Spichere
users: Benutzer users: Benutzer
roles: Rollen 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

View File

@@ -1,6 +1,6 @@
language: Sprache language: Sprache
prename: Vorname prename: Vorname
lastname: Nachname lastName: Nachname
birthday: Geburtstag birthday: Geburtstag
email: Email email: Email
group: Gruppe group: Gruppe
@@ -46,3 +46,17 @@ resetColors: Farben zurücksetzen
save: Speichern save: Speichern
users: Benutzer users: Benutzer
roles: Rollen 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

View File

@@ -1,6 +1,6 @@
language: Language language: Language
prename: Prename prename: Prename
lastname: Name lastName: Name
birthday: Birthday birthday: Birthday
email: Email email: Email
group: Group group: Group
@@ -46,3 +46,17 @@ resetColors: Reset Colors
save: Save save: Save
users: Users users: Users
roles: Roles 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

View File

@@ -14,7 +14,7 @@ export default boot(async ({ app }) => {
await appApi await appApi
.get('/login/me') .get('/login/me')
.then((resp) => { .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)); login.refresh().catch((err) => console.error(err));
}) })
.catch(() => { .catch(() => {

View File

@@ -8,7 +8,7 @@ export const portApp = 9500;
// Create axios instance // Create axios instance
export const appApi: AxiosInstance = axios.create({ export const appApi: AxiosInstance = axios.create({
baseURL: `http://${host}:${portApp}/api`, baseURL: `https://${host}:${portApp}/api`,
timeout: 10000, timeout: 10000,
withCredentials: true, withCredentials: true,
}); });

View File

@@ -9,11 +9,17 @@ export default boot(({ app, router }) => {
const $q = app.config.globalProperties.$q; const $q = app.config.globalProperties.$q;
setQuasarInstance($q); setQuasarInstance($q);
Logo.value = localStorage.getItem('icon') ?? ''; Logo.value = localStorage.getItem('icon') ?? Logo.value;
databaseName.value = localStorage.getItem('databaseName') ?? ''; databaseName.value = localStorage.getItem('databaseName') ?? databaseName.value;
document.documentElement.style.setProperty('--q-primary', localStorage.getItem('primaryColor')); let primaryColor = localStorage.getItem('primaryColor');
document.documentElement.style.setProperty( if (primaryColor == null || primaryColor === 'undefined' || primaryColor.trim() === '') {
'--q-secondary', primaryColor = null;
localStorage.getItem('secondaryColor'), }
); 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');
}); });

View File

@@ -1,139 +0,0 @@
<template>
<DialogFrame
ref="dialog"
:header-title="
newMember ? 'Add new Member' : 'Edit ' + localMember.firstName + ' ' + localMember.lastName
"
:height="600"
:width="500"
>
<div class="row justify-center q-gutter-md">
<q-input
class="q-ml-md col-5"
label="First Name"
filled
v-model="localMember.firstName"
autofocus
></q-input>
<q-input
class="q-ml-md col-5"
label="Last Name"
filled
v-model="localMember.lastName"
></q-input>
<q-input
class="q-ml-md col-5"
label="Birthday"
filled
v-model="localMember.birthday"
></q-input>
<q-input class="q-ml-md col-5" label="Address" filled v-model="localMember.address"></q-input>
<q-input class="q-ml-md col-5" label="Town" filled v-model="localMember.town"></q-input>
<q-input class="q-ml-md col-5" label="Zip Code" filled v-model="localMember.zip"></q-input>
<q-input class="q-ml-md col-5" label="Phone" filled v-model="localMember.phone"></q-input>
<q-input class="q-ml-md col-5" label="E-Mail" filled v-model="localMember.email"></q-input>
<q-input class="q-ml-md col-5" label="Group" filled v-model="localMember.group"></q-input>
<q-input
class="q-ml-md col-5"
label="Responsible"
filled
v-model="localMember.responsiblePerson"
></q-input>
<q-input
v-if="!newMember"
class="q-ml-md col-11"
label="First Visit"
filled
v-model="localMember.firstVisit"
></q-input>
<q-input
v-if="!newMember"
class="q-ml-md col-11"
label="Last Visit"
filled
v-model="localMember.lastVisit"
></q-input>
</div>
<div class="row justify-center">
<q-btn class="q-ma-md" color="primary" no-caps @click="save">Save</q-btn>
</div>
</DialogFrame>
</template>
<script setup lang="ts">
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
import { ref } from 'vue';
import { appApi } from 'src/boot/axios';
import type { Member } from 'src/vueLib/models/member';
import { useNotify } from 'src/vueLib/general/useNotify';
const { NotifyResponse } = useNotify();
const dialog = ref();
const newMember = ref(false);
const localMember = ref<Member>({
id: 0,
firstName: '',
lastName: '',
birthday: '',
age: '',
address: '',
town: '',
zip: '',
phone: '',
email: '',
group: '',
responsiblePerson: '',
firstVisit: '',
lastVisit: '',
});
const emit = defineEmits(['update-member']);
function open(member: Member | null) {
if (member === undefined) {
return;
}
if (member !== null) {
localMember.value = member;
newMember.value = false;
} else {
localMember.value = {
id: 0,
firstName: '',
lastName: '',
birthday: '',
age: '',
address: '',
town: '',
zip: '',
phone: '',
email: '',
group: '',
responsiblePerson: '',
firstVisit: '',
lastVisit: '',
};
newMember.value = true;
}
dialog.value?.open();
}
function save() {
let query = 'secure/members/edit?id=' + localMember.value.id;
if (newMember.value) {
query = 'ecure/members/add';
}
appApi
.post(query, JSON.stringify(localMember.value))
.then(() => {
emit('update-member', '');
dialog.value.close();
})
.catch((err) => NotifyResponse(err, 'error'));
}
defineExpose({ open });
</script>

View File

@@ -27,9 +27,9 @@ const dialog = ref();
const localMember = ref(); const localMember = ref();
const localTitle = ref(''); const localTitle = ref('');
const localField = ref(''); const localField = ref('');
const value = ref(); const value = ref('');
const emit = defineEmits(['update-member']); const emit = defineEmits(['update']);
const { NotifyResponse } = useNotify(); const { NotifyResponse } = useNotify();
function open(label: string, field: string, member: Member) { function open(label: string, field: string, member: Member) {
@@ -37,44 +37,7 @@ function open(label: string, field: string, member: Member) {
localField.value = field; localField.value = field;
localMember.value = member; localMember.value = member;
switch (field) { value.value = localMember.value[field];
case 'firstName':
value.value = member.firstName;
break;
case 'lastName':
value.value = member.lastName;
break;
case 'birthday':
value.value = member.birthday;
break;
case 'address':
value.value = member.address;
break;
case 'town':
value.value = member.town;
break;
case 'zip':
value.value = member.zip;
break;
case 'phone':
value.value = member.phone;
break;
case 'email':
value.value = member.email;
break;
case 'group':
value.value = member.group;
break;
case 'responsiblePerson':
value.value = member.responsiblePerson;
break;
case 'firstVisit':
value.value = member.firstVisit;
break;
case 'lastVisit':
value.value = member.lastVisit;
break;
}
dialog.value?.open(); dialog.value?.open();
} }
@@ -82,120 +45,18 @@ function save() {
const query = 'secure/members/edit?id=' + localMember.value.id; const query = 'secure/members/edit?id=' + localMember.value.id;
let payload = {}; let payload = {};
switch (localField.value) { if (value.value === localMember.value[localField.value]) {
case 'firstName':
if (value.value === localMember.value.firstName) {
dialog.value.close(); dialog.value.close();
return; return;
} }
payload = { payload = {
firstName: value.value, [localField.value]: value.value,
}; };
break;
case 'lastName':
if (value.value === localMember.value.lastName) {
dialog.value.close();
return;
}
payload = {
lastName: value.value,
};
break;
case 'birthday':
if (value.value === localMember.value.birthday) {
dialog.value.close();
return;
}
payload = {
birthday: value.value,
};
break;
case 'address':
if (value.value === localMember.value.address) {
dialog.value.close();
return;
}
payload = {
address: value.value,
};
break;
case 'town':
if (value.value === localMember.value.town) {
dialog.value.close();
return;
}
payload = {
town: value.value,
};
break;
case 'zip':
if (value.value === localMember.value.zip) {
dialog.value.close();
return;
}
payload = {
zip: value.value,
};
break;
case 'phone':
if (value.value === localMember.value.phone) {
dialog.value.close();
return;
}
payload = {
phone: value.value,
};
break;
case 'email':
if (value.value === localMember.value.email) {
dialog.value.close();
return;
}
payload = {
email: value.value,
};
break;
case 'group':
if (value.value === localMember.value.group) {
dialog.value.close();
return;
}
payload = {
group: value.value,
};
break;
case 'responsiblePerson':
if (value.value === localMember.value.responsiblePerson) {
dialog.value.close();
return;
}
payload = {
responsiblePerson: value.value,
};
break;
case 'firstVisit':
if (value.value === localMember.value.firstVisit) {
dialog.value.close();
return;
}
payload = {
firstVisit: value.value,
};
break;
case 'lastVisit':
if (value.value === localMember.value.lastVisit) {
dialog.value.close();
return;
}
payload = {
lastVisit: value.value,
};
break;
}
appApi appApi
.post(query, payload) .post(query, payload)
.then(() => { .then(() => {
emit('update-member', ''); emit('update', '');
dialog.value.close(); dialog.value.close();
}) })
.catch((err) => { .catch((err) => {

View File

@@ -0,0 +1,186 @@
<template>
<DialogFrame
ref="dialog"
:header-title="
newMember ? $t('addNewMember') : 'Edit ' + localMember.firstName + ' ' + localMember.lastName
"
:height="600"
:width="500"
>
<q-form ref="form">
<div class="row justify-center q-gutter-md">
<q-input
class="q-ml-md col-5 required"
:label="$t('prename')"
filled
:rules="[(val) => !!val || $t('prenameIsRequired')]"
v-model="localMember.firstName"
autofocus
></q-input>
<q-input
class="q-ml-md col-5 required"
:label="$t('lastName')"
filled
:rules="[(val) => !!val || $t('lastNameIsRequired')]"
v-model="localMember.lastName"
></q-input>
<q-input
class="q-ml-md col-5 required"
:label="$t('birthday')"
:rules="[(val) => !!val || $t('birthdayIsRequired')]"
filled
v-model="localMember.birthday"
></q-input>
<q-input
class="q-ml-md col-5"
:label="$t('address')"
filled
v-model="localMember.address"
></q-input>
<q-input
class="q-ml-md col-5"
:label="$t('town')"
filled
v-model="localMember.town"
></q-input>
<q-input
class="q-ml-md col-5"
:label="$t('zipCode')"
filled
v-model="localMember.zip"
></q-input>
<q-input
class="q-ml-md col-5"
:label="$t('phone')"
filled
v-model="localMember.phone"
></q-input>
<q-input
class="q-ml-md col-5"
:label="$t('email')"
filled
v-model="localMember.email"
></q-input>
<q-input
class="q-ml-md col-5"
:label="$t('group')"
filled
v-model="localMember.group"
></q-input>
<q-input
class="q-ml-md col-5"
:label="$t('responsible')"
filled
v-model="localMember.responsiblePerson"
></q-input>
<q-input
v-if="!newMember"
class="q-ml-md col-11"
:label="$t('firstVisit')"
filled
v-model="localMember.firstVisit"
></q-input>
<q-input
v-if="!newMember"
class="q-ml-md col-11"
:label="$t('lastVisit')"
filled
v-model="localMember.lastVisit"
></q-input>
</div>
</q-form>
<div class="row justify-center">
<q-btn class="q-ma-md" color="primary" no-caps @click="save">Save</q-btn>
</div>
</DialogFrame>
</template>
<script setup lang="ts">
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
import { ref } from 'vue';
import { appApi } from 'src/boot/axios';
import type { Member } from 'src/vueLib/models/member';
import { useNotify } from 'src/vueLib/general/useNotify';
const { NotifyResponse } = useNotify();
const dialog = ref();
const form = ref();
const newMember = ref(false);
const localMember = ref<Member>({
id: 0,
firstName: '',
lastName: '',
birthday: '',
age: '',
address: '',
town: '',
zip: '',
phone: '',
email: '',
group: '',
responsiblePerson: '',
firstVisit: '',
lastVisit: '',
});
const emit = defineEmits(['update-member']);
function open(member: Member | null) {
if (member === undefined) {
return;
}
if (member !== null) {
localMember.value = member;
newMember.value = false;
} else {
localMember.value = {
id: 0,
firstName: '',
lastName: '',
birthday: '',
age: '',
address: '',
town: '',
zip: '',
phone: '',
email: '',
group: '',
responsiblePerson: '',
firstVisit: '',
lastVisit: '',
};
newMember.value = true;
}
dialog.value?.open();
}
async function save() {
const valid = await form.value.validate();
if (!valid) return;
let query = 'secure/members/edit?id=' + localMember.value.id;
if (newMember.value) {
query = 'secure/members/add';
}
appApi
.post(query, JSON.stringify(localMember.value))
.then(() => {
emit('update-member');
dialog.value.close();
})
.catch((err) => NotifyResponse(err, 'error'));
}
defineExpose({ open });
</script>
<style>
.required .q-field__label::after {
content: ' *';
color: red;
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<DialogFrame
ref="dialog"
:header-title="newRole ? $t('addNewRole') : 'Edit ' + localRole.role"
:height="600"
:width="500"
>
<div class="row justify-center q-gutter-md">
<q-input
class="q-ml-md col-5 required"
:label="$t('role')"
filled
:rules="[(val) => !!val || $t('roleIsRequired')]"
v-model="localRole.role"
autofocus
></q-input>
</div>
<div class="row justify-center">
<q-btn class="q-ma-md" color="primary" no-caps @click="save">Save</q-btn>
</div>
</DialogFrame>
</template>
<script setup lang="ts">
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
import { ref } from 'vue';
import { appApi } from 'src/boot/axios';
import type { Role } from 'src/vueLib/models/roles';
import { useNotify } from 'src/vueLib/general/useNotify';
const { NotifyResponse } = useNotify();
const dialog = ref();
const newRole = ref(false);
const localRole = ref<Role>({
role: '',
rights: null,
});
const emit = defineEmits(['update-role']);
function open(role: Role | null) {
if (role === undefined) {
return;
}
if (role !== null) {
localRole.value = role;
newRole.value = false;
} else {
localRole.value = {
role: '',
rights: null,
};
newRole.value = true;
}
dialog.value?.open();
}
function save() {
let query = 'secure/roles/edit?id=' + localRole.value.id;
if (newRole.value) {
query = 'secure/roles/add';
}
appApi
.post(query, JSON.stringify(localRole.value))
.then(() => {
emit('update-role');
dialog.value.close();
})
.catch((err) => NotifyResponse(err, 'error'));
}
defineExpose({ open });
</script>
<style>
.required .q-field__label::after {
content: ' *';
color: red;
}
</style>

View File

@@ -4,7 +4,7 @@
<div class="row"> <div class="row">
<q-uploader <q-uploader
style="max-width: 300px" style="max-width: 300px"
:url="`http://localhost:` + portApp + `/api/members/import/csv`" :url="/api/members/import/csv"
label="Import CSV" label="Import CSV"
multiple multiple
accept=".csv" accept=".csv"

View File

@@ -0,0 +1,107 @@
<template>
<DialogFrame
ref="dialog"
:header-title="newUser ? $t('addNewUser') : 'Edit ' + localUser.user"
:height="600"
:width="500"
>
<div class="row justify-center q-gutter-md">
<q-input
class="q-ml-md col-5 required"
:label="$t('user')"
filled
:rules="[(val) => !!val || $t('userIsRequired')]"
v-model="localUser.user"
autofocus
></q-input>
<q-input
class="q-ml-md col-5 required"
:label="$t('email')"
filled
:rules="[(val) => !!val || $t('emailIsRequired')]"
v-model="localUser.email"
></q-input>
<q-input
class="q-ml-md col-5 required"
:label="$t('role')"
filled
:rules="[(val) => !!val || $t('roleIsRequired')]"
v-model="localUser.role"
></q-input>
<q-input
class="q-ml-md col-5"
:label="$t('expires')"
filled
v-model="localUser.expires"
></q-input>
</div>
<div class="row justify-center">
<q-btn class="q-ma-md" color="primary" no-caps @click="save">Save</q-btn>
</div>
</DialogFrame>
</template>
<script setup lang="ts">
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
import { ref } from 'vue';
import { appApi } from 'src/boot/axios';
import type { User } from 'src/vueLib/models/users';
import { useNotify } from 'src/vueLib/general/useNotify';
const { NotifyResponse } = useNotify();
const dialog = ref();
const newUser = ref(false);
const localUser = ref<User>({
user: '',
email: '',
role: '',
expires: '',
});
const emit = defineEmits(['update-user']);
function open(user: User | null) {
if (user === undefined) {
return;
}
if (user !== null) {
localUser.value = user;
newUser.value = false;
} else {
localUser.value = {
user: '',
email: '',
role: '',
expires: '',
};
newUser.value = true;
}
dialog.value?.open();
}
function save() {
let query = 'secure/users/edit?id=' + localUser.value.id;
if (newUser.value) {
query = 'secure/users/add';
}
appApi
.post(query, JSON.stringify(localUser.value))
.then(() => {
emit('update-user');
dialog.value.close();
})
.catch((err) => NotifyResponse(err, 'error'));
}
defineExpose({ open });
</script>
<style>
.required .q-field__label::after {
content: ' *';
color: red;
}
</style>

View File

@@ -14,32 +14,25 @@
<q-tab name="users" no-caps :label="$t('users')" /> <q-tab name="users" no-caps :label="$t('users')" />
<q-tab name="roles" no-caps :label="$t('roles')" /> <q-tab name="roles" no-caps :label="$t('roles')" />
</q-tabs> </q-tabs>
<q-seperator /> <q-separator />
<q-tab-panels v-model="tab" animated> <q-tab-panels v-model="tab" animated>
<q-tab-panel name="users"> <q-tab-panel name="users">
<q-table :rows="users" :columns="columns"> </q-table> <UserTable />
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="roles"> <q-tab-panel name="roles">
<q-table :rows="users" :columns="columns"> </q-table> <RoleTable />
</q-tab-panel> </q-tab-panel>
</q-tab-panels> </q-tab-panels>
</q-card> </q-card>
</div> </div>
</div> </div>
<q-btn no-caps color="primary" @click="console.log('save')">{{ $t('save') }}</q-btn>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Users } from 'src/vueLib/models/users'; import { ref } from 'vue';
import { computed, ref } from 'vue'; import UserTable from 'src/vueLib/tables/users/UserTable.vue';
import RoleTable from 'src/vueLib/tables/roles/RoleTable.vue';
const tab = ref('users'); const tab = ref('users');
const columns = computed(() => [
{ name: 'name', align: 'center' as const, label: 'Name', field: 'name' },
]);
const users = ref<Users>([]);
</script> </script>

View File

@@ -26,7 +26,7 @@ export function useLogin() {
}); });
const resp = await appApi.get('/login/me'); const resp = await appApi.get('/login/me');
userStore.setUser({ username: resp.data.user, role: resp.data.role }); userStore.setUser({ id: resp.data.id, username: resp.data.user, role: resp.data.role });
startRefreshInterval(); startRefreshInterval();
return true; return true;
@@ -52,7 +52,7 @@ export function useLogin() {
appApi appApi
.get('/login/me') .get('/login/me')
.then((resp) => { .then((resp) => {
userStore.setUser({ username: resp.data.user, role: resp.data.role }); userStore.setUser({ id: resp.data.id, username: resp.data.user, role: resp.data.role });
if (!intervalId) { if (!intervalId) {
startRefreshInterval(); startRefreshInterval();
} }

View File

@@ -3,6 +3,7 @@ import { useGlobalRouter } from 'src/utils/globalRouter';
import { useGlobalQ } from 'src/utils/globalQ'; import { useGlobalQ } from 'src/utils/globalQ';
interface User { interface User {
id: number;
username: string; username: string;
role: string; role: string;
} }

View File

@@ -0,0 +1,8 @@
export interface Right {
name: string;
read: boolean;
write: boolean;
delete: boolean;
}
export type Rights = Right[];

View File

@@ -0,0 +1,9 @@
import type { Rights } from './rights';
export interface Role {
id?: number;
role: string;
rights: Rights | null;
}
export type Roles = Role[];

View File

@@ -1,6 +1,7 @@
export interface User { export interface User {
id: number; id?: number;
name: string; user: string;
email: string;
role: string; role: string;
expires: string; expires: string;
} }

View File

@@ -28,7 +28,7 @@ export function useMemberTable() {
{ {
name: 'lastName', name: 'lastName',
align: 'left' as const, align: 'left' as const,
label: i18n.global.t('lastname'), label: i18n.global.t('lastName'),
field: 'lastName', field: 'lastName',
sortable: true, sortable: true,
}, },
@@ -196,6 +196,7 @@ export function useMemberTable() {
loading.value = false; loading.value = false;
}); });
} }
return { return {
members, members,
pagination, pagination,

View File

@@ -104,8 +104,8 @@
</template> </template>
</q-table> </q-table>
</div> </div>
<EditOneDialog ref="editOneDialog" v-on:update-member="updateMembers"></EditOneDialog> <EditOneDialog ref="editOneDialog" v-on:update="updateMembers"></EditOneDialog>
<EditAllDialog ref="editAllDialog" v-on:update-member="updateMembers"></EditAllDialog> <EditAllDialog ref="editAllDialog" v-on:update="updateMembers"></EditAllDialog>
<OkDialog <OkDialog
ref="okDialog" ref="okDialog"
:dialog-label="$t('delete')" :dialog-label="$t('delete')"
@@ -125,7 +125,7 @@ import { appApi } from 'src/boot/axios';
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import type { Member, Members } from 'src/vueLib/models/member'; import type { Member, Members } from 'src/vueLib/models/member';
import EditOneDialog from 'src/components/EditOneDialog.vue'; import EditOneDialog from 'src/components/EditOneDialog.vue';
import EditAllDialog from 'src/components/EditAllDialog.vue'; import EditAllDialog from 'src/components/MemberEditAllDialog.vue';
import OkDialog from 'src/components/dialog/OkDialog.vue'; import OkDialog from 'src/components/dialog/OkDialog.vue';
import { useNotify } from 'src/vueLib/general/useNotify'; import { useNotify } from 'src/vueLib/general/useNotify';
import { useMemberTable } from './MembersTable'; import { useMemberTable } from './MembersTable';

View File

@@ -0,0 +1,79 @@
import { appApi } from 'src/boot/axios';
import { ref, computed } from 'vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { i18n } from 'boot/lang';
import type { Roles } from 'src/vueLib/models/roles';
export const roles = ref<Roles>([]);
export function useRoleTable() {
const pagination = ref({
sortBy: 'role',
descending: false,
page: 1,
rowsPerPage: 10,
});
const columns = computed(() => [
{
name: 'id',
align: 'left' as const,
label: 'Id',
field: 'id',
sortable: true,
style: 'width: 50px; max-width: 50px;',
},
{
name: 'role',
align: 'left' as const,
label: i18n.global.t('role'),
field: 'role',
sortable: true,
},
{
name: 'rights',
align: 'left' as const,
label: i18n.global.t('rights'),
field: 'rights',
sortable: true,
style: 'width: 120px; max-width: 120px;',
},
{ name: 'option', align: 'center' as const, label: '', field: 'option', icon: 'option' },
]);
const { NotifyResponse } = useNotify();
const loading = ref(false);
//updates user list from database
function updateRoles() {
loading.value = true;
appApi
.get('secure/roles')
.then((resp) => {
if (resp.data === null) {
roles.value = [];
return;
}
roles.value = resp.data as Roles;
if (roles.value === null) {
roles.value = [];
return;
}
})
.catch((err) => {
NotifyResponse(err, 'error');
})
.finally(() => {
loading.value = false;
});
}
return {
roles,
pagination,
columns,
loading,
updateRoles,
};
}

View File

@@ -0,0 +1,201 @@
<template>
<div class="q-pa-md">
<q-table
flat
bordered
ref="tableRef"
title="Roles"
title-class="text-bold text-blue-9"
:no-data-label="$t('noDataAvailable')"
:loading-label="$t('loading')"
:rows-per-page-label="$t('recordsPerPage')"
:selected-rows-label="(val) => val + $t('recordSelected')"
:rows="roles"
:columns="columns"
row-key="id"
v-model:pagination="pagination"
:loading="loading"
:filter="filter"
:selection="selectOption ? 'multiple' : 'none'"
v-model:selected="selected"
binary-state-sort
class="bigger-table-text"
>
<template v-slot:top-left>
<q-btn-group push flat style="color: grey">
<q-btn dense flat icon="add" @click="openAllValueDialog(null)">
<q-tooltip>{{ $t('addNewRole') }}</q-tooltip>
</q-btn>
<q-btn
dense
flat
style="color: grey"
:icon="selectOption ? 'check_box' : 'check_box_outline_blank'"
@click="selectOption = !selectOption"
>
<q-tooltip>{{ $t('selectRoleOptions') }}</q-tooltip>
</q-btn>
</q-btn-group>
<div v-if="selectOption && selected.length > 0">
<q-btn flat dense icon="more_vert" @click="openSubmenu = true" />
<q-menu v-if="openSubmenu" anchor="bottom middle" self="top middle">
<q-item
clickable
v-close-popup
@click="openRemoveDialog(...selected)"
class="text-negative"
>{{ $t('delete') }}</q-item
>
</q-menu>
</div>
<div v-if="selectOption && selected.length > 0" class="q-ml-md text-weight-bold">
{{ $t('selected') }}: {{ selected.length }}
</div>
</template>
<template v-slot:top-right>
<q-input filled dense debounce="300" v-model="filter" :placeholder="$t('search')">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</template>
<template v-slot:body-cell="props">
<q-td
:props="props"
@click="openSingleValueDialog(props.col.label, props.col.name, props.row)"
>
{{ props.value }}
</q-td>
</template>
<template v-slot:body-cell-option="props">
<q-td :props="props">
<q-btn flat dense icon="delete" color="negative" @click="openRemoveDialog(props.row)">
<q-tooltip> {{ $t('delete') }} </q-tooltip>
</q-btn>
</q-td>
</template>
</q-table>
</div>
<EditOneDialog ref="editOneDialog" v-on:update="updateRoles"></EditOneDialog>
<EditAllDialog ref="editAllDialog" v-on:update="updateRoles"></EditAllDialog>
<OkDialog
ref="okDialog"
:dialog-label="$t('delete')"
:text="$t('doYouWantToDelete') + ' ' + deleteText"
label-color="red"
:button-cancel-label="$t('cancel')"
:button-ok-label="$t('confirm')"
:button-ok-flat="false"
button-ok-color="red"
v-on:update-confirm="(val) => removeRole(...val)"
></OkDialog>
</template>
<script setup lang="ts">
import { appApi } from 'src/boot/axios';
import { ref, onMounted } from 'vue';
import type { Roles, Role } from 'src/vueLib/models/roles';
import EditOneDialog from 'src/components/EditOneDialog.vue';
import EditAllDialog from 'src/components/RoleEditAllDialog.vue';
import OkDialog from 'src/components/dialog/OkDialog.vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { useRoleTable } from './RoleTable';
import { useLogin } from 'src/vueLib/login/useLogin';
const { NotifyResponse } = useNotify();
const editOneDialog = ref();
const editAllDialog = ref();
const okDialog = ref();
const deleteText = ref('');
const selectOption = ref(false);
const selected = ref<Roles>([]);
const openSubmenu = ref(false);
const filter = ref('');
const { roles, pagination, loading, columns, updateRoles } = useRoleTable();
//load on mounting page
onMounted(() => {
loading.value = true;
updateRoles();
});
// opens dialog for all role values
function openSingleValueDialog(label: string, field: string, role: Role) {
editOneDialog.value?.open(label, field, role);
}
//opens dialog for one value
function openAllValueDialog(role: Role | null) {
editAllDialog.value?.open(role);
}
//opens remove dialog
function openRemoveDialog(...roles: Roles) {
if (roles.length === 1) {
deleteText.value = "'" + roles[0]?.role + "'";
} else {
deleteText.value = String(roles.length) + ' roles';
}
okDialog.value?.open(roles);
}
//remove role from database
function removeRole(...removeRoles: Roles) {
const roles: string[] = [];
removeRoles.forEach((role: Role) => {
if (role.role) {
roles.push(role.role);
}
});
const login = useLogin();
const user = login.getUser();
appApi
.post('secure/roles/delete?role=' + user?.role, { roles: roles })
.then(() => {
updateRoles();
selected.value = [];
})
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => {
loading.value = false;
});
}
function getSelected(): Roles {
if (selected.value.length === 0) return [];
return selected.value;
}
defineExpose({
getSelected,
});
</script>
<style>
@keyframes blink-yellow {
0%,
100% {
background-color: yellow;
}
50% {
background-color: transparent;
}
}
.blink-yellow {
animation: blink-yellow 1.5s step-start 6 !important;
}
.bigger-table-text .q-table__middle td {
font-size: 14px;
}
.bigger-table-text .q-table__top,
.bigger-table-text .q-table__bottom,
.bigger-table-text th {
font-size: 14px;
}
</style>

View File

@@ -8,68 +8,26 @@ export function useUserTable() {
const users = ref<Users>([]); const users = ref<Users>([]);
const pagination = ref({ const pagination = ref({
sortBy: 'firstName', sortBy: 'user',
descending: false, descending: false,
page: 1, page: 1,
rowsPerPage: 10, rowsPerPage: 10,
}); });
const columns = computed(() => [ const columns = computed(() => [
{ name: 'cake', align: 'center' as const, label: '', field: 'cake', icon: 'cake' },
{ {
name: 'firstName', name: 'id',
align: 'left' as const, align: 'left' as const,
label: i18n.global.t('prename'), label: 'Id',
field: 'firstName', field: 'id',
sortable: true, sortable: true,
style: 'width: 50px; max-width: 50px;',
}, },
{ {
name: 'lastName', name: 'user',
align: 'left' as const, align: 'left' as const,
label: i18n.global.t('lastname'), label: i18n.global.t('user'),
field: 'lastName', field: 'user',
sortable: true,
},
{
name: 'birthday',
align: 'left' as const,
label: i18n.global.t('birthday'),
field: 'birthday',
sortable: true,
},
{
name: 'age',
align: 'left' as const,
label: i18n.global.t('age'),
field: 'age',
sortable: true,
},
{
name: 'address',
align: 'left' as const,
label: i18n.global.t('address'),
field: 'address',
sortable: true,
},
{
name: 'town',
align: 'left' as const,
label: i18n.global.t('town'),
field: 'town',
sortable: true,
},
{
name: 'zip',
align: 'left' as const,
label: i18n.global.t('zipCode'),
field: 'zip',
sortable: true,
},
{
name: 'phone',
align: 'left' as const,
label: i18n.global.t('phone'),
field: 'phone',
sortable: true, sortable: true,
}, },
{ {
@@ -80,32 +38,12 @@ export function useUserTable() {
sortable: true, sortable: true,
}, },
{ {
name: 'group', name: 'role',
align: 'left' as const, align: 'left' as const,
label: i18n.global.t('group'), label: i18n.global.t('role'),
field: 'group', field: 'role',
sortable: true,
},
{
name: 'responsiblePerson',
align: 'left' as const,
label: i18n.global.t('responsible'),
field: 'responsiblePerson',
sortable: true,
},
{
name: 'firstVisit',
align: 'left' as const,
label: i18n.global.t('firstVisit'),
field: 'firstVisit',
sortable: true,
},
{
name: 'lastVisit',
align: 'left' as const,
label: i18n.global.t('lastVisit'),
field: 'lastVisit',
sortable: true, sortable: true,
style: 'width: 120px; max-width: 120px;',
}, },
{ name: 'option', align: 'center' as const, label: '', field: 'option', icon: 'option' }, { name: 'option', align: 'center' as const, label: '', field: 'option', icon: 'option' },
]); ]);
@@ -119,7 +57,7 @@ export function useUserTable() {
loading.value = true; loading.value = true;
appApi appApi
.get('users') .get('secure/users')
.then((resp) => { .then((resp) => {
if (resp.data === null) { if (resp.data === null) {
users.value = []; users.value = [];

View File

@@ -35,9 +35,6 @@
> >
<q-tooltip>{{ $t('selectUserOptions') }}</q-tooltip> <q-tooltip>{{ $t('selectUserOptions') }}</q-tooltip>
</q-btn> </q-btn>
<q-btn dense flat icon="upload" @click="openUploadDialog">
<q-tooltip>{{ $t('importCSV') }}</q-tooltip>
</q-btn>
</q-btn-group> </q-btn-group>
<div v-if="selectOption && selected.length > 0"> <div v-if="selectOption && selected.length > 0">
<q-btn flat dense icon="more_vert" @click="openSubmenu = true" /> <q-btn flat dense icon="more_vert" @click="openSubmenu = true" />
@@ -63,7 +60,11 @@
</q-input> </q-input>
</template> </template>
<template v-slot:body-cell="props"> <template v-slot:body-cell="props">
<q-td v-if="props.col.name === 'role'" :props="props">
<q-select dense v-model="props.row.role" :options="localRoles"></q-select>
</q-td>
<q-td <q-td
v-else
:props="props" :props="props"
@click="openSingleValueDialog(props.col.label, props.col.name, props.row)" @click="openSingleValueDialog(props.col.label, props.col.name, props.row)"
> >
@@ -72,30 +73,15 @@
</template> </template>
<template v-slot:body-cell-option="props"> <template v-slot:body-cell-option="props">
<q-td :props="props"> <q-td :props="props">
<q-btn flat dense icon="more_vert" @click="openSubmenu = true" /> <q-btn flat dense icon="delete" color="negative" @click="openRemoveDialog(props.row)">
<q-menu v-if="openSubmenu" anchor="top right" self="top left"> <q-tooltip> {{ $t('delete') }} </q-tooltip>
<q-item </q-btn>
clickable
v-close-popup
@click="openAllValueDialog(props.row)"
class="text-primary"
>{{ $t('edit') }}</q-item
>
<q-item
clickable
v-close-popup
@click="openRemoveDialog(props.row)"
class="text-negative"
title="zu"
>{{ $t('delete') }}</q-item
>
</q-menu>
</q-td> </q-td>
</template> </template>
</q-table> </q-table>
</div> </div>
<EditOneDialog ref="editOneDialog" v-on:update-member="updateUsers"></EditOneDialog> <EditOneDialog ref="editOneDialog" v-on:update="updateUsers"></EditOneDialog>
<EditAllDialog ref="editAllDialog" v-on:update-member="updateUsers"></EditAllDialog> <EditAllDialog ref="editAllDialog" v-on:update="updateUsers"></EditAllDialog>
<OkDialog <OkDialog
ref="okDialog" ref="okDialog"
:dialog-label="$t('delete')" :dialog-label="$t('delete')"
@@ -107,27 +93,26 @@
button-ok-color="red" button-ok-color="red"
v-on:update-confirm="(val) => removeUser(...val)" v-on:update-confirm="(val) => removeUser(...val)"
></OkDialog> ></OkDialog>
<UploadDialog ref="uploadDialog" @update-upload="updateUsers"> </UploadDialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { appApi } from 'src/boot/axios'; import { appApi } from 'src/boot/axios';
import { ref, onMounted } from 'vue'; import { ref, onMounted, computed } from 'vue';
import type { Users, User } from 'src/vueLib/models/users'; import type { Users, User } from 'src/vueLib/models/users';
import EditOneDialog from 'src/components/EditOneDialog.vue'; import EditOneDialog from 'src/components/EditOneDialog.vue';
import EditAllDialog from 'src/components/EditAllDialog.vue'; import EditAllDialog from 'src/components/UserEditAllDialog.vue';
import OkDialog from 'src/components/dialog/OkDialog.vue'; import OkDialog from 'src/components/dialog/OkDialog.vue';
import { useNotify } from 'src/vueLib/general/useNotify'; import { useNotify } from 'src/vueLib/general/useNotify';
import UploadDialog from 'src/components/UploadDialog.vue';
import { databaseName } from '../members/MembersTable';
import { useUserTable } from './UserTable'; import { useUserTable } from './UserTable';
import { useLogin } from 'src/vueLib/login/useLogin';
import { roles } from '../roles/RoleTable';
const { NotifyResponse } = useNotify(); const { NotifyResponse } = useNotify();
const editOneDialog = ref(); const editOneDialog = ref();
const editAllDialog = ref(); const editAllDialog = ref();
const uploadDialog = ref();
const okDialog = ref(); const okDialog = ref();
const deleteText = ref(''); const deleteText = ref('');
const localRoles = computed(() => roles.value.map((role) => role.role));
const selectOption = ref(false); const selectOption = ref(false);
const selected = ref<Users>([]); const selected = ref<Users>([]);
const openSubmenu = ref(false); const openSubmenu = ref(false);
@@ -138,17 +123,7 @@ const { users, pagination, loading, columns, updateUsers } = useUserTable();
//load on mounting page //load on mounting page
onMounted(() => { onMounted(() => {
loading.value = true; loading.value = true;
appApi
.post('secure/database/open', { dbPath: databaseName.value, create: true })
.then(() => {
updateUsers(); updateUsers();
})
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => {
loading.value = false;
});
}); });
// opens dialog for all user values // opens dialog for all user values
@@ -164,28 +139,28 @@ function openAllValueDialog(user: User | null) {
//opens remove dialog //opens remove dialog
function openRemoveDialog(...users: Users) { function openRemoveDialog(...users: Users) {
if (users.length === 1) { if (users.length === 1) {
deleteText.value = "'" + users[0]?.name + "'"; deleteText.value = "'" + users[0]?.user + "'";
} else { } else {
deleteText.value = String(users.length) + ' users'; deleteText.value = String(users.length) + ' users';
} }
okDialog.value?.open(users); okDialog.value?.open(users);
} }
//opens uploader dialog
function openUploadDialog() {
uploadDialog.value?.open();
}
//remove user from database //remove user from database
function removeUser(...removeUsers: Users) { function removeUser(...removeUsers: Users) {
const userIds: number[] = []; const userIds: number[] = [];
removeUsers.forEach((user: User) => { removeUsers.forEach((user: User) => {
if (user.id) {
userIds.push(user.id); userIds.push(user.id);
}
}); });
const login = useLogin();
const user = login.getUser();
appApi appApi
.post('users/delete', { ids: userIds }) .post('secure/users/delete?id=' + user?.id, { ids: userIds })
.then(() => { .then(() => {
updateUsers(); updateUsers();
selected.value = []; selected.value = [];
@@ -196,17 +171,6 @@ function removeUser(...removeUsers: Users) {
}); });
} }
//const blinkingId = ref<number | null>(null);
// function triggerBlink(id: number) {
// blinkingId.value = id;
// // Optional: stop blinking after 3 seconds
// setTimeout(() => {
// blinkingId.value = null;
// }, 3000);
// }
function getSelected(): Users { function getSelected(): Users {
if (selected.value.length === 0) return []; if (selected.value.length === 0) return [];
return selected.value; return selected.value;

5
src/vueLib/types/vuedraggable.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module 'vuedraggable' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
export default component;
}