added adding and editing of availabilities
This commit is contained in:
3
backend/.gitignore
vendored
3
backend/.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
config.yaml
|
config.yaml
|
||||||
logs
|
logs
|
||||||
__debug_bin*
|
__debug_bin*
|
||||||
|
database.db
|
||||||
@@ -6,6 +6,7 @@ require github.com/go-sql-driver/mysql v1.8.1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
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/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.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-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // 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/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.51.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/net v0.21.0 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
golang.org/x/text v0.21.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 (
|
require (
|
||||||
@@ -35,4 +41,5 @@ require (
|
|||||||
golang.org/x/crypto v0.32.0
|
golang.org/x/crypto v0.32.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
modernc.org/sqlite v1.34.5
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
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/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 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
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=
|
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 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
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/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/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 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
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/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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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=
|
||||||
|
|||||||
@@ -50,10 +50,10 @@ func (config ConfigStruct) SignJWT(val any) (string, error) {
|
|||||||
return t.SignedString([]byte(config.ClientSession.JwtSignature))
|
return t.SignedString([]byte(config.ClientSession.JwtSignature))
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig() ConfigStruct {
|
func LoadConfig() ConfigStruct {
|
||||||
Config := ConfigYaml{}
|
Config := ConfigYaml{}
|
||||||
|
|
||||||
yamlFile, err := os.ReadFile("config.yaml")
|
yamlFile, err := os.ReadFile(CONFIG_PATH)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Sprintf("Error opening config-file: %q", err))
|
panic(fmt.Sprintf("Error opening config-file: %q", err))
|
||||||
}
|
}
|
||||||
@@ -91,5 +91,5 @@ func loadConfig() ConfigStruct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
Config = loadConfig()
|
Config = LoadConfig()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,8 @@ var CONFIG_PATH = "config.yaml"
|
|||||||
|
|
||||||
type ConfigYaml struct {
|
type ConfigYaml struct {
|
||||||
LogLevel string `yaml:"log_level"`
|
LogLevel string `yaml:"log_level"`
|
||||||
Database struct {
|
Database string `yaml:"database"`
|
||||||
Host string `yaml:"host"`
|
Server struct {
|
||||||
User string `yaml:"user"`
|
|
||||||
Password string `yaml:"password"`
|
|
||||||
Database string `yaml:"database"`
|
|
||||||
} `yaml:"database"`
|
|
||||||
Server struct {
|
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
} `yaml:"server"`
|
} `yaml:"server"`
|
||||||
ClientSession struct {
|
ClientSession struct {
|
||||||
|
|||||||
@@ -1,59 +1,58 @@
|
|||||||
package availabilities
|
package availabilities
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
cache "github.com/jfarleyx/go-simple-cache"
|
|
||||||
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
type availabilitiesDB struct {
|
type AvailabilityDB struct {
|
||||||
Id int `db:"id"`
|
Id int `db:"id" json:"id" validate:"required"`
|
||||||
Text string `db:"text"`
|
Availability
|
||||||
Disabled bool `db:"disabled"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Availability struct {
|
type Availability struct {
|
||||||
Text string
|
Text string `db:"text" json:"text" validate:"required"`
|
||||||
Disabled bool
|
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) {
|
return err
|
||||||
if availabilities, hit := c.Get("availabilities"); !hit {
|
}
|
||||||
refresh()
|
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
return availabilities.(map[int]Availability), nil
|
return availabilitiesRaw, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func refresh() {
|
func Keys() (map[int]Availability, error) {
|
||||||
// get the availabilitiesRaw from the database
|
if availabilitiesRaw, err := Slice(); err != nil {
|
||||||
var availabilitiesRaw []availabilitiesDB
|
return nil, err
|
||||||
|
} else {
|
||||||
if err := db.DB.Select(&availabilitiesRaw, "SELECT * FROM AVAILABILITIES"); err == nil {
|
|
||||||
// convert the result in a map
|
// convert the result in a map
|
||||||
availabilities := map[int]Availability{}
|
availabilities := map[int]Availability{}
|
||||||
|
|
||||||
for _, a := range availabilitiesRaw {
|
for _, a := range availabilitiesRaw {
|
||||||
availabilities[a.Id] = Availability{
|
availabilities[a.Id] = Availability{
|
||||||
Text: a.Text,
|
Text: a.Text,
|
||||||
Disabled: a.Disabled,
|
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()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/go-sql-driver/mysql"
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
_config "github.com/johannesbuehl/golunteer/backend/pkg/config"
|
_config "github.com/johannesbuehl/golunteer/backend/pkg/config"
|
||||||
|
_ "modernc.org/sqlite" // SQLite driver
|
||||||
)
|
)
|
||||||
|
|
||||||
var config = _config.Config
|
var config = _config.Config
|
||||||
@@ -14,20 +16,31 @@ var config = _config.Config
|
|||||||
var DB *sqlx.DB
|
var DB *sqlx.DB
|
||||||
|
|
||||||
func init() {
|
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
|
// connect to the database
|
||||||
DB = sqlx.MustOpen("mysql", sqlConfig.FormatDSN())
|
DB = sqlx.MustOpen("sqlite", config.Database)
|
||||||
DB.SetMaxIdleConns(10)
|
|
||||||
DB.SetMaxIdleConns(100)
|
|
||||||
DB.SetConnMaxLifetime(time.Minute)
|
|
||||||
|
|
||||||
|
// 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
80
backend/pkg/db/setup.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,14 +9,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type tasksDB struct {
|
type tasksDB struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
Text string `db:"text"`
|
Text string `db:"text"`
|
||||||
Disabled bool `db:"disabled"`
|
Enabled bool `db:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Task struct {
|
type Task struct {
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Disabled bool `json:"disabled"`
|
Enabled bool `json:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var c *cache.Cache
|
var c *cache.Cache
|
||||||
@@ -41,8 +41,8 @@ func refresh() {
|
|||||||
|
|
||||||
for _, a := range tasksRaw {
|
for _, a := range tasksRaw {
|
||||||
tasks[a.ID] = Task{
|
tasks[a.ID] = Task{
|
||||||
Text: a.Text,
|
Text: a.Text,
|
||||||
Disabled: a.Disabled,
|
Enabled: a.Enabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ func init() {
|
|||||||
Out: os.Stdout,
|
Out: os.Stdout,
|
||||||
TimeFormat: time.DateTime,
|
TimeFormat: time.DateTime,
|
||||||
FormatLevel: func(i interface{}) string {
|
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 {
|
FormatFieldName: func(i interface{}) string {
|
||||||
return fmt.Sprintf("%s", i)
|
return fmt.Sprintf("%s", i)
|
||||||
|
|||||||
107
backend/pkg/router/availabilities.go
Normal file
107
backend/pkg/router/availabilities.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,14 +81,17 @@ func init() {
|
|||||||
"events/user/pending": getEventsUserPending,
|
"events/user/pending": getEventsUserPending,
|
||||||
"tasks": getTasks,
|
"tasks": getTasks,
|
||||||
"users": getUsers,
|
"users": getUsers,
|
||||||
|
"availabilities": getAvailabilities,
|
||||||
},
|
},
|
||||||
"POST": {
|
"POST": {
|
||||||
"events": postEvent,
|
"events": postEvent,
|
||||||
"users": postUser,
|
"users": postUser,
|
||||||
|
"availabilities": postAvailabilitie,
|
||||||
},
|
},
|
||||||
"PATCH": {
|
"PATCH": {
|
||||||
"users": patchUser,
|
"users": patchUser,
|
||||||
"events": patchEvent,
|
"events": patchEvent,
|
||||||
|
"availabilities": patchAvailabilities,
|
||||||
},
|
},
|
||||||
"PUT": {
|
"PUT": {
|
||||||
"users/password": putPassword,
|
"users/password": putPassword,
|
||||||
|
|||||||
47
backend/setup.sql
Normal file
47
backend/setup.sql
Normal 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
|
||||||
|
);
|
||||||
@@ -18,33 +18,28 @@ export default function Main({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const user = zustand((state) => state.user);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
let loggedIn = false;
|
let loggedIn = false;
|
||||||
|
|
||||||
if (zustand.getState().user === null) {
|
const welcomeResult = await apiCall<{
|
||||||
const welcomeResult = await apiCall<{
|
userName: string;
|
||||||
userName: string;
|
loggedIn: boolean;
|
||||||
loggedIn: boolean;
|
}>("GET", "welcome");
|
||||||
}>("GET", "welcome");
|
|
||||||
|
|
||||||
if (welcomeResult.ok) {
|
if (welcomeResult.ok) {
|
||||||
try {
|
try {
|
||||||
const response = await welcomeResult.json();
|
const response = await welcomeResult.json();
|
||||||
|
|
||||||
if (response.userName !== undefined && response.userName !== "") {
|
if (response.userName !== undefined && response.userName !== "") {
|
||||||
zustand.getState().reset({ user: response });
|
zustand.getState().reset({ user: response });
|
||||||
|
|
||||||
loggedIn = true;
|
loggedIn = true;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
} else {
|
|
||||||
zustand.getState().reset();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
loggedIn = true;
|
zustand.getState().reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname === "/login") {
|
if (pathname === "/login") {
|
||||||
@@ -62,7 +57,7 @@ export default function Main({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [pathname, router, user]);
|
}, [pathname, router]);
|
||||||
|
|
||||||
switch (auth) {
|
switch (auth) {
|
||||||
case AuthState.Loading:
|
case AuthState.Loading:
|
||||||
|
|||||||
33
client/src/app/admin/AddAvailability.tsx
Normal file
33
client/src/app/admin/AddAvailability.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { apiCall } from "@/lib";
|
||||||
|
import AvailabilityEditor, { Availability } from "./AvailabilityEditor";
|
||||||
|
import { Button } from "@heroui/react";
|
||||||
|
import { AddLarge } from "@carbon/icons-react";
|
||||||
|
|
||||||
|
export default function AddAvailability(props: {
|
||||||
|
isOpen?: boolean;
|
||||||
|
onOpenChange?: (isOpen: boolean) => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}) {
|
||||||
|
async function addAvailability(a: Availability) {
|
||||||
|
const result = await apiCall("POST", "availabilities", undefined, a);
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
props.onSuccess?.();
|
||||||
|
props.onOpenChange?.(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AvailabilityEditor
|
||||||
|
header="Add Availability"
|
||||||
|
footer={
|
||||||
|
<Button type="submit" color="primary" startContent={<AddLarge />}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
isOpen={props.isOpen}
|
||||||
|
onOpenChange={props.onOpenChange}
|
||||||
|
onSubmit={addAvailability}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,164 @@
|
|||||||
import ColorSelector from "@/components/Colorselector";
|
import { color2Tailwind, colors } from "@/components/Colorselector";
|
||||||
|
import { apiCall } from "@/lib";
|
||||||
|
import { Edit } from "@carbon/icons-react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Chip,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableColumn,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
Tooltip,
|
||||||
|
} from "@heroui/react";
|
||||||
|
import { useAsyncList } from "@react-stately/data";
|
||||||
|
import { useState } from "react";
|
||||||
|
import AddAvailability from "./AddAvailability";
|
||||||
|
import { Availability } from "./AvailabilityEditor";
|
||||||
|
import EditAvailability from "./EditAvailability";
|
||||||
|
|
||||||
export default function Availabilities() {
|
export default function Availabilities() {
|
||||||
|
const [showAddAvailability, setShowAddAvailability] = useState(false);
|
||||||
|
const [editAvailability, setEditAvailability] = useState<Availability>();
|
||||||
|
|
||||||
|
const availabilities = useAsyncList<Availability>({
|
||||||
|
async load() {
|
||||||
|
const result = await apiCall("GET", "availabilities");
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
const json = await result.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: json.availabilities,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async sort({ items, sortDescriptor }) {
|
||||||
|
return {
|
||||||
|
items: items.sort((a, b) => {
|
||||||
|
let cmp = 0;
|
||||||
|
|
||||||
|
switch (sortDescriptor.column) {
|
||||||
|
case "text":
|
||||||
|
cmp = a.text.localeCompare(b.text);
|
||||||
|
break;
|
||||||
|
case "enabled":
|
||||||
|
if (a.enabled && !b.enabled) {
|
||||||
|
cmp = -1;
|
||||||
|
} else if (!a.enabled && b.enabled) {
|
||||||
|
cmp = 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "color":
|
||||||
|
const aIndex = colors.findIndex((c) => c.value === a.color);
|
||||||
|
const bIndex = colors.findIndex((c) => c.value === a.color);
|
||||||
|
|
||||||
|
if (aIndex > bIndex) {
|
||||||
|
cmp = -1;
|
||||||
|
} else {
|
||||||
|
cmp = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortDescriptor.direction === "descending") {
|
||||||
|
cmp *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmp;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const topContent = (
|
||||||
|
<>
|
||||||
|
<Button onPress={() => setShowAddAvailability(true)}>
|
||||||
|
Add Availability
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ColorSelector />
|
<Table
|
||||||
|
aria-label="Table with the availabilites"
|
||||||
|
shadow="none"
|
||||||
|
isHeaderSticky
|
||||||
|
topContent={topContent}
|
||||||
|
sortDescriptor={availabilities.sortDescriptor}
|
||||||
|
onSortChange={availabilities.sort}
|
||||||
|
>
|
||||||
|
<TableHeader>
|
||||||
|
<TableColumn allowsSorting key="userName">
|
||||||
|
Text
|
||||||
|
</TableColumn>
|
||||||
|
<TableColumn allowsSorting key="color" align="center">
|
||||||
|
Color
|
||||||
|
</TableColumn>
|
||||||
|
<TableColumn allowsSorting key="admin" align="center">
|
||||||
|
Enabled
|
||||||
|
</TableColumn>
|
||||||
|
<TableColumn key="edit" align="center">
|
||||||
|
Edit
|
||||||
|
</TableColumn>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody items={availabilities.items}>
|
||||||
|
{(availability) => (
|
||||||
|
<TableRow key={availability.text}>
|
||||||
|
<TableCell
|
||||||
|
className={`text-${color2Tailwind(availability.color)}`}
|
||||||
|
>
|
||||||
|
{availability.text}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Chip
|
||||||
|
classNames={{
|
||||||
|
base: `bg-${color2Tailwind(availability.color)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{availability.color}
|
||||||
|
</Chip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox isSelected={availability.enabled} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
onPress={() => setEditAvailability(availability)}
|
||||||
|
>
|
||||||
|
<Tooltip content="Edit availability">
|
||||||
|
<Edit />
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<AddAvailability
|
||||||
|
isOpen={showAddAvailability}
|
||||||
|
onOpenChange={setShowAddAvailability}
|
||||||
|
onSuccess={availabilities.reload}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditAvailability
|
||||||
|
value={editAvailability}
|
||||||
|
isOpen={!!editAvailability}
|
||||||
|
onOpenChange={(isOpen) =>
|
||||||
|
!isOpen ? setEditAvailability(undefined) : null
|
||||||
|
}
|
||||||
|
onSuccess={availabilities.reload}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
93
client/src/app/admin/AvailabilityEditor.tsx
Normal file
93
client/src/app/admin/AvailabilityEditor.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import ColorSelector from "@/components/Colorselector";
|
||||||
|
import {
|
||||||
|
Checkbox,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
} from "@heroui/react";
|
||||||
|
import React, { FormEvent, useState } from "react";
|
||||||
|
|
||||||
|
export interface Availability {
|
||||||
|
text: string;
|
||||||
|
color: string;
|
||||||
|
id: number | undefined;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AvailabilityEditor(props: {
|
||||||
|
header: React.ReactNode;
|
||||||
|
footer: React.ReactNode;
|
||||||
|
value?: Availability;
|
||||||
|
isOpen?: boolean;
|
||||||
|
onOpenChange?: (isOpen: boolean) => void;
|
||||||
|
onSubmit?: (e: Availability) => void;
|
||||||
|
}) {
|
||||||
|
const [text, setText] = useState(props.value?.text);
|
||||||
|
const [color, setColor] = useState(props.value?.color ?? "Red");
|
||||||
|
const [enabled, setEnabled] = useState(props.value?.enabled ?? true);
|
||||||
|
|
||||||
|
function submit(e: FormEvent<HTMLFormElement>) {
|
||||||
|
const formData = Object.fromEntries(new FormData(e.currentTarget)) as {
|
||||||
|
text: string;
|
||||||
|
color: string;
|
||||||
|
enabled: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
props.onSubmit?.({
|
||||||
|
...formData,
|
||||||
|
id: props.value?.id,
|
||||||
|
enabled: formData.enabled == "true",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={props.isOpen}
|
||||||
|
onOpenChange={props.onOpenChange}
|
||||||
|
shadow={"none" as "sm"}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
validationBehavior="native"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
submit(e);
|
||||||
|
}}
|
||||||
|
className="w-fit border-2"
|
||||||
|
>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>
|
||||||
|
<h1>{props.header}</h1>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<Input
|
||||||
|
value={text}
|
||||||
|
onValueChange={setText}
|
||||||
|
name="text"
|
||||||
|
label="Text"
|
||||||
|
isRequired
|
||||||
|
variant="bordered"
|
||||||
|
/>
|
||||||
|
<ColorSelector
|
||||||
|
value={color}
|
||||||
|
onValueChange={setColor}
|
||||||
|
name="color"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
value="true"
|
||||||
|
isSelected={enabled}
|
||||||
|
onValueChange={setEnabled}
|
||||||
|
name="enabled"
|
||||||
|
>
|
||||||
|
Enabled
|
||||||
|
</Checkbox>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>{props.footer} </ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
client/src/app/admin/EditAvailability.tsx
Normal file
36
client/src/app/admin/EditAvailability.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { apiCall } from "@/lib";
|
||||||
|
import AvailabilityEditor, { Availability } from "./AvailabilityEditor";
|
||||||
|
import { Button } from "@heroui/react";
|
||||||
|
import { Renew } from "@carbon/icons-react";
|
||||||
|
|
||||||
|
export default function EditAvailability(props: {
|
||||||
|
value: Availability | undefined;
|
||||||
|
isOpen?: boolean;
|
||||||
|
onOpenChange?: (isOpen: boolean) => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}) {
|
||||||
|
async function addAvailability(a: Availability) {
|
||||||
|
const result = await apiCall("PATCH", "availabilities", undefined, a);
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
props.onSuccess?.();
|
||||||
|
props.onOpenChange?.(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AvailabilityEditor
|
||||||
|
key={props.value?.id}
|
||||||
|
header="Edit Availability"
|
||||||
|
footer={
|
||||||
|
<Button type="submit" color="primary" startContent={<Renew />}>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
value={props.value}
|
||||||
|
isOpen={props.isOpen}
|
||||||
|
onOpenChange={props.onOpenChange}
|
||||||
|
onSubmit={addAvailability}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -67,23 +67,23 @@ export default function EditUser(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={props.isOpen} onOpenChange={props.onOpenChange}>
|
<Modal isOpen={props.isOpen} onOpenChange={props.onOpenChange}>
|
||||||
{props.user !== undefined ? (
|
<Form
|
||||||
<ModalContent>
|
validationBehavior="native"
|
||||||
<ModalHeader>
|
onSubmit={(e) => {
|
||||||
<h1 className="text-2xl">
|
e.preventDefault();
|
||||||
Edit User{" "}
|
updateUser(e);
|
||||||
<span className="font-numbers font-normal italic">
|
}}
|
||||||
{props.user.userName}
|
>
|
||||||
</span>
|
{props.user !== undefined ? (
|
||||||
</h1>
|
<ModalContent>
|
||||||
</ModalHeader>
|
<ModalHeader>
|
||||||
<Form
|
<h1 className="text-2xl">
|
||||||
validationBehavior="native"
|
Edit User{" "}
|
||||||
onSubmit={(e) => {
|
<span className="font-numbers font-normal italic">
|
||||||
e.preventDefault();
|
{props.user.userName}
|
||||||
updateUser(e);
|
</span>
|
||||||
}}
|
</h1>
|
||||||
>
|
</ModalHeader>
|
||||||
<ModalBody className="w-full">
|
<ModalBody className="w-full">
|
||||||
<Input
|
<Input
|
||||||
label="Name"
|
label="Name"
|
||||||
@@ -129,9 +129,9 @@ export default function EditUser(props: {
|
|||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Form>
|
</ModalContent>
|
||||||
</ModalContent>
|
) : null}
|
||||||
) : null}
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export default function Users() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onPress={() => setEditUser(user)}
|
onPress={() => setEditUser(user)}
|
||||||
>
|
>
|
||||||
<Tooltip content="Edit event">
|
<Tooltip content="Edit user">
|
||||||
<Edit />
|
<Edit />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -40,26 +40,6 @@ import React, { Key, useState } from "react";
|
|||||||
|
|
||||||
type EventWithAvailabilities = EventData & { availabilities: string[] };
|
type EventWithAvailabilities = EventData & { availabilities: string[] };
|
||||||
|
|
||||||
function availability2Tailwind(availability?: Availability) {
|
|
||||||
switch (availability) {
|
|
||||||
case "yes":
|
|
||||||
return "";
|
|
||||||
default:
|
|
||||||
return "italic";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function availability2Color(availability?: Availability) {
|
|
||||||
switch (availability) {
|
|
||||||
case "yes":
|
|
||||||
return "default";
|
|
||||||
case "maybe":
|
|
||||||
return "warning";
|
|
||||||
default:
|
|
||||||
return "danger";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AdminPanel() {
|
export default function AdminPanel() {
|
||||||
const [showAddEvent, setShowAddEvent] = useState(false);
|
const [showAddEvent, setShowAddEvent] = useState(false);
|
||||||
const [editEvent, setEditEvent] = useState<EventData | undefined>();
|
const [editEvent, setEditEvent] = useState<EventData | undefined>();
|
||||||
@@ -75,7 +55,7 @@ export default function AdminPanel() {
|
|||||||
{ key: "date", label: "Date" },
|
{ key: "date", label: "Date" },
|
||||||
{ key: "description", label: "Description" },
|
{ key: "description", label: "Description" },
|
||||||
...Object.values(tasks)
|
...Object.values(tasks)
|
||||||
.filter((task) => !task.disabled)
|
.filter((task) => task.enabled)
|
||||||
.map((task) => ({ label: task.text, key: task.text })),
|
.map((task) => ({ label: task.text, key: task.text })),
|
||||||
{ key: "actions", label: "Action" },
|
{ key: "actions", label: "Action" },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,34 +5,49 @@ import {
|
|||||||
VisuallyHidden,
|
VisuallyHidden,
|
||||||
} from "@heroui/react";
|
} from "@heroui/react";
|
||||||
|
|
||||||
export default function ColorSelector() {
|
export const colors = [
|
||||||
const colors = [
|
{ value: "Red", tailwind: "red-600" },
|
||||||
{ value: "Red", tailwind: "red-600" },
|
{ value: "Orange", tailwind: "orange-600" },
|
||||||
{ value: "Orange", tailwind: "orange-600" },
|
{ value: "Amber", tailwind: "amber-600" },
|
||||||
{ value: "Amber", tailwind: "amber-600" },
|
{ value: "Yellow", tailwind: "yellow-600" },
|
||||||
{ value: "Yellow", tailwind: "yellow-600" },
|
{ value: "Lime", tailwind: "lime-600" },
|
||||||
{ value: "Lime", tailwind: "lime-600" },
|
{ value: "Green", tailwind: "green-600" },
|
||||||
{ value: "Green", tailwind: "green-600" },
|
{ value: "Emerald", tailwind: "emerald-600" },
|
||||||
{ value: "Emerald", tailwind: "emerald-600" },
|
{ value: "Teal", tailwind: "teal-600" },
|
||||||
{ value: "Teal", tailwind: "teal-600" },
|
{ value: "Cyan", tailwind: "cyan-600" },
|
||||||
{ value: "Cyan", tailwind: "cyan-600" },
|
{ value: "Sky", tailwind: "sky-600" },
|
||||||
{ value: "Sky", tailwind: "sky-600" },
|
{ value: "Blue", tailwind: "blue-600" },
|
||||||
{ value: "Blue", tailwind: "blue-600" },
|
{ value: "Indigo", tailwind: "indigo-600" },
|
||||||
{ value: "Indigo", tailwind: "indigo-600" },
|
{ value: "Violet", tailwind: "violet-600" },
|
||||||
{ value: "Violet", tailwind: "violet-600" },
|
{ value: "Purple", tailwind: "purple-600" },
|
||||||
{ value: "Purple", tailwind: "purple-600" },
|
{ value: "Fuchsia", tailwind: "fuchsia-600" },
|
||||||
{ value: "Fuchsia", tailwind: "fuchsia-600" },
|
{ value: "Pink", tailwind: "pink-600" },
|
||||||
{ value: "Pink", tailwind: "pink-600" },
|
];
|
||||||
];
|
|
||||||
|
|
||||||
|
export function color2Tailwind(v: string): string | undefined {
|
||||||
|
const find = colors.find((c) => c.value === v)?.tailwind;
|
||||||
|
|
||||||
|
return find;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ColorSelector(props: {
|
||||||
|
name?: string;
|
||||||
|
value?: string;
|
||||||
|
onValueChange?: (value: string) => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<RadioGroup classNames={{ wrapper: "grid grid-cols-4" }}>
|
<RadioGroup
|
||||||
|
value={props.value}
|
||||||
|
onValueChange={props.onValueChange}
|
||||||
|
classNames={{ wrapper: "grid grid-cols-4" }}
|
||||||
|
name={props.name}
|
||||||
|
>
|
||||||
{colors.map((color) => (
|
{colors.map((color) => (
|
||||||
<ColorRadio
|
<ColorRadio
|
||||||
description={color.value}
|
description={color.value}
|
||||||
value={color.value}
|
value={color.value}
|
||||||
key={color.value}
|
key={color.value}
|
||||||
radioColor={`bg-${color.tailwind}`}
|
radiocolor={`bg-${color.tailwind}`}
|
||||||
>
|
>
|
||||||
<div>{color.value}</div>
|
<div>{color.value}</div>
|
||||||
</ColorRadio>
|
</ColorRadio>
|
||||||
@@ -41,13 +56,13 @@ export default function ColorSelector() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ColorRadio(props: { radioColor: string } & RadioProps) {
|
function ColorRadio(props: { radiocolor: string } & RadioProps) {
|
||||||
const { Component, children, getBaseProps, getInputProps } = useRadio(props);
|
const { Component, children, getBaseProps, getInputProps } = useRadio(props);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
{...getBaseProps()}
|
{...getBaseProps()}
|
||||||
className={`aspect-square cursor-pointer rounded-lg border-2 border-default tap-highlight-transparent hover:opacity-70 active:opacity-50 data-[selected=true]:border-primary ${props.radioColor} flex items-center justify-center p-1`}
|
className={`aspect-square cursor-pointer rounded-lg border-2 border-default text-foreground transition tap-highlight-transparent hover:opacity-70 active:opacity-50 data-[selected=true]:border-2 data-[selected=true]:border-stone-300 ${props.radiocolor} flex select-none items-center justify-center p-1 text-sm`}
|
||||||
>
|
>
|
||||||
<VisuallyHidden>
|
<VisuallyHidden>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ export default function EditEvent(props: {
|
|||||||
>
|
>
|
||||||
{tasksMap !== undefined ? (
|
{tasksMap !== undefined ? (
|
||||||
Object.entries(tasksMap)
|
Object.entries(tasksMap)
|
||||||
.filter(([, task]) => !task.disabled)
|
.filter(([, task]) => task.enabled)
|
||||||
.map(([id, task]) => (
|
.map(([id, task]) => (
|
||||||
<div key={id}>
|
<div key={id}>
|
||||||
<Checkbox value={id}>{task.text}</Checkbox>
|
<Checkbox value={id}>{task.text}</Checkbox>
|
||||||
|
|||||||
@@ -95,14 +95,11 @@ export function vaidatePassword(password: string): string[] {
|
|||||||
|
|
||||||
export interface Task {
|
export interface Task {
|
||||||
text: string;
|
text: string;
|
||||||
disabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTasks(): Promise<Record<number, Task>> {
|
export async function getTasks(): Promise<Record<number, Task>> {
|
||||||
const result = await apiCall<{ text: string; disabled: boolean }[]>(
|
const result = await apiCall<Task[]>("GET", "tasks");
|
||||||
"GET",
|
|
||||||
"tasks",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
const tasks = await result.json();
|
const tasks = await result.json();
|
||||||
|
|||||||
Reference in New Issue
Block a user