diff --git a/backend/.devcontainer/devcontainer.json b/backend/.devcontainer/devcontainer.json new file mode 100644 index 0000000..3495528 --- /dev/null +++ b/backend/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/go +{ + "name": "Go", + // 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" + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "go version", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..a539470 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1 @@ +config.yaml \ No newline at end of file diff --git a/backend/config.go b/backend/config.go new file mode 100644 index 0000000..16f7aca --- /dev/null +++ b/backend/config.go @@ -0,0 +1,72 @@ +package main + +import ( + "bytes" + "fmt" + "os" + "time" + + "gopkg.in/yaml.v3" +) + +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"` + ClientSession struct { + JwtSignature string `yaml:"jwt_signature"` + Expire string `yaml:"expire"` + } `yaml:"client_session"` +} + +type CacheConfig struct { + Expiration time.Duration + Purge time.Duration +} + +var config ConfigYaml + +func loadConfig() ConfigYaml { + config := ConfigYaml{} + + yamlFile, err := os.ReadFile(CONFIG_PATH) + if err != nil { + panic(fmt.Sprintf("Error opening config-file: %v", 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) + } + + return config +} + +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); err != nil { + panic(err) + } else { + if err := os.WriteFile(CONFIG_PATH, buf.Bytes(), 0644); err != nil { + panic(err) + } + } +} + +func init() { + config = loadConfig() +} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..ed452b7 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,3 @@ +module github.com/johannesbuehl/golunteer/backend + +go 1.23.4 diff --git a/backend/setup.go b/backend/setup.go new file mode 100644 index 0000000..a1dc27f --- /dev/null +++ b/backend/setup.go @@ -0,0 +1,118 @@ +package main + +import ( + "database/sql" + "fmt" + "math/rand/v2" + "os" + "regexp" + "strings" + + "github.com/go-sql-driver/mysql" + "golang.org/x/crypto/bcrypt" +) + +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 exit(e error) { + fmt.Printf("%v\n", e) + os.Exit(1) +} + +func main() { + fmt.Println("connecting to database") + + // connect to the database + sqlConfig := mysql.Config{ + AllowNativePasswords: true, + Net: "tcp", + User: config.Database.User, + Passwd: config.Database.Password, + Addr: config.Database.Host, + DBName: config.Database.Database, + } + + db, err := sql.Open("mysql", sqlConfig.FormatDSN()) + if err != nil { + exit(err) + } + + // load the sql-script + fmt.Println(`reading "setup.sql"`) + var sqlScriptCommands []byte + if c, err := os.ReadFile("setup.sql"); err != nil { + exit(err) + } else { + sqlScriptCommands = c + } + + // read the currently availabe tables + fmt.Println("reading available tables in database") + if rows, err := db.Query("SHOW TABLES"); err != nil { + exit(err) + } else { + defer rows.Close() + + fmt.Println("checking for already existing tables in database") + for rows.Next() { + var name string + + if err := rows.Scan(&name); err != nil { + exit(err) + } else { + // check wether for the table there exists a create command + + if match, err := regexp.Match(fmt.Sprintf(`(?i)^create table %s`, name), sqlScriptCommands); err != nil { + exit(err) + } else { + if match { + exit(fmt.Errorf("can't setup databases: table %q already exists", name)) + } + } + } + } + } + + // everything is good (so far), create the tables + fmt.Println("Creating the individual tables:") + for _, cmd := range strings.Split(string(sqlScriptCommands), ";") { + db.Exec(cmd) + } + + 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 { + exit(err) + } else { + fmt.Println("\thashed password") + + // create an admin-user + if _, err := db.Exec("INSERT INTO USERS (name, password) VALUES ('admin', ?)", passwordHash); err != nil { + exit(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() +} diff --git a/.devcontainer/devcontainer.json b/client/.devcontainer/devcontainer.json similarity index 94% rename from .devcontainer/devcontainer.json rename to client/.devcontainer/devcontainer.json index a57d28c..33d9fdd 100644 --- a/.devcontainer/devcontainer.json +++ b/client/.devcontainer/devcontainer.json @@ -13,7 +13,7 @@ // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "yarn install", - "postCreateCommand": "cd client && npm install" + "postCreateCommand": "npm install" // Configure tool-specific properties. // "customizations": {}, diff --git a/setup/.devcontainer/devcontainer.json b/setup/.devcontainer/devcontainer.json new file mode 100644 index 0000000..3495528 --- /dev/null +++ b/setup/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/go +{ + "name": "Go", + // 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" + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "go version", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/setup/.gitignore b/setup/.gitignore new file mode 100644 index 0000000..5b6b072 --- /dev/null +++ b/setup/.gitignore @@ -0,0 +1 @@ +config.yaml diff --git a/setup/go.mod b/setup/go.mod new file mode 100644 index 0000000..ff1eb61 --- /dev/null +++ b/setup/go.mod @@ -0,0 +1,13 @@ +module github.com/johannesbuehl/golunteer/setup + +go 1.23.4 + +require ( + github.com/go-sql-driver/mysql v1.8.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + golang.org/x/crypto v0.32.0 +) diff --git a/setup/go.sum b/setup/go.sum new file mode 100644 index 0000000..b77143f --- /dev/null +++ b/setup/go.sum @@ -0,0 +1,9 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +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= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/setup/setup.sql b/setup/setup.sql new file mode 100644 index 0000000..cdc1e18 --- /dev/null +++ b/setup/setup.sql @@ -0,0 +1,44 @@ +CREATE TABLE TASKS ( + id INTEGER PRIMARY KEY AUTO_INCREMENT, + text varchar(64) NOT NULL, + disabled BOOL DEFAULT(false) +); + +CREATE TABLE AVAILABILITIES ( + id INTEGER PRIMARY KEY AUTO_INCREMENT, + text varchar(32) NOT NULL, + disabled BOOL DEFAULT(false) +); + +CREATE TABLE USERS ( + id INTEGER PRIMARY KEY AUTO_INCREMENT, + name varchar(64) NOT NULL, + password binary(60) NOT NULL, + admin BOOL NOT NULL DEFAULT(false) +); + +CREATE TABLE EVENTS ( + id INTEGER PRIMARY KEY AUTO_INCREMENT, + date DATETIME NOT NULL, + description TEXT DEFAULT("") +); + +CREATE TABLE USER_AVAILABILITIES ( + userID INTEGER NOT NULL, + eventID INTEGER NOT NULL, + availabilityID INTEGER NOT NULL, + PRIMARY KEY (userID, eventID), + FOREIGN KEY (userID) REFERENCES USERS(id) ON DELETE CASCADE, + FOREIGN KEY (eventID) REFERENCES EVENTS(id) ON DELETE CASCADE, + FOREIGN KEY (avaibID) REFERENCES AVAILABILITIES(id) ON DELETE RESTRICT, +); + +CREATE TABLE USER_ASSIGNMENTS ( + eventID INTEGER NOT NULL, + taskID INTEGER NOT NULL, + userID INTEGER NOT NULL, + PRIMARY KEY (eventID, taskID), + FOREIGN KEY (userID) REFERENCES USERS(id) ON DELETE CASCADE, + FOREIGN KEY (taskID) REFERENCES TASKS(id) ON DELETE RESTRICT, + FOREIGN KEY (eventID) REFERENCES EVENTS(id) ON DELETE CASCADE +); \ No newline at end of file