diff --git a/backend/bin/memberApp.exe b/backend/bin/memberApp.exe deleted file mode 100644 index 798a8eb..0000000 Binary files a/backend/bin/memberApp.exe and /dev/null differ diff --git a/backend/bin/server-arm64 b/backend/bin/server-arm64 deleted file mode 100644 index a48561c..0000000 Binary files a/backend/bin/server-arm64 and /dev/null differ diff --git a/backend/dbRequest/dbRequest.go b/backend/dbRequest/dbRequest.go deleted file mode 100644 index 76afc8f..0000000 --- a/backend/dbRequest/dbRequest.go +++ /dev/null @@ -1,45 +0,0 @@ -package dbRequest - -import ( - "backend/models" - "fmt" - "net/http" - - "github.com/gin-gonic/gin" -) - -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 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 { - if err.Error() == "sql: no rows in result set" { - c.JSON(http.StatusBadRequest, models.NewJsonErrorMessageResponse(fmt.Sprintf("no user '%s' found", username))) - return true - } - c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err)) - return true - } - return false -} diff --git a/backend/go.mod b/backend/go.mod index e1dfc07..2f46216 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,29 +3,34 @@ module backend go 1.24.5 require ( - gitea.tecamino.com/paadi/memberDB v1.0.1 + gitea.tecamino.com/paadi/access-handler v1.0.12 + gitea.tecamino.com/paadi/memberDB v1.0.4 gitea.tecamino.com/paadi/tecamino-dbm v0.1.1 gitea.tecamino.com/paadi/tecamino-logger v0.2.1 github.com/gin-contrib/cors v1.7.6 github.com/gin-gonic/gin v1.11.0 - github.com/golang-jwt/jwt/v5 v5.2.2 - golang.org/x/crypto v0.40.0 - modernc.org/sqlite v1.39.0 + golang.org/x/crypto v0.43.0 ) require ( + gitea.tecamino.com/paadi/dbHandler v1.0.4 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/glebarez/sqlite v1.11.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.18.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -45,15 +50,17 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/arch v0.20.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/mod v0.25.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.27.0 // indirect - golang.org/x/tools v0.34.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/net v0.45.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.37.0 // indirect google.golang.org/protobuf v1.36.9 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gorm.io/gorm v1.31.0 // indirect modernc.org/libc v1.66.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.39.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index b6ff9e3..1d33899 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,5 +1,9 @@ -gitea.tecamino.com/paadi/memberDB v1.0.1 h1:hNnnoCeFRBEOQ+QmizF9nzvrQ8bNed8YrDln5jeRy2Y= -gitea.tecamino.com/paadi/memberDB v1.0.1/go.mod h1:4tgbjrSZ2FZeJL68R2TFHPH34+aGxx5wtZxRmu6nZv4= +gitea.tecamino.com/paadi/access-handler v1.0.12 h1:lSmW0YrBJJvCqCg0ukTJHlFUNwd7q6hFYtNd2rfztrE= +gitea.tecamino.com/paadi/access-handler v1.0.12/go.mod h1:w71lpnuu5MgAWG3oiI9vsY2dWi4njF/iPrM/xV/dbBQ= +gitea.tecamino.com/paadi/dbHandler v1.0.4 h1:ctnaec0GDdtw3gRQdUISVDYLJ9x+vt50VW41OemfhD4= +gitea.tecamino.com/paadi/dbHandler v1.0.4/go.mod h1:y/xn/POJg1DO++67uKvnO23lJQgh+XFQq7HZCS9Getw= +gitea.tecamino.com/paadi/memberDB v1.0.4 h1:2H7obSoMq4dW+VN8PsDOv1UK75hCtlF+NymU5NBGuw4= +gitea.tecamino.com/paadi/memberDB v1.0.4/go.mod h1:iLm7nunVRzqJK8CV4PJVuWIhgPlQjNIaeOkmtfK5fMg= gitea.tecamino.com/paadi/tecamino-dbm v0.1.1 h1:vAq7mwUxlxJuLzCQSDMrZCwo8ky5usWi9Qz+UP+WnkI= gitea.tecamino.com/paadi/tecamino-dbm v0.1.1/go.mod h1:+tmf1rjPaKEoNeUcr1vdtoFIFweNG3aUGevDAl3NMBk= gitea.tecamino.com/paadi/tecamino-logger v0.2.1 h1:sQTBKYPdzn9mmWX2JXZBtGBvNQH7cuXIwsl4TD0aMgE= @@ -23,6 +27,10 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -35,8 +43,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -44,6 +52,10 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -94,23 +106,23 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -119,6 +131,8 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= +gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= diff --git a/backend/main.go b/backend/main.go index b40d8ae..080622b 100644 --- a/backend/main.go +++ b/backend/main.go @@ -3,7 +3,6 @@ package main import ( "backend/models" "backend/server" - "backend/user" "backend/utils" "flag" "fmt" @@ -14,9 +13,10 @@ import ( "strings" "time" + AccessHandler "gitea.tecamino.com/paadi/access-handler" + dbApi "gitea.tecamino.com/paadi/memberDB/api" "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" "github.com/gin-gonic/gin" @@ -59,14 +59,14 @@ func main() { TerminalOut: true, }) if err != nil { - logger.Error("main new logger", err.Error()) + logger.Error("main new logger", err) panic(err) } //new login manager - userManager, err := user.NewUserManager(".") + accessHandler, err := AccessHandler.NewAccessHandler(".", logger) if err != nil { - logger.Error("main login manager", err.Error()) + logger.Error("main login manager", err) panic(err) } @@ -74,7 +74,11 @@ func main() { s := server.NewServer() // initiate Database handler - dbHandler := dbApi.NewAPIHandler() + dbHandler, err := dbApi.NewAPIHandler(logger) + if err != nil { + logger.Error("main login manager", err) + panic(err) + } //get local ip httpString := "http://" @@ -82,17 +86,18 @@ func main() { httpString = "https://" } - allowOrigins = append(allowOrigins, httpString+"localhost:9000", httpString+"localhost:9500", httpString+"127.0.0.1:9500") + allowOrigins = append(allowOrigins, httpString+"localhost:9000", httpString+"localhost:9500", httpString+"127.0.0.1:9500", httpString+"0.0.0.0:9500") localIP, err := utils.GetLocalIP() if err != nil { - logger.Error("main", fmt.Sprintf("get local ip : %s", err.Error())) + logger.Error("main", fmt.Sprintf("get local ip : %s", err)) } else { allowOrigins = append(allowOrigins, fmt.Sprintf("%s%s:9000", httpString, localIP), fmt.Sprintf("%s%s:9500", httpString, localIP)) } s.Routes.Use(cors.New(cors.Config{ - AllowOrigins: allowOrigins, + AllowOrigins: allowOrigins, + //AllowOrigins: []string{"*"}, AllowMethods: []string{"POST", "GET", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Origin", "Content-Type"}, ExposeHeaders: []string{"Content-Length"}, @@ -100,21 +105,25 @@ func main() { MaxAge: 12 * time.Hour, })) + //set logger for AuthMiddleware + accessHandler.SetMiddlewareLogger(s.Routes) api := s.Routes.Group("/api") //set routes //public - api.GET("/logout", userManager.Logout) - api.GET("/login/me", userManager.Me) + api.GET("/logout", accessHandler.Logout) + api.GET("/login/me", accessHandler.Me) - api.POST("/login", userManager.Login) + api.POST("/login", accessHandler.Login) //private - auth := api.Group("/secure", user.AuthMiddleware()) + auth := api.Group("", accessHandler.AuthMiddleware()) - auth.GET("/users", userManager.GetUserById) - auth.GET("/members", dbHandler.GetMemberById) - auth.GET("/roles", userManager.GetRoleById) + role := auth.Group("", accessHandler.AuthorizeRole("/api")) + role.GET("/members", dbHandler.GetMember) + + auth.GET("/users", accessHandler.GetUser) + auth.GET("/roles", accessHandler.GetRole) auth.POST("database/open", dbHandler.OpenDatabase) auth.POST("/members/add", dbHandler.AddNewMember) @@ -122,23 +131,22 @@ func main() { auth.POST("/members/delete", dbHandler.DeleteMember) auth.POST("/members/import/csv", dbHandler.ImportCSV) - auth.POST("/settings/update", userManager.UpdateSettings) + auth.POST("/roles/add", accessHandler.AddRole) + auth.POST("/roles/update", accessHandler.UpdateRole) + auth.POST("/roles/delete", accessHandler.DeleteRole) - auth.POST("/roles/add", userManager.AddRole) - auth.POST("/roles/update", userManager.UpdateRole) - auth.POST("/roles/delete", userManager.DeleteRole) + auth.POST("/users/add", accessHandler.AddUser) + auth.POST("/users/update", accessHandler.UpdateUser) + auth.POST("/users/delete", accessHandler.DeleteUser) - auth.POST("/users/add", userManager.AddUser) - auth.POST("/users/delete", userManager.DeleteUser) - - auth.POST("/login/refresh", userManager.Refresh) + api.POST("/login/refresh", accessHandler.Refresh) // Serve static files s.Routes.StaticFS("/assets", gin.Dir(filepath.Join(*spa, "assets"), true)) s.Routes.NoRoute(func(c *gin.Context) { // Disallow fallback for /api paths if strings.HasPrefix(c.Request.URL.Path, "/api") { - c.JSON(http.StatusNotFound, models.NewJsonErrorMessageResponse("API endpoint not found")) + c.JSON(http.StatusNotFound, models.NewJsonMessageResponse("API endpoint not found")) return } // Try to serve file from SPA directory @@ -155,7 +163,7 @@ func main() { go func() { time.Sleep(500 * time.Millisecond) 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)) } }() diff --git a/backend/members.dba b/backend/members.dba index eb0c588..5cd7f65 100644 Binary files a/backend/members.dba and b/backend/members.dba differ diff --git a/backend/models/jsonResponse.go b/backend/models/jsonResponse.go index 00eb5c6..e7d7cab 100644 --- a/backend/models/jsonResponse.go +++ b/backend/models/jsonResponse.go @@ -5,16 +5,14 @@ type JsonResponse struct { Message string `json:"message,omitempty"` } -func NewJsonErrorMessageResponse(msg string) JsonResponse { +func NewJsonMessageResponse(msg string) JsonResponse { return JsonResponse{ - Error: true, Message: msg, } } func NewJsonErrorResponse(err error) JsonResponse { return JsonResponse{ - Error: true, Message: err.Error(), } } diff --git a/backend/models/rights.go b/backend/models/rights.go index 70fecfa..e93beec 100644 --- a/backend/models/rights.go +++ b/backend/models/rights.go @@ -1,8 +1,6 @@ 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"` -} +// type Rights struct { +// Name string `json:"name"` +// Rights int `json:"rights"` +// } diff --git a/backend/models/role.go b/backend/models/role.go index 7a99145..728d71e 100644 --- a/backend/models/role.go +++ b/backend/models/role.go @@ -1,11 +1,11 @@ package models -type Role struct { - Id int `json:"id"` - Role string `json:"role"` - Rights []Rights `json:"rights"` -} +// type Role struct { +// Id int `json:"id"` +// Role string `json:"role"` +// Rights []Rights `json:"rights"` +// } -func (r *Role) IsValid() bool { - return r.Role != "" -} +// func (r *Role) IsValid() bool { +// return r.Role != "" +// } diff --git a/backend/models/settings.go b/backend/models/settings.go index ac4c916..36ce147 100644 --- a/backend/models/settings.go +++ b/backend/models/settings.go @@ -1,9 +1,11 @@ package models -type Settings struct { - PrimaryColor string `json:"primaryColor,omitempty"` - SecondaryColor string `json:"secondaryColor,omitempty"` - Icon string `json:"icon,omitempty"` - DatabaseName string `json:"databaseName,omitempty"` - DatabaseToken string `json:"databaseToken,omitempty"` -} +// type Settings struct { +// PrimaryColor string `json:"primaryColor,omitempty"` +// PrimaryColorText string `json:"primaryColorText,omitempty"` +// SecondaryColor string `json:"secondaryColor,omitempty"` +// SecondaryColorText string `json:"secondaryColorText,omitempty"` +// Icon string `json:"icon,omitempty"` +// DatabaseName string `json:"databaseName,omitempty"` +// DatabaseToken string `json:"databaseToken,omitempty"` +// } diff --git a/backend/models/user.go b/backend/models/user.go index 54a52cc..c09281a 100644 --- a/backend/models/user.go +++ b/backend/models/user.go @@ -1,14 +1,14 @@ 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"` -} +// 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"` +// } -func (u *User) IsValid() bool { - return u.Name != "" -} +// func (u *User) IsValid() bool { +// return u.Name != "" +// } diff --git a/backend/user/Middleware.go b/backend/user/Middleware.go deleted file mode 100644 index 7fd79a7..0000000 --- a/backend/user/Middleware.go +++ /dev/null @@ -1,41 +0,0 @@ -package user - -import ( - "net/http" - - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" -) - -func AuthMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - // Read access token from cookie - cookie, err := c.Cookie("access_token") - if err != nil { - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "not logged in"}) - return - } - - token, err := jwt.Parse(cookie, func(t *jwt.Token) (any, error) { - return JWT_SECRET, nil - }) - if err != nil || !token.Valid { - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "invalid token"}) - return - } - c.Next() - } -} - -func AuthorizeRole(roles ...string) gin.HandlerFunc { - return func(c *gin.Context) { - userRole := c.GetString("role") - for _, role := range roles { - if userRole == role { - c.Next() - return - } - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Forbidden"}) - } - } -} diff --git a/backend/user/login.go b/backend/user/login.go deleted file mode 100644 index 99f096a..0000000 --- a/backend/user/login.go +++ /dev/null @@ -1,181 +0,0 @@ -package user - -import ( - "backend/dbRequest" - "backend/models" - "backend/utils" - "encoding/json" - "io" - "net/http" - "time" - - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" - _ "modernc.org/sqlite" -) - -var JWT_SECRET = []byte("4h5Jza1Fn_zuzu&417%8nH*UH100+55-") -var DOMAIN = "localhost" -var ACCESS_TOKEN_TIME = 15 * time.Minute -var REFRESH_TOKEN_TIME = 72 * time.Hour - -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)) - 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 storedPassword, settingsJsonString string - if err := um.database.QueryRow(dbRequest.DBQueryPassword, user.Name).Scan(&user.Id, &user.Role, &storedPassword, &settingsJsonString); dbRequest.CheckDBError(c, user.Name, err) { - return - } - - err = json.Unmarshal([]byte(settingsJsonString), &user.Settings) - if err != nil { - c.JSON(http.StatusBadRequest, models.NewJsonErrorMessageResponse(err.Error())) - return - } - - if !utils.CheckPassword(user.Password, storedPassword) { - c.JSON(http.StatusBadRequest, models.NewJsonErrorMessageResponse("wrong password")) - return - } - - // ---- Create JWT tokens ---- - accessTokenExp := time.Now().Add(ACCESS_TOKEN_TIME) - refreshTokenExp := time.Now().Add(REFRESH_TOKEN_TIME) - - // Create token - accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "id": user.Id, - "username": user.Name, - "role": user.Role, - "type": "access", - "exp": accessTokenExp.Unix(), - }) - - refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "id": user.Id, - "username": user.Name, - "role": user.Role, - "type": "refresh", - "exp": refreshTokenExp.Unix(), - }) - - accessString, err := accessToken.SignedString(JWT_SECRET) - if err != nil { - c.JSON(http.StatusInternalServerError, models.NewJsonErrorMessageResponse("could not create access token")) - return - } - - refreshString, err := refreshToken.SignedString(JWT_SECRET) - if err != nil { - c.JSON(http.StatusInternalServerError, models.NewJsonErrorMessageResponse("could not create refresh token")) - return - } - - // ---- Set secure cookies ---- - secure := gin.Mode() == gin.ReleaseMode - c.SetCookie("access_token", accessString, int(time.Until(accessTokenExp).Seconds()), - "/", "", secure, true) // Path=/, Secure=true (only HTTPS), HttpOnly=true - c.SetCookie("refresh_token", refreshString, int(time.Until(refreshTokenExp).Seconds()), - "/", "", secure, true) - - c.JSON(http.StatusOK, gin.H{ - "message": "login successful", - "id": user.Id, - "user": user.Name, - "role": user.Role, - "settings": user.Settings, - }) -} - -func (um *UserManager) Refresh(c *gin.Context) { - refreshCookie, err := c.Cookie("refresh_token") - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"message": "no refresh token"}) - return - } - - token, err := jwt.Parse(refreshCookie, func(token *jwt.Token) (any, error) { - return JWT_SECRET, nil - }) - if err != nil || !token.Valid { - c.JSON(http.StatusUnauthorized, gin.H{"message": "invalid refresh token"}) - return - } - - claims, ok := token.Claims.(jwt.MapClaims) - if !ok || claims["type"] != "refresh" { - c.JSON(http.StatusUnauthorized, gin.H{"message": "invalid token type"}) - return - } - - 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(), - }) - accessString, _ := newAccess.SignedString(JWT_SECRET) - - c.SetCookie("access_token", accessString, int(time.Until(accessExp).Seconds()), "/", DOMAIN, gin.Mode() == gin.ReleaseMode, true) - c.JSON(http.StatusOK, gin.H{"message": "token refreshed"}) -} - -func (um *UserManager) Me(c *gin.Context) { - // Read access token from cookie - cookie, err := c.Cookie("access_token") - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"message": "not logged in"}) - return - } - - // Verify token - token, err := jwt.Parse(cookie, func(t *jwt.Token) (any, error) { - return JWT_SECRET, nil - }) - if err != nil || !token.Valid { - c.JSON(http.StatusUnauthorized, gin.H{"message": "invalid token"}) - return - } - - claims := token.Claims.(jwt.MapClaims) - c.JSON(http.StatusOK, gin.H{ - "id": claims["id"], - "user": claims["username"], - "role": claims["role"], - }) -} - -func (um *UserManager) Logout(c *gin.Context) { - secure := gin.Mode() == gin.ReleaseMode - - c.SetCookie("access_token", "", -1, "/", DOMAIN, secure, true) - c.SetCookie("refresh_token", "", -1, "/", DOMAIN, secure, true) - - c.JSON(http.StatusOK, gin.H{"message": "logged out"}) -} diff --git a/backend/user/manager.go b/backend/user/manager.go deleted file mode 100644 index c653d30..0000000 --- a/backend/user/manager.go +++ /dev/null @@ -1,261 +0,0 @@ -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 { - database *sql.DB -} - -func NewUserManager(dir string) (*UserManager, error) { - if dir == "" { - 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 { - _, err = um.database.Exec(dbRequest.CreateUserTable) - if err != nil { - return nil, err - } - - hash, err := utils.HashPassword("tecamino@2025") - if err != nil { - return nil, err - } - _, err = um.database.Exec(dbRequest.NewUser, "admin", "", "admin", hash, `{"databaseName":"members.dba","primaryColor":"#1976d2", "secondaryColor":"#26a69a"}`) - if err != nil { - return nil, err - } - } - 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 deleted file mode 100644 index c677f4d..0000000 --- a/backend/user/roles.go +++ /dev/null @@ -1,245 +0,0 @@ -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 deleted file mode 100644 index ee20561..0000000 --- a/backend/user/settings.go +++ /dev/null @@ -1,46 +0,0 @@ -package user - -import ( - "backend/dbRequest" - "backend/models" - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/gin-gonic/gin" -) - -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)) - return - } - - user := models.User{} - err = json.Unmarshal(body, &user) - if err != nil { - c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err)) - return - } - - jsonBytes, err := json.Marshal(user.Settings) - if err != nil { - c.JSON(http.StatusOK, models.NewJsonErrorMessageResponse(err.Error())) - return - } - - if _, err := um.database.Exec(dbRequest.DBUpdateSettings, string(jsonBytes), user.Name); err != nil { - c.JSON(http.StatusBadRequest, models.NewJsonErrorResponse(err)) - return - } - - c.JSON(http.StatusOK, gin.H{ - "message": fmt.Sprintf("user settings '%s' successfully updated", user.Name), - }) -} diff --git a/backend/utils/utils.go b/backend/utils/utils.go index 21370a1..8acad74 100644 --- a/backend/utils/utils.go +++ b/backend/utils/utils.go @@ -6,7 +6,6 @@ import ( "os" "os/exec" "path/filepath" - "regexp" "runtime" "gitea.tecamino.com/paadi/tecamino-logger/logging" @@ -64,8 +63,3 @@ 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/package-lock.json b/package-lock.json index fb8d593..83f46f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lightcontrol", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lightcontrol", - "version": "1.0.0", + "version": "1.0.1", "hasInstallScript": true, "dependencies": { "@capacitor-community/sqlite": "^7.0.1", @@ -19,12 +19,15 @@ "vue": "^3.4.18", "vue-i18n": "^11.1.12", "vue-router": "^4.0.12", - "vuedraggable": "^4.1.0" + "vue3-touch-events": "^5.0.13", + "vuedraggable": "^4.1.0", + "zxcvbn": "^4.4.2" }, "devDependencies": { "@eslint/js": "^9.14.0", "@quasar/app-vite": "^2.1.0", "@types/node": "^20.5.9", + "@types/zxcvbn": "^4.4.5", "@vue/eslint-config-prettier": "^10.1.0", "@vue/eslint-config-typescript": "^14.4.0", "autoprefixer": "^10.4.2", @@ -1883,6 +1886,13 @@ "@types/send": "*" } }, + "node_modules/@types/zxcvbn": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.5.tgz", + "integrity": "sha512-FZJgC5Bxuqg7Rhsm/bx6gAruHHhDQ55r+s0JhDh8CQ16fD7NsJJ+p8YMMQDhSQoIrSmjpqqYWA96oQVMNkjRyA==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.31.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz", @@ -8222,6 +8232,15 @@ "typescript": ">=5.0.0" } }, + "node_modules/vue3-touch-events": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/vue3-touch-events/-/vue3-touch-events-5.0.13.tgz", + "integrity": "sha512-VOprVhKsL5DaistDFU0+oLJz/LaFzVENeUzs4Hp3PeD0TVx1vNEgNgS/9WehUHlUIUuCdnmLm0TkmITjwVmUBQ==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/vuedraggable": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz", @@ -8415,6 +8434,12 @@ "engines": { "node": ">= 14" } + }, + "node_modules/zxcvbn": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz", + "integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==", + "license": "MIT" } } } diff --git a/package.json b/package.json index 7f7a5b9..44332c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lightcontrol", - "version": "1.0.1", + "version": "1.0.4", "description": "A Tecamino App", "productName": "Member Database", "author": "A. Zuercher", @@ -25,12 +25,15 @@ "vue": "^3.4.18", "vue-i18n": "^11.1.12", "vue-router": "^4.0.12", - "vuedraggable": "^4.1.0" + "vue3-touch-events": "^5.0.13", + "vuedraggable": "^4.1.0", + "zxcvbn": "^4.4.2" }, "devDependencies": { "@eslint/js": "^9.14.0", "@quasar/app-vite": "^2.1.0", "@types/node": "^20.5.9", + "@types/zxcvbn": "^4.4.5", "@vue/eslint-config-prettier": "^10.1.0", "@vue/eslint-config-typescript": "^14.4.0", "autoprefixer": "^10.4.2", diff --git a/quasar.config.ts b/quasar.config.ts index e445758..90459c8 100644 --- a/quasar.config.ts +++ b/quasar.config.ts @@ -109,7 +109,7 @@ export default defineConfig((/* ctx */) => { // you can manually specify Quasar components/directives to be available everywhere: // // components: [], - // directives: [], + //directives: [], // Quasar plugins plugins: ['Notify', 'Dialog'], diff --git a/src/assets/lang/de-CH.yaml b/src/assets/lang/de-CH.yaml index eff15da..4612c97 100644 --- a/src/assets/lang/de-CH.yaml +++ b/src/assets/lang/de-CH.yaml @@ -37,7 +37,9 @@ password: Passwort isRequired: isch erforderlich colors: Farbe primaryColor: Primär Farb +primaryColorText: Primär Text Farb secondaryColor: Sekondär Farb +secondaryColorText: Sekondär Text Farb database: Datebank general: Augemein setColors: Setz Farbe @@ -51,12 +53,31 @@ 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 +prenameIsRequired: Vorname isch erforderlich +lastNameIsRequired: Nachname isch erforderlich +birthdayIsRequired: Geburtstag isch erforderlich +userIsRequired: Benutzer isch erforderlich +emailIsRequired: Email isch erforderlich +roleIsRequired: Rolle isch erforderlich +permissions: Recht selectRoleOptions: Wähle Roue Optione addNewRole: Füeg neui Roue hinzue +veryWeak: sehr Schwach +weak: Schwach +fair: So so +good: Guet +strong: Stark +passwordIsRequired: Password isch erforderlich +passwordTooShort: Ds Passwort mues mindestens 8 Zeiche läng si +passwordNeedsUppercase: Ds Passwort mues mindestens ei Grossbuechstabe enthaute +passwordNeedsLowercase: Ds Passwort mues mindestens ei Chlibuechstabe enthaute +passwordNeedsNumber: Ds Passwort mues mindestens ei Zau enthaute +passwordNeedsSpecial: Ds Passwort mues mindestens eis Sonderzeiche enthaute +passwordDoNotMatch: Passwörter stimme nid überei +read: Lese +write: Schribe +userSettings: Benutzer Istellige +members: Mitglider +attendanceTable: Anweseheits Tabelle +excursionTable: Usflugs Tabelle +updated: aktualisiert diff --git a/src/assets/lang/de-DE.yaml b/src/assets/lang/de-DE.yaml index 59e2f1e..f45deb9 100644 --- a/src/assets/lang/de-DE.yaml +++ b/src/assets/lang/de-DE.yaml @@ -37,7 +37,9 @@ password: Passwort isRequired: ist erforderlich colors: Farben primaryColor: Primär Farbe +primaryColorText: Primär Text Farbe secondaryColor: Sekondär Farbe +secondaryColorText: Sekondär Text Farbe database: Datenbank general: Allgemein setColors: Setze Farben @@ -57,6 +59,25 @@ birthdayIsRequired: Geburtstag ist erforderlich userIsRequired: Benutzer ist erforderlich emailIsRequired: Email ist erforderlich roleIsRequired: Rolle ist erforderlich -rights: Rechte +permissions: Rechte selectRoleOptions: Wähle Rollen Option addNewRole: Füge neue Rolle hinzu +veryWeak: sehr Schwach +weak: Schwach +fair: Ausreichend +good: Gut +strong: +passwordIsRequired: Password ist erforderlich +passwordTooShort: Das Passwort muss mindestens 8 Zeichen lang sein +passwordNeedsUppercase: Das Passwort muss mindestens einen Großbuchstaben enthalten +passwordNeedsLowercase: Das Passwort muss mindestens einen Kleinbuchstaben enthalten +passwordNeedsNumber: Das Passwort muss mindestens eine Zahl enthalten +passwordNeedsSpecial: Das Passwort muss mindestens ein Sonderzeichen enthalten +passwordDoNotMatch: Passwörter stimmen nicht überein +read: Lesen +write: Schreiben +userSettings: Benutzer Einstellungen +members: Mitglieder +attendanceTable: Anwesenheits Tabelle +excursionTable: Ausflugs Tabelle +updated: aktualisiert diff --git a/src/assets/lang/en-US.yaml b/src/assets/lang/en-US.yaml index 9dfca59..706ffa8 100644 --- a/src/assets/lang/en-US.yaml +++ b/src/assets/lang/en-US.yaml @@ -37,7 +37,9 @@ password: Password isRequired: is required colors: Colors primaryColor: Primary Color +primaryColorText: Primary Text Color secondaryColor: Secondary Color +secondaryColorText: Secondary Text Color database: Database general: General setColors: Set Colors @@ -57,6 +59,25 @@ birthdayIsRequired: Birthday is required userIsRequired: User is required emailIsRequired: Email is required roleIsRequired: Role is required -rights: Rights +permissions: Permissions selectRoleOptions: Select Role Options addNewRole: Add new Role +veryWeak: very Weak +weak: Weak +fair: Fair +good: Good +strong: Strong +passwordIsRequired: Password is required +passwordTooShort: Password must be at least 8 characters long +passwordNeedsUppercase: Password must contain at least one uppercase letter +passwordNeedsLowercase: Password must contain at least one lowercase letter +passwordNeedsNumber: Password must contain at least one number +passwordNeedsSpecial: Password must contain at least one special character +passwordDoNotMatch: Password do not match +read: Read +write: Write +userSettings: User Settings +members: Members +attendanceTable: Attendance Table +excursionTable: Excursion Table +updated: updated diff --git a/src/boot/auth.ts b/src/boot/auth.ts index e864967..3bc8220 100644 --- a/src/boot/auth.ts +++ b/src/boot/auth.ts @@ -14,7 +14,9 @@ export default boot(async ({ app }) => { await appApi .get('/login/me') .then((resp) => { - useStore.setUser({ id: resp.data.id, username: resp.data.username, role: resp.data.role }); + useStore + .setUser({ id: resp.data.id, username: resp.data.username, role: resp.data.role }) + .catch((err) => console.log(err)); login.refresh().catch((err) => console.error(err)); }) .catch(() => { diff --git a/src/boot/axios.ts b/src/boot/axios.ts index fe58472..e3ee246 100644 --- a/src/boot/axios.ts +++ b/src/boot/axios.ts @@ -17,7 +17,7 @@ interface RetryRequestConfig extends AxiosRequestConfig { _retry?: boolean; } -const noRefreshEndpoints = ['/login', '/secure/login/refresh', '/logout']; +const noRefreshEndpoints = ['/login', '/login/refresh', '/logout']; // ========= Refresh Queue Handling ========= // let isRefreshing = false; diff --git a/src/boot/quasar-global.ts b/src/boot/quasar-global.ts index 249af1f..1c309a6 100644 --- a/src/boot/quasar-global.ts +++ b/src/boot/quasar-global.ts @@ -1,6 +1,6 @@ import { boot } from 'quasar/wrappers'; -import { setQuasarInstance } from 'src/utils/globalQ'; -import { setRouterInstance } from 'src/utils/globalRouter'; +import { setQuasarInstance } from 'src/vueLib/utils/globalQ'; +import { setRouterInstance } from 'src/vueLib/utils/globalRouter'; import { databaseName } from 'src/vueLib/tables/members/MembersTable'; import { Logo } from 'src/vueLib/models/logo'; @@ -15,11 +15,29 @@ export default boot(({ app, router }) => { if (primaryColor == null || primaryColor === 'undefined' || primaryColor.trim() === '') { primaryColor = null; } + let primaryColorText = localStorage.getItem('primaryColorText'); + if ( + primaryColorText == null || + primaryColorText === 'undefined' || + primaryColorText.trim() === '' + ) { + primaryColorText = null; + } let secondaryColor = localStorage.getItem('secondaryColor'); if (secondaryColor == null || secondaryColor === 'undefined' || secondaryColor.trim() === '') { secondaryColor = null; } + let secondaryColorText = localStorage.getItem('secondaryColorText'); + if ( + secondaryColorText == null || + secondaryColorText === 'undefined' || + secondaryColorText.trim() === '' + ) { + secondaryColorText = null; + } document.documentElement.style.setProperty('--q-primary', primaryColor ?? '#1976d2'); + document.documentElement.style.setProperty('--q-primary-text', primaryColorText ?? '#ffffff'); document.documentElement.style.setProperty('--q-secondary', secondaryColor ?? '#26a69a'); + document.documentElement.style.setProperty('--q-secondary-text', secondaryColorText ?? '#ffffff'); }); diff --git a/src/components/EditOneDialog.vue b/src/components/EditOneDialog.vue index c719bbf..8da98e4 100644 --- a/src/components/EditOneDialog.vue +++ b/src/components/EditOneDialog.vue @@ -29,6 +29,16 @@ const localTitle = ref(''); const localField = ref(''); const value = ref(''); +const props = defineProps({ + endpoint: { + type: String, + required: true, + }, + queryId: { + type: Boolean, + }, +}); + const emit = defineEmits(['update']); const { NotifyResponse } = useNotify(); @@ -42,7 +52,10 @@ function open(label: string, field: string, member: Member) { } function save() { - const query = 'secure/members/edit?id=' + localMember.value.id; + let query = props.endpoint; + if (props.queryId) { + query += '?id=' + localMember.value.id; + } let payload = {}; if (value.value === localMember.value[localField.value]) { @@ -50,13 +63,15 @@ function save() { return; } payload = { + id: localMember.value.id, [localField.value]: value.value, }; appApi .post(query, payload) - .then(() => { - emit('update', ''); + .then((resp) => { + emit('update'); + NotifyResponse(resp.data); dialog.value.close(); }) .catch((err) => { diff --git a/src/components/MemberEditAllDialog.vue b/src/components/MemberEditAllDialog.vue index 628d909..8df03d2 100644 --- a/src/components/MemberEditAllDialog.vue +++ b/src/components/MemberEditAllDialog.vue @@ -161,9 +161,9 @@ async function save() { if (!valid) return; - let query = 'secure/members/edit?id=' + localMember.value.id; + let query = 'members/edit?id=' + localMember.value.id; if (newMember.value) { - query = 'secure/members/add'; + query = 'members/add'; } appApi diff --git a/src/components/RoleEditAllDialog.vue b/src/components/RoleEditAllDialog.vue index 4ab489b..861de59 100644 --- a/src/components/RoleEditAllDialog.vue +++ b/src/components/RoleEditAllDialog.vue @@ -2,18 +2,29 @@ -
+
+ + {{ + $t('permissions') + }} + + +
Save @@ -27,29 +38,35 @@ 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'; +import PermissionsCheckBoxGroup from 'src/vueLib/checkboxes/CheckBoxGroupPermissions.vue'; +import { defaultPermissions } from 'src/vueLib/checkboxes/permissions'; +import { i18n } from 'src/boot/lang'; const { NotifyResponse } = useNotify(); const dialog = ref(); const newRole = ref(false); +const showRoleField = ref(true); const localRole = ref({ role: '', - rights: null, + permissions: [], }); -const emit = defineEmits(['update-role']); +const emit = defineEmits(['update']); -function open(role: Role | null) { +function open(role: Role | null, typ?: 'permissions') { if (role === undefined) { return; } + showRoleField.value = typ !== 'permissions'; if (role !== null) { localRole.value = role; + localRole.value.permissions = role.permissions || defaultPermissions; newRole.value = false; } else { localRole.value = { role: '', - rights: null, + permissions: defaultPermissions, }; newRole.value = true; } @@ -58,15 +75,23 @@ function open(role: Role | null) { } function save() { - let query = 'secure/roles/edit?id=' + localRole.value.id; + let query = 'roles/update?id=' + localRole.value.id; + let update = true; if (newRole.value) { - query = 'secure/roles/add'; + query = 'roles/add'; + update = false; + localRole.value.permissions = defaultPermissions; } appApi .post(query, JSON.stringify(localRole.value)) .then(() => { - emit('update-role'); + if (update) { + NotifyResponse( + i18n.global.t('role') + " '" + localRole.value.role + "' " + i18n.global.t('updated'), + ); + } + emit('update'); dialog.value.close(); }) .catch((err) => NotifyResponse(err, 'error')); diff --git a/src/components/UploadDialog.vue b/src/components/UploadDialog.vue index f0d7423..ee3627e 100644 --- a/src/components/UploadDialog.vue +++ b/src/components/UploadDialog.vue @@ -7,6 +7,7 @@ :url="`https://0.0.0.0:` + portApp + `/api/members/import/csv`" label="Import CSV" multiple + :with-credentials="true" accept=".csv" field-name="file" method="POST" diff --git a/src/components/UserEditAllDialog.vue b/src/components/UserEditAllDialog.vue index c1192bf..c207159 100644 --- a/src/components/UserEditAllDialog.vue +++ b/src/components/UserEditAllDialog.vue @@ -5,36 +5,100 @@ :height="600" :width="500" > -
- - - - -
+ +
+ + +
+ + + + +
+ +
+ {{ strengthLabel }} +
+
+ + + +
+ +
+ + +
+
+
Save
@@ -44,13 +108,25 @@ diff --git a/src/pages/SettingsPage.vue b/src/pages/SettingsPage.vue index 8337731..c2b63e0 100644 --- a/src/pages/SettingsPage.vue +++ b/src/pages/SettingsPage.vue @@ -8,6 +8,7 @@

{{ $t('general') }}

{{ $t('database') }}

{{ $t('primaryColor') }}

- + +
+
+

+ {{ $t('primaryColorText') }} +

+

{{ $t('secondaryColor') }}

- + +
+
+

+ {{ $t('secondaryColorText') }} +

+
- {{ - $t('resetColors') - }} + {{ $t('resetColors') }}
@@ -83,15 +139,21 @@ import { reactive, ref, watch } from 'vue'; import { appApi } from 'src/boot/axios'; import { useNotify } from 'src/vueLib/general/useNotify'; import { type Settings } from 'src/vueLib/models/settings'; +import { useLogin } from 'src/vueLib/login/useLogin'; +import { useUserStore } from 'src/vueLib/login/userStore'; const { NotifyResponse } = useNotify(); +const { getUser } = useLogin(); const colorGroup = ref(false); +const user = useUserStore(); const settings = reactive({ icon: Logo.value, databaseName: databaseName.value, primaryColor: document.documentElement.style.getPropertyValue('--q-primary'), + primaryColorText: document.documentElement.style.getPropertyValue('--q-primary-text'), secondaryColor: document.documentElement.style.getPropertyValue('--q-secondary'), + secondaryColorText: document.documentElement.style.getPropertyValue('--q-secondary-text'), }); watch(settings, (newSettings) => { @@ -101,20 +163,30 @@ watch(settings, (newSettings) => { function resetColors() { document.documentElement.style.setProperty('--q-primary', '#1976d2'); + settings.primaryColor = '#1976d2'; + document.documentElement.style.setProperty('--q-primary-text', '#ffffff'); + settings.primaryColorText = '#ffffff'; document.documentElement.style.setProperty('--q-secondary', '#26a69a'); + settings.secondaryColor = '#26a69a'; + document.documentElement.style.setProperty('--q-secondary-text', '#ffffff'); + settings.secondaryColorText = '#ffffff'; } function save() { document.documentElement.style.setProperty('--q-primary', settings.primaryColor); + document.documentElement.style.setProperty('--q-primary-text', settings.primaryColorText); document.documentElement.style.setProperty('--q-secondary', settings.secondaryColor); + document.documentElement.style.setProperty('--q-secondary-text', settings.secondaryColorText); Logo.value = settings.icon; localStorage.setItem('icon', settings.icon); localStorage.setItem('databaseName', settings.databaseName); localStorage.setItem('primaryColor', settings.primaryColor); + localStorage.setItem('primaryColorText', settings.primaryColorText); localStorage.setItem('secondaryColor', settings.secondaryColor); + localStorage.setItem('secondaryColorText', settings.secondaryColorText); appApi - .post('secure/settings/update', { user: 'admin', settings }) + .post('settings/update', { user: getUser()?.username, settings }) .then((resp) => NotifyResponse(resp.data.message)) .catch((err) => NotifyResponse(err, 'error')); } diff --git a/src/router/index.ts b/src/router/index.ts index 2021e6a..e544b73 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -36,13 +36,13 @@ export default defineRouter(function (/* { store, ssrContext } */) { Router.beforeEach((to, from, next) => { const userStore = useUserStore(); - const isLoggedIn = userStore.isAuthenticated; - const isAdmin = userStore.user?.role === 'admin'; - if (to.meta.requiresAuth && !isLoggedIn) { next('/login'); - } else if (to.meta.requiresAdmin && !isAdmin) { + } else if ( + to.meta.requiresAdmin && + !userStore.isPermittedTo(to.path.replace('/', ''), 'read') + ) { next('/'); } else { next(); diff --git a/src/router/routes.ts b/src/router/routes.ts index 767373e..bb94809 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -10,21 +10,21 @@ const routes: RouteRecordRaw[] = [ component: () => import('pages/LoginPage.vue'), }, { - path: '/login', + path: 'login', component: () => import('pages/LoginPage.vue'), }, { - path: '/members', + path: 'members', component: () => import('pages/MembersTable.vue'), meta: { requiresAuth: true, requiresAdmin: true }, }, { - path: '/settings', + path: 'settings', component: () => import('pages/SettingsPage.vue'), meta: { requiresAuth: true, requiresAdmin: true }, }, { - path: '/usersSettings', + path: 'userSettings', component: () => import('pages/UserSettings.vue'), meta: { requiresAuth: true, requiresAdmin: true }, }, diff --git a/src/vueLib/checkboxes/CheckBoxGroupPermissions.vue b/src/vueLib/checkboxes/CheckBoxGroupPermissions.vue new file mode 100644 index 0000000..51c3c8c --- /dev/null +++ b/src/vueLib/checkboxes/CheckBoxGroupPermissions.vue @@ -0,0 +1,67 @@ + + + diff --git a/src/vueLib/checkboxes/permissions.ts b/src/vueLib/checkboxes/permissions.ts new file mode 100644 index 0000000..65b38ca --- /dev/null +++ b/src/vueLib/checkboxes/permissions.ts @@ -0,0 +1,40 @@ +import { i18n } from 'src/boot/lang'; +import { ref } from 'vue'; + +export interface Permission { + name: string; + label: string; + permission: number; +} + +export type Permissions = Permission[]; + +export const defaultPermissions = [ + { + name: 'settings', + label: i18n.global.t('settings'), + permission: 0, + }, + { + name: 'userSettings', + label: i18n.global.t('userSettings'), + permission: 0, + }, + { + name: 'members', + label: i18n.global.t('members'), + permission: 0, + }, + { + name: 'attendanceTable', + label: i18n.global.t('attendanceTable'), + permission: 0, + }, + { + name: 'excursionTable', + label: i18n.global.t('excursionTable'), + permission: 0, + }, +]; + +export const permissions = ref(defaultPermissions); diff --git a/src/vueLib/db/db.ts b/src/vueLib/db/db.ts deleted file mode 100644 index 89ac1ae..0000000 --- a/src/vueLib/db/db.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { CapacitorSQLite, SQLiteConnection } from '@capacitor-community/sqlite'; -import type { Settings } from '../models/settings'; - -const sqlite = new SQLiteConnection(CapacitorSQLite); - -export async function initDB() { - const db = await sqlite.createConnection('membersDB', true, 'secreto_passwordo', 1, false); - await db.open(); - await db.execute(`CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT NOT NULL, - role TEXT NOT NULL, - password TEXT NOT NULL, - settings TEXT NOT NULL - );`); - - const result = await db.query(`SELECT * FROM users WHERE username = ?`, ['admin']); - if (result.values?.length === 0) { - await db.run(`INSERT INTO users (username, role, password, settings) VALUES (?, ?, ?, ?)`, [ - 'admin', - 'admin', - 'tecamino@2023', - {}, - ]); - } - return db; -} - -export async function addUser(user: string, role: string, password: string, settings: Settings) { - const db = await initDB(); - await db.run(`INSERT INTO users (username, role, password, settings) VALUES (?, ?, ?, ?)`, [ - user, - role, - password, - settings, - ]); -} - -export async function getUsers() { - const db = await initDB(); - const resp = await db.query(`SELECT * FROM users`); - return resp.values; -} - -export async function getUser(user: string) { - const db = await initDB(); - const resp = await db.query(`SELECT EXISTS(SELECT 1 FROM users WHERE username = ?)`, [user]); - return resp.values; -} diff --git a/src/vueLib/login/LoginForm.vue b/src/vueLib/login/LoginForm.vue index 2109e73..67ad030 100644 --- a/src/vueLib/login/LoginForm.vue +++ b/src/vueLib/login/LoginForm.vue @@ -8,6 +8,7 @@ ref="refUserInput" dense filled + autocomplete="username" type="text" :label="$t('user')" v-model="user" @@ -16,6 +17,7 @@
- +
@@ -59,25 +64,24 @@ const { login } = useLogin(); const emit = defineEmits(['update-close']); -const onSubmit = () => { - refForm.value?.validate().then((success: boolean) => { - if (success) { - login(user.value, password.value) - .then(() => { - NotifyResponse("logged in as '" + user.value + "'"); - emit('update-close'); - }) - .catch((err) => { - NotifyResponse(err, 'error'); - shake.value = true; - setTimeout(() => { - shake.value = false; - }, 500); - }); - } else { - NotifyResponse('error submitting login form', 'error'); - } - }); +const onSubmit = async () => { + const valid = refForm.value?.validate(); + if (!valid) { + NotifyResponse('error submitting login form', 'error'); + return; + } + await login(user.value, password.value) + .then(() => { + NotifyResponse("logged in as '" + user.value + "'"); + }) + .catch((err) => { + NotifyResponse(err, 'error'); + shake.value = true; + setTimeout(() => { + shake.value = false; + }, 500); + }); + emit('update-close'); }; diff --git a/src/vueLib/login/LoginMenu.vue b/src/vueLib/login/LoginMenu.vue index 311b5ff..eb366de 100644 --- a/src/vueLib/login/LoginMenu.vue +++ b/src/vueLib/login/LoginMenu.vue @@ -9,6 +9,9 @@ {{ loginText }} + + + - + - - + + @@ -40,6 +52,7 @@ import { useNotify } from '../general/useNotify'; import { lang, i18n } from 'src/boot/lang'; import { useUserStore } from './userStore'; import { useRoute } from 'vue-router'; +import { Dark } from 'quasar'; const route = useRoute(); const showLogin = computed( diff --git a/src/vueLib/login/useLogin.ts b/src/vueLib/login/useLogin.ts index 363f93f..bea4d92 100644 --- a/src/vueLib/login/useLogin.ts +++ b/src/vueLib/login/useLogin.ts @@ -18,15 +18,21 @@ export function useLogin() { Logo.value = sets.icon; document.documentElement.style.setProperty('--q-primary', sets.primaryColor); + document.documentElement.style.setProperty('--q-primary-text', sets.primaryColorText); document.documentElement.style.setProperty('--q-secondary', sets.secondaryColor); + document.documentElement.style.setProperty('--q-secondary-text', sets.secondaryColorText); localStorage.setItem('icon', sets.icon); localStorage.setItem('databaseName', sets.databaseName); localStorage.setItem('primaryColor', sets.primaryColor); + localStorage.setItem('primaryColorText', sets.primaryColorText); localStorage.setItem('secondaryColor', sets.secondaryColor); + localStorage.setItem('secondaryColorText', sets.secondaryColorText); }); const resp = await appApi.get('/login/me'); - userStore.setUser({ id: resp.data.id, username: resp.data.user, role: resp.data.role }); + await userStore + .setUser({ id: resp.data.id, username: resp.data.user, role: resp.data.role }) + .catch((err) => console.log(err)); startRefreshInterval(); return true; @@ -47,12 +53,14 @@ export function useLogin() { async function refresh() { await appApi - .post('secure/login/refresh', {}, { withCredentials: true }) + .post('login/refresh', {}, { withCredentials: true }) .then(() => { appApi .get('/login/me') .then((resp) => { - userStore.setUser({ id: resp.data.id, username: resp.data.user, role: resp.data.role }); + userStore + .setUser({ id: resp.data.id, username: resp.data.user, role: resp.data.role }) + .catch((err) => console.error(err)); if (!intervalId) { startRefreshInterval(); } @@ -67,7 +75,7 @@ export function useLogin() { return false; } function getUser() { - return userStore.getUser(); + return userStore.user; } function startRefreshInterval() { diff --git a/src/vueLib/login/userStore.ts b/src/vueLib/login/userStore.ts index 5cb33ad..9fdd58b 100644 --- a/src/vueLib/login/userStore.ts +++ b/src/vueLib/login/userStore.ts @@ -1,30 +1,52 @@ import { defineStore } from 'pinia'; -import { useGlobalRouter } from 'src/utils/globalRouter'; -import { useGlobalQ } from 'src/utils/globalQ'; +import { useGlobalRouter } from 'src/vueLib/utils/globalRouter'; +import { useGlobalQ } from 'src/vueLib/utils/globalQ'; +import { appApi } from 'src/boot/axios'; +import { useNotify } from '../general/useNotify'; +import type { Role } from '../models/roles'; +import type { UserState, User } from '../models/user'; +import type { Permission } from '../checkboxes/permissions'; -interface User { - id: number; - username: string; - role: string; -} - -interface UserState { - user: User | null; -} +const { NotifyResponse } = useNotify(); export const useUserStore = defineStore('user', { state: (): UserState => ({ user: null, }), getters: { - isAuthenticated: (state): boolean => !!state.user, + isAuthenticated: (state: UserState): boolean => { + return !!state.user; + }, + + isAuthorizedAs: (state: UserState) => { + return (roles: string[]) => { + return state.user !== null && roles.includes(state.user.role); + }; + }, + isPermittedTo: (state: UserState) => { + return (name: string, type: 'read' | 'write' | 'delete'): boolean => { + const permission = state.user?.permissions?.find((r: Permission) => r.name === name); + switch (type) { + case 'read': + return permission?.permission ? (permission.permission & (1 << 0)) === 1 : false; + case 'write': + return permission?.permission ? (permission.permission & (1 << 1)) === 2 : false; + case 'delete': + return permission?.permission ? (permission.permission & (1 << 2)) === 4 : false; + } + }; + }, }, actions: { - setUser(user: User) { - this.user = user; - }, - getUser() { - return this.user; + async setUser(user: User) { + await appApi + .get('roles?role=' + user.role) + .then((resp) => { + const roleData = resp.data.find((role: Role) => role.role === user.role); + user.permissions = roleData?.permissions || []; + this.user = user; + }) + .catch((err) => NotifyResponse(err, 'error')); }, clearUser() { const $q = useGlobalQ(); @@ -60,9 +82,5 @@ export const useUserStore = defineStore('user', { } }); }, - - isAuthorizedAs(roles: string[]) { - return this.user !== null && roles.includes(this.user.role); - }, }, }); diff --git a/src/vueLib/models/rights.ts b/src/vueLib/models/rights.ts deleted file mode 100644 index be2d30c..0000000 --- a/src/vueLib/models/rights.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface Right { - name: string; - read: boolean; - write: boolean; - delete: boolean; -} - -export type Rights = Right[]; diff --git a/src/vueLib/models/roles.ts b/src/vueLib/models/roles.ts index 0e37ddb..b8169f4 100644 --- a/src/vueLib/models/roles.ts +++ b/src/vueLib/models/roles.ts @@ -1,9 +1,9 @@ -import type { Rights } from './rights'; +import type { Permissions } from '../checkboxes/permissions'; export interface Role { id?: number; role: string; - rights: Rights | null; + permissions: Permissions; } export type Roles = Role[]; diff --git a/src/vueLib/models/settings.ts b/src/vueLib/models/settings.ts index 1891369..ff11af4 100644 --- a/src/vueLib/models/settings.ts +++ b/src/vueLib/models/settings.ts @@ -2,5 +2,18 @@ export type Settings = { icon: string; databaseName: string; primaryColor: string; + primaryColorText: string; secondaryColor: string; + secondaryColorText: string; }; + +export function DefaultSettings(): Settings { + return { + icon: '', + databaseName: 'members.dba', + primaryColor: document.documentElement.style.getPropertyValue('--q-primary-text'), + primaryColorText: document.documentElement.style.getPropertyValue('--q-primary'), + secondaryColor: document.documentElement.style.getPropertyValue('--q-secondary'), + secondaryColorText: document.documentElement.style.getPropertyValue('--q-secondary-text'), + }; +} diff --git a/src/vueLib/models/user.ts b/src/vueLib/models/user.ts new file mode 100644 index 0000000..3078017 --- /dev/null +++ b/src/vueLib/models/user.ts @@ -0,0 +1,12 @@ +import type { Permissions } from '../checkboxes/permissions'; + +export interface User { + id: number; + username: string; + role: string; + permissions?: Permissions; +} + +export interface UserState { + user: User | null; +} diff --git a/src/vueLib/models/users.ts b/src/vueLib/models/users.ts index f8cb525..d1d0b5c 100644 --- a/src/vueLib/models/users.ts +++ b/src/vueLib/models/users.ts @@ -1,9 +1,13 @@ +import type { Settings } from './settings'; + export interface User { id?: number; user: string; email: string; role: string; expires: string; + password?: string; + settings?: Settings; } export type Users = User[]; diff --git a/src/vueLib/tables/members/MembersTable.ts b/src/vueLib/tables/members/MembersTable.ts index bfeeb01..09aacb0 100644 --- a/src/vueLib/tables/members/MembersTable.ts +++ b/src/vueLib/tables/members/MembersTable.ts @@ -13,7 +13,7 @@ export function useMemberTable() { sortBy: 'firstName', descending: false, page: 1, - rowsPerPage: 10, + rowsPerPage: 20, }); const columns = computed(() => [ @@ -171,7 +171,7 @@ export function useMemberTable() { loading.value = true; appApi - .get('secure/members') + .get('members') .then((resp) => { if (resp.data === null) { members.value = []; diff --git a/src/vueLib/tables/members/MembersTable.vue b/src/vueLib/tables/members/MembersTable.vue index 01a99f9..7ef8f07 100644 --- a/src/vueLib/tables/members/MembersTable.vue +++ b/src/vueLib/tables/members/MembersTable.vue @@ -23,10 +23,17 @@ >