started working on the backend
This commit is contained in:
@@ -3,13 +3,13 @@
|
|||||||
{
|
{
|
||||||
"name": "Go",
|
"name": "Go",
|
||||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
"image": "mcr.microsoft.com/devcontainers/go:1-1.23-bookworm"
|
"image": "mcr.microsoft.com/devcontainers/go:1-1.23-bookworm",
|
||||||
|
|
||||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
// "features": {},
|
// "features": {},
|
||||||
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
// "forwardPorts": [],
|
// "forwardPorts": [8080]
|
||||||
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
// "postCreateCommand": "go version",
|
// "postCreateCommand": "go version",
|
||||||
|
|||||||
15
backend/.vscode/launch.json
vendored
Normal file
15
backend/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch Package",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,3 +1,31 @@
|
|||||||
module github.com/johannesbuehl/golunteer/backend
|
module github.com/johannesbuehl/golunteer/backend
|
||||||
|
|
||||||
go 1.23.4
|
go 1.23.4
|
||||||
|
|
||||||
|
require github.com/go-sql-driver/mysql v1.8.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/jfarleyx/go-simple-cache v1.1.0 // indirect
|
||||||
|
github.com/jmoiron/sqlx v1.4.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.6
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
|
github.com/rs/zerolog v1.33.0
|
||||||
|
golang.org/x/crypto v0.32.0
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|||||||
54
backend/go.sum
Normal file
54
backend/go.sum
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
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/jfarleyx/go-simple-cache v1.1.0 h1:R3sEDKnZwyrXbXiu1UswmuNPUK15uOGaIkaJEqs3OmU=
|
||||||
|
github.com/jfarleyx/go-simple-cache v1.1.0/go.mod h1:inxrRa6HjKThMm7/gARxbNMId0DMDf27u5N1c0Nz0Bw=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
|
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||||
|
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||||
|
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||||
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
27
backend/logs/backend.log
Normal file
27
backend/logs/backend.log
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{"level":"debug","time":1736261973,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"error","time":1736261977,"message":"error while reading data for event with id 1: Error 1241 (21000): Operand should contain 1 column(s)"}
|
||||||
|
{"level":"debug","time":1736262010,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"debug","time":1736262031,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"debug","time":1736263786,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"error","time":1736263809,"message":"error while populating event with id = 1: Error 1054 (42S22): Unknown column 'USER_ASSIGNMENTS.userID' in 'ON'"}
|
||||||
|
{"level":"debug","time":1736263844,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"error","time":1736263850,"message":"error while populating event with id = 1: Error 1054 (42S22): Unknown column 'USERS.id' in 'ON'"}
|
||||||
|
{"level":"debug","time":1736263865,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"error","time":1736263865,"message":"error while populating event with id = 1: missing destination name eventID in *[]assignments.assignemntDB"}
|
||||||
|
{"level":"debug","time":1736263897,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"error","time":1736263911,"message":"error while populating event with id = 1: missing destination name eventID in *[]assignments.assignemntDB"}
|
||||||
|
{"level":"debug","time":1736263915,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"debug","time":1736263949,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"debug","time":1736264022,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"debug","time":1736264029,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"debug","time":1736264046,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"debug","time":1736264049,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"debug","time":1736264080,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"debug","time":1736264124,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"debug","time":1736264162,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"debug","time":1736264165,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"debug","time":1736264204,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"debug","time":1736264212,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"debug","time":1736264299,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"debug","time":1736264321,"message":"HTTP GET request: \"/api/events\""}
|
||||||
|
{"level":"debug","time":1736264349,"message":"HTTP GET request: \"/api/events\""}
|
||||||
9
backend/main.go
Normal file
9
backend/main.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/johannesbuehl/golunteer/backend/pkg/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
router.Listen()
|
||||||
|
}
|
||||||
95
backend/pkg/config/config.go
Normal file
95
backend/pkg/config/config.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/johannesbuehl/golunteer/backend/pkg/lib"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReservationConfig struct {
|
||||||
|
Expiration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigStruct struct {
|
||||||
|
ConfigYaml
|
||||||
|
LogLevel zerolog.Level
|
||||||
|
SessionExpire time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var Config ConfigStruct
|
||||||
|
|
||||||
|
type Payload struct {
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
CustomClaims map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config ConfigStruct) SignJWT(val any) (string, error) {
|
||||||
|
valMap, err := lib.StrucToMap(val)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := Payload{
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(config.SessionExpire)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
},
|
||||||
|
CustomClaims: valMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
t := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
|
||||||
|
|
||||||
|
return t.SignedString([]byte(config.ClientSession.JwtSignature))
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig() ConfigStruct {
|
||||||
|
Config := ConfigYaml{}
|
||||||
|
|
||||||
|
yamlFile, err := os.ReadFile("config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("Error opening config-file: %q", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := bytes.NewReader(yamlFile)
|
||||||
|
|
||||||
|
dec := yaml.NewDecoder(reader)
|
||||||
|
dec.KnownFields(true)
|
||||||
|
err = dec.Decode(&Config)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error parsing config-file: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if logLevel, err := zerolog.ParseLevel(Config.LogLevel); err != nil {
|
||||||
|
panic(fmt.Errorf("can't parse log-level: %v", err))
|
||||||
|
} else {
|
||||||
|
var configStruct ConfigStruct
|
||||||
|
|
||||||
|
// parse the durations
|
||||||
|
if session_expire, err := time.ParseDuration(Config.ClientSession.Expire); err != nil {
|
||||||
|
log.Fatalf(`Error parsing "client_session.expire": %v`, err)
|
||||||
|
|
||||||
|
// parse the templates
|
||||||
|
} else {
|
||||||
|
configStruct = ConfigStruct{
|
||||||
|
ConfigYaml: Config,
|
||||||
|
LogLevel: logLevel,
|
||||||
|
SessionExpire: session_expire,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return configStruct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Config = loadConfig()
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -19,6 +19,9 @@ type ConfigYaml struct {
|
|||||||
Password string `yaml:"password"`
|
Password string `yaml:"password"`
|
||||||
Database string `yaml:"database"`
|
Database string `yaml:"database"`
|
||||||
} `yaml:"database"`
|
} `yaml:"database"`
|
||||||
|
Server struct {
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
} `yaml:"server"`
|
||||||
ClientSession struct {
|
ClientSession struct {
|
||||||
JwtSignature string `yaml:"jwt_signature"`
|
JwtSignature string `yaml:"jwt_signature"`
|
||||||
Expire string `yaml:"expire"`
|
Expire string `yaml:"expire"`
|
||||||
@@ -30,9 +33,9 @@ type CacheConfig struct {
|
|||||||
Purge time.Duration
|
Purge time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
var config ConfigYaml
|
var YamlConfig ConfigYaml
|
||||||
|
|
||||||
func loadConfig() ConfigYaml {
|
func _loadConfig() ConfigYaml {
|
||||||
config := ConfigYaml{}
|
config := ConfigYaml{}
|
||||||
|
|
||||||
yamlFile, err := os.ReadFile(CONFIG_PATH)
|
yamlFile, err := os.ReadFile(CONFIG_PATH)
|
||||||
@@ -53,12 +56,12 @@ func loadConfig() ConfigYaml {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeConfig() {
|
func WriteConfig() {
|
||||||
buf := bytes.Buffer{}
|
buf := bytes.Buffer{}
|
||||||
enc := yaml.NewEncoder(&buf)
|
enc := yaml.NewEncoder(&buf)
|
||||||
enc.SetIndent(2)
|
enc.SetIndent(2)
|
||||||
// Can set default indent here on the encoder
|
// Can set default indent here on the encoder
|
||||||
if err := enc.Encode(&config); err != nil {
|
if err := enc.Encode(&YamlConfig); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
} else {
|
} else {
|
||||||
if err := os.WriteFile(CONFIG_PATH, buf.Bytes(), 0644); err != nil {
|
if err := os.WriteFile(CONFIG_PATH, buf.Bytes(), 0644); err != nil {
|
||||||
@@ -68,5 +71,5 @@ func writeConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
config = loadConfig()
|
YamlConfig = _loadConfig()
|
||||||
}
|
}
|
||||||
30
backend/pkg/db/assignments/assignments.go
Normal file
30
backend/pkg/db/assignments/assignments.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package assignments
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type assignments map[string]string
|
||||||
|
|
||||||
|
type assignemntDB struct {
|
||||||
|
TaskName string `db:"taskName"`
|
||||||
|
UserName string `db:"userName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Event(eventID int) (assignments, error) {
|
||||||
|
// get the assignments from the database
|
||||||
|
var assignmentRows []assignemntDB
|
||||||
|
|
||||||
|
if err := db.DB.Select(&assignmentRows, "SELECT USERS.name AS userName, TASKS.text AS taskName FROM USER_ASSIGNMENTS JOIN USERS ON USER_ASSIGNMENTS.userName = USERS.name LEFT JOIN TASKS ON USER_ASSIGNMENTS.taskID = TASKS.id WHERE USER_ASSIGNMENTS.eventID = ?", eventID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
// transform the rows into the returned map
|
||||||
|
eventAssignments := assignments{}
|
||||||
|
|
||||||
|
for _, assignment := range assignmentRows {
|
||||||
|
eventAssignments[assignment.TaskName] = assignment.UserName
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventAssignments, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
59
backend/pkg/db/availabilites/availabilities.go
Normal file
59
backend/pkg/db/availabilites/availabilities.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package availabilites
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
cache "github.com/jfarleyx/go-simple-cache"
|
||||||
|
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type availabilitesDB struct {
|
||||||
|
Id int `db:"id"`
|
||||||
|
Text string `db:"text"`
|
||||||
|
Disabled bool `db:"disabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Availability struct {
|
||||||
|
Text string
|
||||||
|
Disabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var c *cache.Cache
|
||||||
|
|
||||||
|
func Keys() (map[int]Availability, error) {
|
||||||
|
if availabilities, hit := c.Get("availabilites"); !hit {
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("availabilites not stored cached")
|
||||||
|
} else {
|
||||||
|
return availabilities.(map[int]Availability), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func refresh() {
|
||||||
|
// get the availabilitesRaw from the database
|
||||||
|
var availabilitesRaw []availabilitesDB
|
||||||
|
|
||||||
|
if err := db.DB.Select(&availabilitesRaw, "SELECT * FROM AVAILABILITIES"); err == nil {
|
||||||
|
// convert the result in a map
|
||||||
|
availabilites := map[int]Availability{}
|
||||||
|
|
||||||
|
for _, a := range availabilitesRaw {
|
||||||
|
availabilites[a.Id] = Availability{
|
||||||
|
Text: a.Text,
|
||||||
|
Disabled: a.Disabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("availabilites", availabilites)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
c = cache.New(24 * time.Hour)
|
||||||
|
|
||||||
|
c.OnExpired(refresh)
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
36
backend/pkg/db/availabilites/userAvailabilities.go
Normal file
36
backend/pkg/db/availabilites/userAvailabilities.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package availabilites
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||||
|
"github.com/johannesbuehl/golunteer/backend/pkg/db/users"
|
||||||
|
)
|
||||||
|
|
||||||
|
type eventAvailabilites struct {
|
||||||
|
userName string `db:"userName"`
|
||||||
|
AvailabilityID int `db:"availabilityID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Event(eventID int) (map[string]string, error) {
|
||||||
|
// get the availabilites for the event
|
||||||
|
var availabilitesRows []eventAvailabilites
|
||||||
|
|
||||||
|
if err := db.DB.Select(&availabilitesRows, "SELECT (userID, availabilityID) FROM USER_AVAILABILITES WHERE eventID = ?", eventID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
// transform the result into a map
|
||||||
|
eventAvailabilities := map[string]string{}
|
||||||
|
|
||||||
|
// get the availabilites
|
||||||
|
if availabilitesMap, err := Keys(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if usersMap, err := users.Get(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
for _, a := range availabilitesRows {
|
||||||
|
eventAvailabilities[usersMap[a.userName].Name] = availabilitesMap[a.AvailabilityID].Text
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventAvailabilities, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
225
backend/pkg/db/db.go
Normal file
225
backend/pkg/db/db.go
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-sql-driver/mysql"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
_config "github.com/johannesbuehl/golunteer/backend/pkg/config"
|
||||||
|
_logger "github.com/johannesbuehl/golunteer/backend/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger = _logger.Logger
|
||||||
|
var config = _config.Config
|
||||||
|
|
||||||
|
// connection to database
|
||||||
|
var DB *sqlx.DB
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// setup the database-connection
|
||||||
|
sqlConfig := mysql.Config{
|
||||||
|
AllowNativePasswords: true,
|
||||||
|
Net: "tcp",
|
||||||
|
User: config.Database.User,
|
||||||
|
Passwd: config.Database.Password,
|
||||||
|
Addr: config.Database.Host,
|
||||||
|
DBName: config.Database.Database,
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect to the database
|
||||||
|
DB = sqlx.MustOpen("mysql", sqlConfig.FormatDSN())
|
||||||
|
DB.SetMaxIdleConns(10)
|
||||||
|
DB.SetMaxIdleConns(100)
|
||||||
|
DB.SetConnMaxLifetime(time.Minute)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// query the database
|
||||||
|
func SelectOld[T any](table string, where string, args ...any) ([]T, error) {
|
||||||
|
// validate columns against struct T
|
||||||
|
tType := reflect.TypeOf(new(T)).Elem()
|
||||||
|
columns := make([]string, tType.NumField())
|
||||||
|
|
||||||
|
validColumns := make(map[string]any)
|
||||||
|
for ii := 0; ii < tType.NumField(); ii++ {
|
||||||
|
field := tType.Field(ii)
|
||||||
|
validColumns[strings.ToLower(field.Name)] = struct{}{}
|
||||||
|
columns[ii] = strings.ToLower(field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, col := range columns {
|
||||||
|
if _, ok := validColumns[strings.ToLower(col)]; !ok {
|
||||||
|
return nil, fmt.Errorf("invalid column: %s for struct type %T", col, new(T))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the query
|
||||||
|
completeQuery := fmt.Sprintf("SELECT %s FROM %s", strings.Join(columns, ", "), table)
|
||||||
|
|
||||||
|
if where != "" && where != "*" {
|
||||||
|
completeQuery = fmt.Sprintf("%s WHERE %s", completeQuery, where)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows *sql.Rows
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
DB.Ping()
|
||||||
|
|
||||||
|
rows, err = DB.Query(completeQuery, args...)
|
||||||
|
} else {
|
||||||
|
DB.Ping()
|
||||||
|
|
||||||
|
rows, err = DB.Query(completeQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Msgf("database access failed with error %v", err)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
results := []T{}
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var lineResult T
|
||||||
|
|
||||||
|
scanArgs := make([]any, len(columns))
|
||||||
|
v := reflect.ValueOf(&lineResult).Elem()
|
||||||
|
|
||||||
|
for ii, col := range columns {
|
||||||
|
field := v.FieldByName(col)
|
||||||
|
|
||||||
|
if field.IsValid() && field.CanSet() {
|
||||||
|
scanArgs[ii] = field.Addr().Interface()
|
||||||
|
} else {
|
||||||
|
logger.Warn().Msgf("Field %s not found in struct %T", col, lineResult)
|
||||||
|
scanArgs[ii] = new(any) // save dummy value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scan the row into the struct
|
||||||
|
if err := rows.Scan(scanArgs...); err != nil {
|
||||||
|
logger.Warn().Msgf("Scan-error: %v", err)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, lineResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
logger.Error().Msgf("rows-error: %v", err)
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert data intot the databse
|
||||||
|
func Insert(table string, vals any) error {
|
||||||
|
// extract columns from vals
|
||||||
|
v := reflect.ValueOf(vals)
|
||||||
|
t := v.Type()
|
||||||
|
|
||||||
|
columns := make([]string, t.NumField())
|
||||||
|
values := make([]any, t.NumField())
|
||||||
|
|
||||||
|
for ii := 0; ii < t.NumField(); ii++ {
|
||||||
|
fieldValue := v.Field(ii)
|
||||||
|
|
||||||
|
field := t.Field(ii)
|
||||||
|
|
||||||
|
columns[ii] = strings.ToLower(field.Name)
|
||||||
|
values[ii] = fieldValue.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
placeholders := strings.Repeat(("?, "), len(columns))
|
||||||
|
placeholders = strings.TrimSuffix(placeholders, ", ")
|
||||||
|
|
||||||
|
completeQuery := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", table, strings.Join(columns, ", "), placeholders)
|
||||||
|
|
||||||
|
_, err := DB.Exec(completeQuery, values...)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update data in the database
|
||||||
|
func Update(table string, set, where any) error {
|
||||||
|
setV := reflect.ValueOf(set)
|
||||||
|
setT := setV.Type()
|
||||||
|
|
||||||
|
setColumns := make([]string, setT.NumField())
|
||||||
|
setValues := make([]any, setT.NumField())
|
||||||
|
|
||||||
|
for ii := 0; ii < setT.NumField(); ii++ {
|
||||||
|
fieldValue := setV.Field(ii)
|
||||||
|
|
||||||
|
field := setT.Field(ii)
|
||||||
|
|
||||||
|
setColumns[ii] = strings.ToLower(field.Name) + " = ?"
|
||||||
|
setValues[ii] = fieldValue.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
whereV := reflect.ValueOf(where)
|
||||||
|
whereT := whereV.Type()
|
||||||
|
|
||||||
|
whereColumns := make([]string, whereT.NumField())
|
||||||
|
whereValues := make([]any, whereT.NumField())
|
||||||
|
|
||||||
|
for ii := 0; ii < whereT.NumField(); ii++ {
|
||||||
|
fieldValue := whereV.Field(ii)
|
||||||
|
|
||||||
|
// skip empty (zero) values
|
||||||
|
if !fieldValue.IsZero() {
|
||||||
|
field := whereT.Field(ii)
|
||||||
|
|
||||||
|
whereColumns[ii] = strings.ToLower(field.Name) + " = ?"
|
||||||
|
whereValues[ii] = fmt.Sprint(fieldValue.Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sets := strings.Join(setColumns, ", ")
|
||||||
|
wheres := strings.Join(whereColumns, " AND ")
|
||||||
|
|
||||||
|
placeholderValues := append(setValues, whereValues...)
|
||||||
|
|
||||||
|
completeQuery := fmt.Sprintf("UPDATE %s SET %s WHERE %s", table, sets, wheres)
|
||||||
|
|
||||||
|
_, err := DB.Exec(completeQuery, placeholderValues...)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove data from the database
|
||||||
|
func Delete(table string, vals any) error {
|
||||||
|
// extract columns from vals
|
||||||
|
v := reflect.ValueOf(vals)
|
||||||
|
t := v.Type()
|
||||||
|
|
||||||
|
columns := make([]string, t.NumField())
|
||||||
|
values := make([]any, t.NumField())
|
||||||
|
|
||||||
|
for ii := 0; ii < t.NumField(); ii++ {
|
||||||
|
fieldValue := v.Field(ii)
|
||||||
|
|
||||||
|
// skip empty (zero) values
|
||||||
|
if !fieldValue.IsZero() {
|
||||||
|
field := t.Field(ii)
|
||||||
|
|
||||||
|
columns[ii] = strings.ToLower(field.Name) + " = ?"
|
||||||
|
values[ii] = fmt.Sprint(fieldValue.Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completeQuery := fmt.Sprintf("DELETE FROM %s WHERE %s", table, strings.Join(columns, ", "))
|
||||||
|
|
||||||
|
_, err := DB.Exec(completeQuery, values...)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
48
backend/pkg/db/events/events.go
Normal file
48
backend/pkg/db/events/events.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||||
|
"github.com/johannesbuehl/golunteer/backend/pkg/db/assignments"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
eventDataDB
|
||||||
|
Tasks []string
|
||||||
|
Assignments map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type eventDataDB struct {
|
||||||
|
Id int `db:"id"`
|
||||||
|
Date string `db:"date"`
|
||||||
|
Description string `db:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// transform the database-entry to an Event
|
||||||
|
func (e *eventDataDB) Event() (Event, error) {
|
||||||
|
// get the availabilites associated with the event
|
||||||
|
if assignemnts, err := assignments.Event(e.Id); err != nil {
|
||||||
|
return Event{}, err
|
||||||
|
} else {
|
||||||
|
return Event{
|
||||||
|
eventDataDB: *e,
|
||||||
|
Assignments: assignemnts,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all the event ids
|
||||||
|
func All() (map[int]eventDataDB, error) {
|
||||||
|
var dbRows []eventDataDB
|
||||||
|
|
||||||
|
if err := db.DB.Select(&dbRows, "SELECT * FROM EVENTS"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
eventsMap := map[int]eventDataDB{}
|
||||||
|
|
||||||
|
for _, idRow := range dbRows {
|
||||||
|
eventsMap[idRow.Id] = idRow
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventsMap, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
59
backend/pkg/db/tasks/tasks.go
Normal file
59
backend/pkg/db/tasks/tasks.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package tasks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
cache "github.com/jfarleyx/go-simple-cache"
|
||||||
|
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tasksDB struct {
|
||||||
|
Id int `db:"id"`
|
||||||
|
Text string `db:"text"`
|
||||||
|
Disabled bool `db:"disabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Task struct {
|
||||||
|
Text string
|
||||||
|
Disabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var c *cache.Cache
|
||||||
|
|
||||||
|
func Get() (map[int]Task, error) {
|
||||||
|
if tasks, hit := c.Get("tasks"); !hit {
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("tasks not stored cached")
|
||||||
|
} else {
|
||||||
|
return tasks.(map[int]Task), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func refresh() {
|
||||||
|
// get the tasksRaw from the database
|
||||||
|
var tasksRaw []tasksDB
|
||||||
|
|
||||||
|
if err := db.DB.Select(&tasksRaw, "SELECT * FROM TASKS"); err == nil {
|
||||||
|
// convert the result in a map
|
||||||
|
tasks := map[int]Task{}
|
||||||
|
|
||||||
|
for _, a := range tasksRaw {
|
||||||
|
tasks[a.Id] = Task{
|
||||||
|
Text: a.Text,
|
||||||
|
Disabled: a.Disabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("tasks", tasks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
c = cache.New(24 * time.Hour)
|
||||||
|
|
||||||
|
c.OnExpired(refresh)
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
52
backend/pkg/db/users/users.go
Normal file
52
backend/pkg/db/users/users.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package users
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
cache "github.com/jfarleyx/go-simple-cache"
|
||||||
|
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Name string `db:"text"`
|
||||||
|
Password []byte `db:"password"`
|
||||||
|
TokenID string `db:"tokenID"`
|
||||||
|
Admin bool `db:"disabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var c *cache.Cache
|
||||||
|
|
||||||
|
func Get() (map[string]User, error) {
|
||||||
|
if users, hit := c.Get("users"); !hit {
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("users not stored cached")
|
||||||
|
} else {
|
||||||
|
return users.(map[string]User), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func refresh() {
|
||||||
|
// get the usersRaw from the database
|
||||||
|
var usersRaw []User
|
||||||
|
|
||||||
|
if err := db.DB.Select(&usersRaw, "SELECT * FROM USERS"); err == nil {
|
||||||
|
// convert the result in a map
|
||||||
|
users := map[string]User{}
|
||||||
|
|
||||||
|
for _, user := range usersRaw {
|
||||||
|
users[user.Name] = user
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("users", users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
c = cache.New(24 * time.Hour)
|
||||||
|
|
||||||
|
c.OnExpired(refresh)
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
30
backend/pkg/lib/lib.go
Normal file
30
backend/pkg/lib/lib.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StrucToMap(data any) (map[string]any, error) {
|
||||||
|
result := make(map[string]any)
|
||||||
|
|
||||||
|
v := reflect.ValueOf(data)
|
||||||
|
|
||||||
|
if v.Kind() != reflect.Struct {
|
||||||
|
return nil, fmt.Errorf("expected a struct but got %T", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
for ii := 0; ii < v.NumField(); ii++ {
|
||||||
|
field := v.Type().Field(ii)
|
||||||
|
value := v.Field(ii)
|
||||||
|
|
||||||
|
// skip unexported fields
|
||||||
|
if field.PkgPath != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result[field.Tag.Get("json")] = value.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
70
backend/pkg/logger/logger.go
Normal file
70
backend/pkg/logger/logger.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/johannesbuehl/golunteer/backend/pkg/config"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Logger zerolog.Logger
|
||||||
|
|
||||||
|
type specificLevelWriter struct {
|
||||||
|
io.Writer
|
||||||
|
Level zerolog.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w specificLevelWriter) WriteLevel(l zerolog.Level, p []byte) (int, error) {
|
||||||
|
if l >= w.Level {
|
||||||
|
return w.Write(p)
|
||||||
|
} else {
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// try to set the log-level
|
||||||
|
zerolog.SetGlobalLevel(config.Config.LogLevel)
|
||||||
|
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||||
|
|
||||||
|
// create the console output
|
||||||
|
outputConsole := zerolog.ConsoleWriter{
|
||||||
|
Out: os.Stdout,
|
||||||
|
TimeFormat: time.DateTime,
|
||||||
|
FormatLevel: func(i interface{}) string {
|
||||||
|
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
|
||||||
|
},
|
||||||
|
FormatFieldName: func(i interface{}) string {
|
||||||
|
return fmt.Sprintf("%s", i)
|
||||||
|
},
|
||||||
|
NoColor: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the logfile output
|
||||||
|
outputLog := &lumberjack.Logger{
|
||||||
|
Filename: "logs/backend.log",
|
||||||
|
MaxAge: 7,
|
||||||
|
LocalTime: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a multi-output-writer
|
||||||
|
multi := zerolog.MultiLevelWriter(
|
||||||
|
specificLevelWriter{
|
||||||
|
Writer: outputConsole,
|
||||||
|
Level: config.Config.LogLevel,
|
||||||
|
},
|
||||||
|
specificLevelWriter{
|
||||||
|
Writer: outputLog,
|
||||||
|
Level: config.Config.LogLevel,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// create a logger-instance
|
||||||
|
Logger = zerolog.New(multi).With().Timestamp().Logger()
|
||||||
|
|
||||||
|
}
|
||||||
33
backend/pkg/router/events.go
Normal file
33
backend/pkg/router/events.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/johannesbuehl/golunteer/backend/pkg/db/events"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getEvents(c *fiber.Ctx) responseMessage {
|
||||||
|
response := responseMessage{}
|
||||||
|
|
||||||
|
// get all eventRows
|
||||||
|
if eventRows, err := events.All(); err != nil {
|
||||||
|
response.Status = fiber.StatusInternalServerError
|
||||||
|
|
||||||
|
logger.Error().Msgf("events retrieving failed: %v", err)
|
||||||
|
} else {
|
||||||
|
// get the data for all the allEvents
|
||||||
|
allEvents := []events.Event{}
|
||||||
|
|
||||||
|
for _, eventRow := range eventRows {
|
||||||
|
if e, err := eventRow.Event(); err != nil {
|
||||||
|
logger.Error().Msgf("error while populating event with id = %d: %v", eventRow.Id, err)
|
||||||
|
} else {
|
||||||
|
allEvents = append(allEvents, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// response.Data = struct{ Events []events.Event }{Events: allEvents}
|
||||||
|
response.Data = allEvents
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
80
backend/pkg/router/login.go
Normal file
80
backend/pkg/router/login.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserLogin struct {
|
||||||
|
UserID int `json:"userID"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
LoggedIn bool `json:"loggedIn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle welcome-messages from clients
|
||||||
|
func handleWelcome(c *fiber.Ctx) error {
|
||||||
|
logger.Debug().Msgf("HTTP %s request: %q", c.Method(), c.OriginalURL())
|
||||||
|
|
||||||
|
response := responseMessage{}
|
||||||
|
response.Data = UserLogin{
|
||||||
|
LoggedIn: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok, err := checkUser(c); err != nil {
|
||||||
|
response.Status = fiber.StatusInternalServerError
|
||||||
|
|
||||||
|
logger.Warn().Msgf("can't check user: %v", err)
|
||||||
|
} else if !ok {
|
||||||
|
response.Status = fiber.StatusNoContent
|
||||||
|
} else {
|
||||||
|
if uid, _, err := extractJWT(c); err != nil {
|
||||||
|
response.Status = fiber.StatusBadRequest
|
||||||
|
|
||||||
|
logger.Error().Msgf("can't extract JWT: %v", err)
|
||||||
|
} else {
|
||||||
|
if users, err := db.SelectOld[UserDB]("users", "uid = ? LIMIT 1", strconv.Itoa(uid)); err != nil {
|
||||||
|
response.Status = fiber.StatusInternalServerError
|
||||||
|
|
||||||
|
logger.Error().Msgf("can't get users from database: %v", err)
|
||||||
|
} else {
|
||||||
|
if len(users) != 1 {
|
||||||
|
response.Status = fiber.StatusForbidden
|
||||||
|
response.Message = "unknown user"
|
||||||
|
|
||||||
|
removeSessionCookie(c)
|
||||||
|
} else {
|
||||||
|
user := users[0]
|
||||||
|
|
||||||
|
response.Data = UserLogin{
|
||||||
|
UserID: user.UserID,
|
||||||
|
Name: user.Name,
|
||||||
|
LoggedIn: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug().Msgf("welcomed user with uid = %v", uid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.send(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleLogin(c *fiber.Ctx) error {
|
||||||
|
panic("not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
// handles logout-requests
|
||||||
|
func handleLogout(c *fiber.Ctx) error {
|
||||||
|
logger.Debug().Msgf("HTTP %s request: %q", c.Method(), c.OriginalURL())
|
||||||
|
|
||||||
|
removeSessionCookie(c)
|
||||||
|
|
||||||
|
return responseMessage{
|
||||||
|
Data: UserLogin{
|
||||||
|
LoggedIn: false,
|
||||||
|
},
|
||||||
|
}.send(c)
|
||||||
|
}
|
||||||
221
backend/pkg/router/router.go
Normal file
221
backend/pkg/router/router.go
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
_config "github.com/johannesbuehl/golunteer/backend/pkg/config"
|
||||||
|
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||||
|
_logger "github.com/johannesbuehl/golunteer/backend/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger = _logger.Logger
|
||||||
|
var config = _config.Config
|
||||||
|
|
||||||
|
var app *fiber.App
|
||||||
|
|
||||||
|
// general message for REST-responses
|
||||||
|
type responseMessage struct {
|
||||||
|
Status int
|
||||||
|
Message string
|
||||||
|
Data any
|
||||||
|
}
|
||||||
|
|
||||||
|
// answer the client request with the response-message
|
||||||
|
func (result responseMessage) send(c *fiber.Ctx) error {
|
||||||
|
// if the status-code is in the error-region, return an error
|
||||||
|
if result.Status >= 400 {
|
||||||
|
// if available, include the message
|
||||||
|
if result.Message != "" {
|
||||||
|
return fiber.NewError(result.Status, result.Message)
|
||||||
|
} else {
|
||||||
|
return fiber.NewError(result.Status)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if there is data, send it as JSON
|
||||||
|
if result.Data != nil {
|
||||||
|
c.JSON(result.Data)
|
||||||
|
|
||||||
|
// if there is a message, send it instead
|
||||||
|
} else if result.Message != "" {
|
||||||
|
c.SendString(result.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.SendStatus(result.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// setup fiber
|
||||||
|
app = fiber.New(fiber.Config{
|
||||||
|
AppName: "johannes-pv",
|
||||||
|
DisableStartupMessage: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// map with the individual methods
|
||||||
|
handleMethods := map[string]func(path string, handlers ...func(*fiber.Ctx) error) fiber.Router{
|
||||||
|
"GET": app.Get,
|
||||||
|
"POST": app.Post,
|
||||||
|
"PATCH": app.Patch,
|
||||||
|
"DELETE": app.Delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
// map with the individual registered endpoints
|
||||||
|
endpoints := map[string]map[string]func(*fiber.Ctx) responseMessage{
|
||||||
|
"GET": {"events": getEvents},
|
||||||
|
"POST": {},
|
||||||
|
"PATCH": {},
|
||||||
|
"DELETE": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle specific requests special
|
||||||
|
app.Get("/api/welcome", handleWelcome)
|
||||||
|
app.Post("/api/login", handleLogin)
|
||||||
|
app.Get("/api/logout", handleLogout)
|
||||||
|
|
||||||
|
// register the registered endpoints
|
||||||
|
for method, handlers := range endpoints {
|
||||||
|
for address, handler := range handlers {
|
||||||
|
handleMethods[method]("/api/"+address, func(c *fiber.Ctx) error {
|
||||||
|
logger.Debug().Msgf("HTTP %s request: %q", c.Method(), c.OriginalURL())
|
||||||
|
|
||||||
|
return handler(c).send(c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Listen() {
|
||||||
|
// start the server
|
||||||
|
err := app.Listen(fmt.Sprintf(":%d", config.Server.Port))
|
||||||
|
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSessionCookie(c *fiber.Ctx, jwt *string) {
|
||||||
|
var value string
|
||||||
|
|
||||||
|
if jwt == nil {
|
||||||
|
value = c.Cookies("session")
|
||||||
|
} else {
|
||||||
|
value = *jwt
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Cookie(&fiber.Cookie{
|
||||||
|
Name: "session",
|
||||||
|
Value: value,
|
||||||
|
HTTPOnly: true,
|
||||||
|
SameSite: "strict",
|
||||||
|
MaxAge: int(config.SessionExpire.Seconds()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes the session-coockie from a request
|
||||||
|
func removeSessionCookie(c *fiber.Ctx) {
|
||||||
|
c.Cookie(&fiber.Cookie{
|
||||||
|
Name: "session",
|
||||||
|
Value: "",
|
||||||
|
HTTPOnly: true,
|
||||||
|
SameSite: "strict",
|
||||||
|
Expires: time.Unix(0, 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// payload of the JSON webtoken
|
||||||
|
type JWTPayload struct {
|
||||||
|
UserID int `json:"userID"`
|
||||||
|
TokenID string `json:"tokenID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// complete JSON webtoken
|
||||||
|
type JWT struct {
|
||||||
|
_config.Payload
|
||||||
|
CustomClaims JWTPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
// extracts the json webtoken from the request
|
||||||
|
//
|
||||||
|
// @returns (userID, tokenID, error)
|
||||||
|
func extractJWT(c *fiber.Ctx) (int, string, error) {
|
||||||
|
// get the session-cookie
|
||||||
|
cookie := c.Cookies("session")
|
||||||
|
|
||||||
|
token, err := jwt.ParseWithClaims(cookie, &JWT{}, func(token *jwt.Token) (any, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected JWT signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(config.ClientSession.JwtSignature), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return -1, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract the claims from the JWT
|
||||||
|
if claims, ok := token.Claims.(*JWT); ok && token.Valid {
|
||||||
|
return claims.CustomClaims.UserID, claims.CustomClaims.TokenID, nil
|
||||||
|
} else {
|
||||||
|
return -1, "", fmt.Errorf("invalid JWT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// user-entry in the database
|
||||||
|
type UserDB struct {
|
||||||
|
UserID int `json:"userID"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Password []byte `json:"password"`
|
||||||
|
Admin bool `json:"admin"`
|
||||||
|
TokenID string `json:"tokenID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks wether the request is from a valid user
|
||||||
|
func checkUser(c *fiber.Ctx) (bool, error) {
|
||||||
|
uid, tid, err := extractJWT(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrieve the user from the database
|
||||||
|
response, err := db.SelectOld[UserDB]("users", "uid = ? LIMIT 1", uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if exactly one user came back and the tID is valid, the user is authorized
|
||||||
|
if len(response) == 1 && response[0].TokenID == tid {
|
||||||
|
// reset the expiration of the cookie
|
||||||
|
setSessionCookie(c, nil)
|
||||||
|
|
||||||
|
return true, err
|
||||||
|
} else {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks wether the request is from the admin
|
||||||
|
func checkAdmin(c *fiber.Ctx) (bool, error) {
|
||||||
|
uid, tokenID, err := extractJWT(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrieve the user from the database
|
||||||
|
response, err := db.SelectOld[UserDB]("users", "uid = ? LIMIT 1", uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if exactly one user came back and its name is "admin", the user is the admin
|
||||||
|
if len(response) != 1 {
|
||||||
|
return false, fmt.Errorf("user doesn't exist")
|
||||||
|
} else {
|
||||||
|
return response[0].Name == "admin" && response[0].TokenID == tokenID, err
|
||||||
|
}
|
||||||
|
}
|
||||||
13
backend/pkg/router/user.go
Normal file
13
backend/pkg/router/user.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import "golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
// hashes a password
|
||||||
|
func hashPassword(password string) ([]byte, error) {
|
||||||
|
return bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validates a password against the password-rules
|
||||||
|
func validatePassword(password string) bool {
|
||||||
|
return len(password) >= 12 && len(password) <= 64
|
||||||
|
}
|
||||||
@@ -2,7 +2,17 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
output: "export"
|
// output: "export",
|
||||||
|
rewrites: async () => ({
|
||||||
|
beforeFiles: [],
|
||||||
|
afterFiles: [],
|
||||||
|
fallback: [
|
||||||
|
{
|
||||||
|
source: "/api/:path*",
|
||||||
|
destination: "http://golunteer-frontend:8080/api/:path*",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
54
client/package-lock.json
generated
54
client/package-lock.json
generated
@@ -15,10 +15,12 @@
|
|||||||
"@nextui-org/checkbox": "^2.3.8",
|
"@nextui-org/checkbox": "^2.3.8",
|
||||||
"@nextui-org/date-picker": "^2.3.9",
|
"@nextui-org/date-picker": "^2.3.9",
|
||||||
"@nextui-org/divider": "^2.2.5",
|
"@nextui-org/divider": "^2.2.5",
|
||||||
|
"@nextui-org/form": "^2.1.8",
|
||||||
"@nextui-org/input": "^2.4.8",
|
"@nextui-org/input": "^2.4.8",
|
||||||
"@nextui-org/modal": "^2.2.7",
|
"@nextui-org/modal": "^2.2.7",
|
||||||
"@nextui-org/radio": "^2.3.8",
|
"@nextui-org/radio": "^2.3.8",
|
||||||
"@nextui-org/select": "^2.4.9",
|
"@nextui-org/select": "^2.4.9",
|
||||||
|
"@nextui-org/switch": "^2.2.8",
|
||||||
"@nextui-org/system": "^2.4.6",
|
"@nextui-org/system": "^2.4.6",
|
||||||
"@nextui-org/table": "^2.2.8",
|
"@nextui-org/table": "^2.2.8",
|
||||||
"@nextui-org/theme": "^2.4.5",
|
"@nextui-org/theme": "^2.4.5",
|
||||||
@@ -1498,6 +1500,30 @@
|
|||||||
"react-dom": ">=18 || >=19.0.0-rc.0"
|
"react-dom": ">=18 || >=19.0.0-rc.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nextui-org/switch": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nextui-org/switch/-/switch-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-wk9qQSOfUEtmdWR1omKjmEYzgMjJhVizvfW6Z0rKOiMUuSud2d4xYnUmZhU22cv2WtoPV//kBjXkYD/E/t6rdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@nextui-org/react-utils": "2.1.3",
|
||||||
|
"@nextui-org/shared-utils": "2.1.2",
|
||||||
|
"@nextui-org/use-safe-layout-effect": "2.1.1",
|
||||||
|
"@react-aria/focus": "3.19.0",
|
||||||
|
"@react-aria/interactions": "3.22.5",
|
||||||
|
"@react-aria/switch": "3.6.10",
|
||||||
|
"@react-aria/utils": "3.26.0",
|
||||||
|
"@react-aria/visually-hidden": "3.8.18",
|
||||||
|
"@react-stately/toggle": "3.8.0",
|
||||||
|
"@react-types/shared": "3.26.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nextui-org/system": ">=2.4.0",
|
||||||
|
"@nextui-org/theme": ">=2.4.3",
|
||||||
|
"react": ">=18 || >=19.0.0-rc.0",
|
||||||
|
"react-dom": ">=18 || >=19.0.0-rc.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nextui-org/system": {
|
"node_modules/@nextui-org/system": {
|
||||||
"version": "2.4.6",
|
"version": "2.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/@nextui-org/system/-/system-2.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/@nextui-org/system/-/system-2.4.6.tgz",
|
||||||
@@ -2179,6 +2205,22 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-aria/switch": {
|
||||||
|
"version": "3.6.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-aria/switch/-/switch-3.6.10.tgz",
|
||||||
|
"integrity": "sha512-FtaI9WaEP1tAmra1sYlAkYXg9x75P5UtgY8pSbe9+1WRyWbuE1QZT+RNCTi3IU4fZ7iJQmXH6+VaMyzPlSUagw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-aria/toggle": "^3.10.10",
|
||||||
|
"@react-stately/toggle": "^3.8.0",
|
||||||
|
"@react-types/shared": "^3.26.0",
|
||||||
|
"@react-types/switch": "^3.5.7",
|
||||||
|
"@swc/helpers": "^0.5.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-aria/table": {
|
"node_modules/@react-aria/table": {
|
||||||
"version": "3.16.0",
|
"version": "3.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/@react-aria/table/-/table-3.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/@react-aria/table/-/table-3.16.0.tgz",
|
||||||
@@ -2759,6 +2801,18 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-types/switch": {
|
||||||
|
"version": "3.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-types/switch/-/switch-3.5.7.tgz",
|
||||||
|
"integrity": "sha512-1IKiq510rPTHumEZuhxuazuXBa2Cuxz6wBIlwf3NCVmgWEvU+uk1ETG0sH2yymjwCqhtJDKXi+qi9HSgPEDwAg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-types/shared": "^3.26.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-types/table": {
|
"node_modules/@react-types/table": {
|
||||||
"version": "3.10.3",
|
"version": "3.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/@react-types/table/-/table-3.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/@react-types/table/-/table-3.10.3.tgz",
|
||||||
|
|||||||
@@ -16,10 +16,12 @@
|
|||||||
"@nextui-org/checkbox": "^2.3.8",
|
"@nextui-org/checkbox": "^2.3.8",
|
||||||
"@nextui-org/date-picker": "^2.3.9",
|
"@nextui-org/date-picker": "^2.3.9",
|
||||||
"@nextui-org/divider": "^2.2.5",
|
"@nextui-org/divider": "^2.2.5",
|
||||||
|
"@nextui-org/form": "^2.1.8",
|
||||||
"@nextui-org/input": "^2.4.8",
|
"@nextui-org/input": "^2.4.8",
|
||||||
"@nextui-org/modal": "^2.2.7",
|
"@nextui-org/modal": "^2.2.7",
|
||||||
"@nextui-org/radio": "^2.3.8",
|
"@nextui-org/radio": "^2.3.8",
|
||||||
"@nextui-org/select": "^2.4.9",
|
"@nextui-org/select": "^2.4.9",
|
||||||
|
"@nextui-org/switch": "^2.2.8",
|
||||||
"@nextui-org/system": "^2.4.6",
|
"@nextui-org/system": "^2.4.6",
|
||||||
"@nextui-org/table": "^2.2.8",
|
"@nextui-org/table": "^2.2.8",
|
||||||
"@nextui-org/theme": "^2.4.5",
|
"@nextui-org/theme": "^2.4.5",
|
||||||
|
|||||||
@@ -7,10 +7,25 @@ import AddEvent from "../components/Event/AddEvent";
|
|||||||
import zustand from "../Zustand";
|
import zustand from "../Zustand";
|
||||||
import { Button } from "@nextui-org/button";
|
import { Button } from "@nextui-org/button";
|
||||||
import AssignmentTable from "@/components/Event/AssignmentTable";
|
import AssignmentTable from "@/components/Event/AssignmentTable";
|
||||||
|
import { useAsyncList } from "@react-stately/data";
|
||||||
|
import { apiCall } from "@/lib";
|
||||||
|
|
||||||
export default function EventVolunteer() {
|
export default function EventVolunteer() {
|
||||||
const [showAddItemDialogue, setShowAddItemDialogue] = useState(false);
|
const [showAddItemDialogue, setShowAddItemDialogue] = useState(false);
|
||||||
|
|
||||||
|
// fetch the events from the server
|
||||||
|
useAsyncList({
|
||||||
|
load: async () => {
|
||||||
|
const data = await apiCall("GET", "events");
|
||||||
|
|
||||||
|
console.debug(await data.json());
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex-1 p-4">
|
<div className="relative flex-1 p-4">
|
||||||
<h2 className="mb-4 text-center text-4xl">Overview</h2>
|
<h2 className="mb-4 text-center text-4xl">Overview</h2>
|
||||||
|
|||||||
@@ -40,8 +40,4 @@
|
|||||||
h6 {
|
h6 {
|
||||||
@apply font-headline text-highlight;
|
@apply font-headline text-highlight;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
|
||||||
@apply border-2 border-accent-1 bg-transparent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,54 @@
|
|||||||
import EventVolunteer from "./Overview";
|
"use client";
|
||||||
|
|
||||||
|
import { Input } from "@nextui-org/input";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ViewFilled, ViewOffFilled } from "@carbon/icons-react";
|
||||||
|
import { Switch } from "@nextui-org/switch";
|
||||||
|
import { Button } from "@nextui-org/button";
|
||||||
|
import { Form } from "@nextui-org/form";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return <EventVolunteer />;
|
const [visibility, setVisibility] = useState(false);
|
||||||
|
|
||||||
|
// return <EventVolunteer />;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-4 text-center text-4xl">Login</h2>
|
||||||
|
<Form
|
||||||
|
validationBehavior="native"
|
||||||
|
className="flex flex-col items-center gap-2"
|
||||||
|
onSubmit={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
isRequired
|
||||||
|
type="user"
|
||||||
|
label="Name"
|
||||||
|
name="username"
|
||||||
|
variant="bordered"
|
||||||
|
className="max-w-xs"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
isRequired
|
||||||
|
label="Password"
|
||||||
|
name="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
endContent={
|
||||||
|
<Switch
|
||||||
|
className="my-auto"
|
||||||
|
startContent={<ViewFilled />}
|
||||||
|
endContent={<ViewOffFilled />}
|
||||||
|
onValueChange={setVisibility}
|
||||||
|
isSelected={visibility}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
type={visibility ? "text" : "password"}
|
||||||
|
variant="bordered"
|
||||||
|
className="max-w-xs"
|
||||||
|
/>
|
||||||
|
<Button className="w-full max-w-xs" color="primary" type="submit">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
56
client/src/lib.ts
Normal file
56
client/src/lib.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
type QueryParams = Record<string, string | { toString(): string }>;
|
||||||
|
|
||||||
|
export type APICallResult<T extends object> = Response & { json: () => Promise<T> };
|
||||||
|
|
||||||
|
export async function apiCall<K extends object>(
|
||||||
|
method: "GET",
|
||||||
|
api: string,
|
||||||
|
params?: QueryParams
|
||||||
|
): Promise<APICallResult<K>>;
|
||||||
|
export async function apiCall<K extends object>(
|
||||||
|
method: "POST" | "PATCH",
|
||||||
|
api: string,
|
||||||
|
params?: QueryParams,
|
||||||
|
body?: object
|
||||||
|
): Promise<APICallResult<K>>;
|
||||||
|
export async function apiCall<K extends object>(
|
||||||
|
method: "DELETE",
|
||||||
|
api: string,
|
||||||
|
params?: QueryParams,
|
||||||
|
body?: object
|
||||||
|
): Promise<APICallResult<K>>;
|
||||||
|
export async function apiCall<K extends object>(
|
||||||
|
method: "GET" | "POST" | "PATCH" | "DELETE",
|
||||||
|
api: string,
|
||||||
|
params?: QueryParams,
|
||||||
|
body?: object
|
||||||
|
): Promise<APICallResult<K>> {
|
||||||
|
let url = window.origin + "/api/" + api;
|
||||||
|
|
||||||
|
if (params) {
|
||||||
|
const urlsearchparams = new URLSearchParams(
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(params).map(([key, value]): [string, string] => {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return [key, value.toString()];
|
||||||
|
} else {
|
||||||
|
return [key, value];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
url += "?" + urlsearchparams.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json; charset=UTF-8"
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
method,
|
||||||
|
body: body !== undefined ? JSON.stringify(body) : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
@@ -24,16 +24,16 @@ export default {
|
|||||||
foreground: {
|
foreground: {
|
||||||
DEFAULT: FOREGROUND,
|
DEFAULT: FOREGROUND,
|
||||||
"50": FOREGROUND,
|
"50": FOREGROUND,
|
||||||
"100": "#fce8ff",
|
"100": FOREGROUND,
|
||||||
"200": "#fad0fe",
|
"200": FOREGROUND,
|
||||||
"300": "#f8abfc",
|
"300": FOREGROUND,
|
||||||
"400": "#f579f9",
|
"400": FOREGROUND,
|
||||||
"500": "#eb46ef",
|
"500": FOREGROUND,
|
||||||
"600": "#d226d3",
|
"600": "#fce8ff",
|
||||||
"700": "#af1cad",
|
"700": "#fad0fe",
|
||||||
"800": "#8f198c",
|
"800": "#f8abfc",
|
||||||
"900": "#751a70",
|
"900": "#f579f9",
|
||||||
"950": "#4e044a",
|
"950": "#eb46ef",
|
||||||
},
|
},
|
||||||
"accent-1": ACCENT1,
|
"accent-1": ACCENT1,
|
||||||
"accent-2": ACCENT2,
|
"accent-2": ACCENT2,
|
||||||
|
|||||||
9
init.sql
Normal file
9
init.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
INSERT INTO TASKS (text) VALUES ("Ton"), ("Livestream"), ("Kamera"), ("Licht"), ("Livestream Ton");
|
||||||
|
|
||||||
|
INSERT INTO AVAILABILITIES (text) VALUES ("Ja"), ("Eventuell"), ("Nein");
|
||||||
|
|
||||||
|
INSERT INTO EVENTS(date, description) VALUES ("2025-01-05T11:00", "Neuer Prädikant");
|
||||||
|
|
||||||
|
INSERT INTO USER_AVAILABILITIES (userName, eventID, availabilityID) VALUES ("admin", 1, 1);
|
||||||
|
|
||||||
|
INSERT INTO USER_ASSIGNMENTS (eventID, taskID, userName) VALUES (1, 1, "admin");
|
||||||
1
setup/.gitignore
vendored
1
setup/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
config.yaml
|
config.yaml
|
||||||
|
passwords
|
||||||
1
setup/pkg/config/config.go
Symbolic link
1
setup/pkg/config/config.go
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../backend/pkg/config/configYAML.go
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-sql-driver/mysql"
|
"github.com/go-sql-driver/mysql"
|
||||||
|
_config "github.com/johannesbuehl/golunteer/setup/pkg/config"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,6 +30,8 @@ func exit(e error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
config := &_config.YamlConfig
|
||||||
|
|
||||||
fmt.Println("connecting to database")
|
fmt.Println("connecting to database")
|
||||||
|
|
||||||
// connect to the database
|
// connect to the database
|
||||||
@@ -114,5 +117,5 @@ func main() {
|
|||||||
config.ClientSession.JwtSignature = createPassword(100)
|
config.ClientSession.JwtSignature = createPassword(100)
|
||||||
|
|
||||||
// write the modified config-file
|
// write the modified config-file
|
||||||
writeConfig()
|
_config.WriteConfig()
|
||||||
}
|
}
|
||||||
@@ -11,10 +11,10 @@ CREATE TABLE AVAILABILITIES (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE USERS (
|
CREATE TABLE USERS (
|
||||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
name varchar(64) PRIMARY KEY,
|
||||||
name varchar(64) NOT NULL,
|
|
||||||
password binary(60) NOT NULL,
|
password binary(60) NOT NULL,
|
||||||
admin BOOL NOT NULL DEFAULT(false)
|
admin BOOL NOT NULL DEFAULT(false),
|
||||||
|
tokenID varchar(64) DEFAULT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE EVENTS (
|
CREATE TABLE EVENTS (
|
||||||
@@ -24,21 +24,21 @@ CREATE TABLE EVENTS (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE USER_AVAILABILITIES (
|
CREATE TABLE USER_AVAILABILITIES (
|
||||||
userID INTEGER NOT NULL,
|
userName varchar(64) NOT NULL,
|
||||||
eventID INTEGER NOT NULL,
|
eventID INTEGER NOT NULL,
|
||||||
availabilityID INTEGER NOT NULL,
|
availabilityID INTEGER NOT NULL,
|
||||||
PRIMARY KEY (userID, eventID),
|
PRIMARY KEY (userName, eventID),
|
||||||
FOREIGN KEY (userID) REFERENCES USERS(id) ON DELETE CASCADE,
|
FOREIGN KEY (userName) REFERENCES USERS(name) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
FOREIGN KEY (eventID) REFERENCES EVENTS(id) ON DELETE CASCADE,
|
FOREIGN KEY (eventID) REFERENCES EVENTS(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
FOREIGN KEY (avaibID) REFERENCES AVAILABILITIES(id) ON DELETE RESTRICT,
|
FOREIGN KEY (availabilityID) REFERENCES AVAILABILITIES(id) ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE USER_ASSIGNMENTS (
|
CREATE TABLE USER_ASSIGNMENTS (
|
||||||
eventID INTEGER NOT NULL,
|
eventID INTEGER NOT NULL,
|
||||||
taskID INTEGER NOT NULL,
|
taskID INTEGER NOT NULL,
|
||||||
userID INTEGER NOT NULL,
|
userName varchar(64),
|
||||||
PRIMARY KEY (eventID, taskID),
|
PRIMARY KEY (eventID, taskID),
|
||||||
FOREIGN KEY (userID) REFERENCES USERS(id) ON DELETE CASCADE,
|
FOREIGN KEY (eventID) REFERENCES EVENTS(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (taskID) REFERENCES TASKS(id) ON DELETE RESTRICT,
|
FOREIGN KEY (userName) REFERENCES USERS(name) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
FOREIGN KEY (eventID) REFERENCES EVENTS(id) ON DELETE CASCADE
|
FOREIGN KEY (taskID) REFERENCES TASKS(id) ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
Reference in New Issue
Block a user