added adding and editing of availabilities

This commit is contained in:
z1glr
2025-01-18 13:25:12 +00:00
parent e37310b774
commit e17c9db90c
24 changed files with 737 additions and 163 deletions

3
backend/.gitignore vendored
View File

@@ -1,3 +1,4 @@
config.yaml
logs
__debug_bin*
__debug_bin*
database.db

View File

@@ -6,6 +6,7 @@ require github.com/go-sql-driver/mysql v1.8.1
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
@@ -17,6 +18,8 @@ require (
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/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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
@@ -24,6 +27,9 @@ require (
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
)
require (
@@ -35,4 +41,5 @@ require (
golang.org/x/crypto v0.32.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.34.5
)

View File

@@ -3,6 +3,8 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4
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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -38,7 +40,11 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
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=
@@ -66,3 +72,11 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST
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=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=

View File

@@ -50,10 +50,10 @@ func (config ConfigStruct) SignJWT(val any) (string, error) {
return t.SignedString([]byte(config.ClientSession.JwtSignature))
}
func loadConfig() ConfigStruct {
func LoadConfig() ConfigStruct {
Config := ConfigYaml{}
yamlFile, err := os.ReadFile("config.yaml")
yamlFile, err := os.ReadFile(CONFIG_PATH)
if err != nil {
panic(fmt.Sprintf("Error opening config-file: %q", err))
}
@@ -91,5 +91,5 @@ func loadConfig() ConfigStruct {
}
func init() {
Config = loadConfig()
Config = LoadConfig()
}

View File

@@ -13,13 +13,8 @@ var CONFIG_PATH = "config.yaml"
type ConfigYaml struct {
LogLevel string `yaml:"log_level"`
Database struct {
Host string `yaml:"host"`
User string `yaml:"user"`
Password string `yaml:"password"`
Database string `yaml:"database"`
} `yaml:"database"`
Server struct {
Database string `yaml:"database"`
Server struct {
Port int `yaml:"port"`
} `yaml:"server"`
ClientSession struct {

View File

@@ -1,59 +1,58 @@
package availabilities
import (
"fmt"
"time"
cache "github.com/jfarleyx/go-simple-cache"
"github.com/johannesbuehl/golunteer/backend/pkg/db"
)
type availabilitiesDB struct {
Id int `db:"id"`
Text string `db:"text"`
Disabled bool `db:"disabled"`
type AvailabilityDB struct {
Id int `db:"id" json:"id" validate:"required"`
Availability
}
type Availability struct {
Text string
Disabled bool
Text string `db:"text" json:"text" validate:"required"`
Enabled bool `db:"enabled" json:"enabled" validate:"required"`
Color string `db:"color" json:"color" validate:"required"`
}
var c *cache.Cache
func Add(a Availability) error {
_, err := db.DB.NamedExec("INSERT INTO AVAILABILITIES (text, color, enabled) VALUES (:text, :color, :enabled)", a)
func Keys() (map[int]Availability, error) {
if availabilities, hit := c.Get("availabilities"); !hit {
refresh()
return err
}
return nil, fmt.Errorf("availabilities not stored cached")
func Update(a AvailabilityDB) error {
_, err := db.DB.NamedExec("UPDATE AVAILABILITIES SET text = :text, color = :color, enabled = :enabled WHERE id = :id", a)
return err
}
func Slice() ([]AvailabilityDB, error) {
// get the availabilitiesRaw from the database
var availabilitiesRaw []AvailabilityDB
if err := db.DB.Select(&availabilitiesRaw, "SELECT * FROM AVAILABILITIES"); err != nil {
return nil, err
} else {
return availabilities.(map[int]Availability), nil
return availabilitiesRaw, nil
}
}
func refresh() {
// get the availabilitiesRaw from the database
var availabilitiesRaw []availabilitiesDB
if err := db.DB.Select(&availabilitiesRaw, "SELECT * FROM AVAILABILITIES"); err == nil {
func Keys() (map[int]Availability, error) {
if availabilitiesRaw, err := Slice(); err != nil {
return nil, err
} else {
// convert the result in a map
availabilities := map[int]Availability{}
for _, a := range availabilitiesRaw {
availabilities[a.Id] = Availability{
Text: a.Text,
Disabled: a.Disabled,
Text: a.Text,
Enabled: a.Enabled,
Color: a.Color,
}
}
c.Set("availabilities", availabilities)
return availabilities, nil
}
}
func init() {
c = cache.New(24 * time.Hour)
c.OnExpired(refresh)
refresh()
}

View File

@@ -1,11 +1,13 @@
package db
import (
"time"
"database/sql"
"fmt"
"os"
"github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
_config "github.com/johannesbuehl/golunteer/backend/pkg/config"
_ "modernc.org/sqlite" // SQLite driver
)
var config = _config.Config
@@ -14,20 +16,31 @@ var config = _config.Config
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)
DB = sqlx.MustOpen("sqlite", config.Database)
// create the tables if they don't exist
if dbSetupInstructions, err := os.ReadFile("setup.sql"); err != nil {
panic("can't read database-setup")
} else {
DB.MustExec(string(dbSetupInstructions))
// take wether the admin-user is present as an indicator to a new instance
var admin struct {
Admin bool `db:"admin"`
}
if err := DB.QueryRowx("SELECT admin FROM USERS WHERE name = 'admin'").StructScan(&admin); err != nil {
// if the error isn't because there was no result, it's a real one
if err != sql.ErrNoRows {
panic(fmt.Errorf("can't query for the admin-user: %v", err))
} else {
// setup everything
setup()
fmt.Println("setup completed")
// reload the config
_config.LoadConfig()
}
}
}
}

80
backend/pkg/db/setup.go Normal file
View File

@@ -0,0 +1,80 @@
package db
import (
"bytes"
"fmt"
"math/rand/v2"
"os"
"github.com/google/uuid"
_config "github.com/johannesbuehl/golunteer/backend/pkg/config"
"golang.org/x/crypto/bcrypt"
"gopkg.in/yaml.v3"
)
func createPassword(l int) string {
passwordChars := [...]string{`A`, `B`, `C`, `D`, `E`, `F`, `G`, `H`, `I`, `J`, `K`, `L`, `M`, `N`, `O`, `P`, `Q`, `R`, `S`, `T`, `U`, `V`, `W`, `X`, `Y`, `Z`, `Ä`, `Ö`, `Ü`, `a`, `b`, `c`, `d`, `e`, `f`, `g`, `h`, `i`, `j`, `k`, `l`, `m`, `n`, `o`, `p`, `q`, `r`, `s`, `t`, `u`, `v`, `w`, `x`, `y`, `z`, `ä`, `ö`, `ü`, `ß`, `0`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9`, `!`, `"`, `§`, `$`, `%`, `&`, `/`, `(`, `)`, `=`, `?`, `@`, `{`, `}`, `[`, `]`, `#`, `+`, `'`, `*`, `,`, `.`, `-`, `;`, `:`, `_`, `<`, `>`, `|`, `°`}
var password string
for ii := 0; ii < l; ii++ {
password += passwordChars[rand.IntN(len(passwordChars))]
}
return password
}
func setup() {
fmt.Println("Creating admin-password:")
// create an admin-password
const passwordLength = 20
password := createPassword(passwordLength)
// hash the admin-password
if passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost); err != nil {
panic(fmt.Errorf("can't generate password: %v", err))
} else {
fmt.Println("\thashed password")
// create an admin-user
user := struct {
Name string `db:"name"`
Password []byte `db:"password"`
Admin bool `db:"admin"`
TokenID string `db:"tokenID"`
}{
Name: "admin",
Password: passwordHash,
Admin: true,
TokenID: uuid.NewString(),
}
if _, err := DB.NamedExec("INSERT INTO USERS (name, password, tokenID, admin) VALUES (:name, :password, :tokenID, :admin)", &user); err != nil {
panic(fmt.Errorf("can't insert admin-user into the database: %v", err))
}
fmt.Println("\twrote hashed password to database")
}
fmt.Printf("created user \"admin\" with password %s\n", password)
// create a jwt-signature
config.ClientSession.JwtSignature = createPassword(100)
// write the modified config-file
WriteConfig()
}
func WriteConfig() {
buf := bytes.Buffer{}
enc := yaml.NewEncoder(&buf)
enc.SetIndent(2)
// Can set default indent here on the encoder
if err := enc.Encode(&config.ConfigYaml); err != nil {
panic(err)
} else {
if err := os.WriteFile(_config.CONFIG_PATH, buf.Bytes(), 0644); err != nil {
panic(err)
}
}
}

View File

@@ -9,14 +9,14 @@ import (
)
type tasksDB struct {
ID int `db:"id"`
Text string `db:"text"`
Disabled bool `db:"disabled"`
ID int `db:"id"`
Text string `db:"text"`
Enabled bool `db:"enabled"`
}
type Task struct {
Text string `json:"text"`
Disabled bool `json:"disabled"`
Text string `json:"text"`
Enabled bool `json:"enabled"`
}
var c *cache.Cache
@@ -41,8 +41,8 @@ func refresh() {
for _, a := range tasksRaw {
tasks[a.ID] = Task{
Text: a.Text,
Disabled: a.Disabled,
Text: a.Text,
Enabled: a.Enabled,
}
}

View File

@@ -37,7 +37,11 @@ func init() {
Out: os.Stdout,
TimeFormat: time.DateTime,
FormatLevel: func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
if i == nil {
return "| LOG |"
} else {
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
}
},
FormatFieldName: func(i interface{}) string {
return fmt.Sprintf("%s", i)

View File

@@ -0,0 +1,107 @@
package router
import (
"github.com/gofiber/fiber/v2"
"github.com/johannesbuehl/golunteer/backend/pkg/db/availabilities"
)
func getAvailabilities(args HandlerArgs) responseMessage {
response := responseMessage{}
// get all the availabilites from the database
if avails, err := availabilities.Slice(); err != nil {
response.Status = fiber.StatusInternalServerError
logger.Error().Msgf("can't get availabilites: %v", err)
return response
} else {
response.Data = struct {
Availabilities []availabilities.AvailabilityDB `json:"availabilities"`
}{Availabilities: avails}
return response
}
}
func postAvailabilitie(args HandlerArgs) responseMessage {
response := responseMessage{}
// check admin
if !args.User.Admin {
response.Status = fiber.StatusUnauthorized
logger.Warn().Msg("user is no admin")
return response
// parse the body
} else {
var body availabilities.Availability
if err := args.C.BodyParser(&body); err != nil {
response.Status = fiber.StatusBadRequest
logger.Log().Msgf("can't parse body: %v", err)
return response
// validate the body
} else if err := validate.Struct(&response); err != nil {
response.Status = fiber.StatusBadRequest
logger.Log().Msgf("invalid body: %v", err)
return response
} else if err := availabilities.Add(body); err != nil {
response.Status = fiber.StatusInternalServerError
logger.Error().Msgf("can't add availability: %v", err)
return response
} else {
return response
}
}
}
func patchAvailabilities(args HandlerArgs) responseMessage {
response := responseMessage{}
// check admin
if !args.User.Admin {
response.Status = fiber.StatusUnauthorized
logger.Warn().Msg("user is no admin")
return response
// parse the body
} else {
var body availabilities.AvailabilityDB
if err := args.C.BodyParser(&body); err != nil {
response.Status = fiber.StatusBadRequest
logger.Log().Msgf("can't parse body: %v", err)
return response
// validate the body
} else if err := validate.Struct(&response); err != nil {
response.Status = fiber.StatusBadRequest
logger.Log().Msgf("invalid body: %v", err)
return response
} else if err := availabilities.Update(body); err != nil {
response.Status = fiber.StatusInternalServerError
logger.Error().Msgf("can't update availability: %v", err)
return response
} else {
return response
}
}
}

View File

@@ -81,14 +81,17 @@ func init() {
"events/user/pending": getEventsUserPending,
"tasks": getTasks,
"users": getUsers,
"availabilities": getAvailabilities,
},
"POST": {
"events": postEvent,
"users": postUser,
"events": postEvent,
"users": postUser,
"availabilities": postAvailabilitie,
},
"PATCH": {
"users": patchUser,
"events": patchEvent,
"users": patchUser,
"events": patchEvent,
"availabilities": patchAvailabilities,
},
"PUT": {
"users/password": putPassword,

47
backend/setup.sql Normal file
View File

@@ -0,0 +1,47 @@
CREATE TABLE IF NOT EXISTS TASKS (
id INTEGER PRIMARY KEY,
text varchar(64) NOT NULL,
enabled BOOL DEFAULT(true)
);
CREATE TABLE IF NOT EXISTS AVAILABILITIES (
id INTEGER PRIMARY KEY,
text varchar(32) NOT NULL,
color varchar(7) NOT NULL,
enabled BOOL DEFAULT(true)
);
CREATE TABLE IF NOT EXISTS USERS (
name varchar(64) PRIMARY KEY,
password binary(60) NOT NULL,
admin BOOL NOT NULL DEFAULT(false),
tokenID varchar(64) NOT NULL,
CHECK (length(password) = 60),
CHECK (length(tokenID) = 36)
);
CREATE TABLE IF NOT EXISTS EVENTS (
id INTEGER PRIMARY KEY,
date DATETIME NOT NULL,
description TEXT DEFAULT ""
);
CREATE TABLE IF NOT EXISTS USER_AVAILABILITIES (
userName varchar(64) NOT NULL,
eventID INTEGER NOT NULL,
availabilityID INTEGER NOT NULL,
PRIMARY KEY (userName, eventID),
FOREIGN KEY (userName) REFERENCES USERS(name) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (eventID) REFERENCES EVENTS(id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (availabilityID) REFERENCES AVAILABILITIES(id) ON UPDATE CASCADE
);
CREATE TABLE IF NOT EXISTS USER_ASSIGNMENTS (
eventID INTEGER NOT NULL,
taskID INTEGER NOT NULL,
userName varchar(64),
PRIMARY KEY (eventID, taskID),
FOREIGN KEY (eventID) REFERENCES EVENTS(id) ON DELETE CASCADE,
FOREIGN KEY (userName) REFERENCES USERS(name) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (taskID) REFERENCES TASKS(id) ON UPDATE CASCADE
);