From b3e67bad75d023a0febe7d3c4b2572a3fffd788e Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 16 Nov 2025 23:08:20 +0000 Subject: [PATCH 1/4] doc: Add config tool to help support people --- Makefile | 3 + service/cmd/config-tool/main.go | 154 +++++++++++++++++++++++ service/internal/api/local_user_login.go | 9 +- service/internal/config/sanitize.go | 12 +- 4 files changed, 170 insertions(+), 8 deletions(-) create mode 100644 service/cmd/config-tool/main.go diff --git a/Makefile b/Makefile index d06002f..aa70614 100644 --- a/Makefile +++ b/Makefile @@ -56,4 +56,7 @@ clean: $(call delete-files,reports) $(call delete-files,gen) +config-tool: + cd service && go run cmd/config-tool/main.go + .PHONY: proto service diff --git a/service/cmd/config-tool/main.go b/service/cmd/config-tool/main.go new file mode 100644 index 0000000..2a92633 --- /dev/null +++ b/service/cmd/config-tool/main.go @@ -0,0 +1,154 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "strconv" + + "github.com/OliveTin/OliveTin/internal/api" + config "github.com/OliveTin/OliveTin/internal/config" + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" + log "github.com/sirupsen/logrus" +) + +func printPwd() { + pwd, err := os.Getwd() + if err != nil { + log.Fatalf("Error getting working directory: %v", err) + } + log.Infof("Working directory: %s", pwd) +} + +func main() { + resetPasswords := flag.Bool("passwords", true, "Reset passwords") + flag.Parse() + + log.Info("Config tool started") + + printPwd() + + k := koanf.New(".") + + configPath, err := filepath.Abs("../config.yaml") + if err != nil { + log.Fatalf("Error getting absolute config path: %v", err) + } + + log.Infof("Loading config from %s", configPath) + + backupOriginalConfig(configPath) + + err = k.Load(file.Provider(configPath), yaml.Parser()) + + if err != nil { + log.Fatalf("Error loading config: %v", err) + } + + cfg := &config.Config{} + + config.AppendSource(cfg, k, configPath) + + if *resetPasswords { + resetAllPasswords(k, cfg) + } + + saveConfig(k) +} + +func backupOriginalConfig(configPath string) { + originalConfigPath := filepath.Join(filepath.Dir(configPath), "config.original.yaml") + data, err := os.ReadFile(configPath) + if err != nil { + log.Fatalf("Error reading config for backup: %v", err) + } + err = os.WriteFile(originalConfigPath, data, 0644) + if err != nil { + log.Fatalf("Error writing backup config: %v", err) + } + log.Infof("Original config backed up to %s", originalConfigPath) +} + +func resetAllPasswords(k *koanf.Koanf, cfg *config.Config) { + if !cfg.AuthLocalUsers.Enabled || len(cfg.AuthLocalUsers.Users) == 0 { + log.Info("No local users found, skipping password reset") + return + } + + hashedPassword, err := api.CreateHash("password") + if err != nil { + log.Fatalf("Error creating password hash: %v", err) + } + + usersSlice := k.Get("authLocalUsers.users") + usersSliceTyped, ok := usersSlice.([]interface{}) + + if ok && len(usersSliceTyped) > 0 { + newUsersSlice := make([]interface{}, len(usersSliceTyped)) + for index, userValue := range usersSliceTyped { + userMap, ok := userValue.(map[string]interface{}) + if !ok { + log.Warnf("User entry at index %d is not a map, skipping", index) + newUsersSlice[index] = userValue + continue + } + + oldPassword, _ := userMap["password"].(string) + username, _ := userMap["username"].(string) + if username == "" { + username = fmt.Sprintf("user[%d]", index) + } + + newUserMap := make(map[string]interface{}) + for k, v := range userMap { + newUserMap[k] = v + } + newUserMap["password"] = hashedPassword + newUsersSlice[index] = newUserMap + + oldHashPreview := oldPassword + if len(oldPassword) > 20 { + oldHashPreview = oldPassword[:20] + } + log.Infof("Reset password for user '%s' (old hash: %s...)", username, oldHashPreview) + } + k.Set("authLocalUsers.users", newUsersSlice) + } else { + for index, user := range cfg.AuthLocalUsers.Users { + key := "authLocalUsers.users." + strconv.Itoa(index) + ".password" + k.Set(key, hashedPassword) + + oldHashPreview := user.Password + if len(oldHashPreview) > 20 { + oldHashPreview = oldHashPreview[:20] + } + log.Infof("Reset password for user '%s' (old hash: %s...)", user.Username, oldHashPreview) + } + } + + log.Infof("Reset %d password(s) to 'password'", len(cfg.AuthLocalUsers.Users)) +} + +func saveConfig(k *koanf.Koanf) { + pwd, err := os.Getwd() + if err != nil { + log.Fatalf("Error getting working directory: %v", err) + } + fullPath := filepath.Join(filepath.Dir(filepath.Dir(filepath.Dir(pwd))), "config.yaml") + + out, err := k.Marshal(yaml.Parser()) + + if err != nil { + log.Fatalf("Error marshalling config: %v", err) + } + + err = os.WriteFile(fullPath, out, 0644) + if err != nil { + log.Fatalf("Error saving config: %v", err) + } + + log.Infof("Config saved to %s", fullPath) +} diff --git a/service/internal/api/local_user_login.go b/service/internal/api/local_user_login.go index c6d8442..db6e99e 100644 --- a/service/internal/api/local_user_login.go +++ b/service/internal/api/local_user_login.go @@ -1,10 +1,11 @@ package api import ( + "runtime" + config "github.com/OliveTin/OliveTin/internal/config" "github.com/alexedwards/argon2id" log "github.com/sirupsen/logrus" - "runtime" ) var defaultParams = argon2id.Params{ @@ -15,7 +16,7 @@ var defaultParams = argon2id.Params{ KeyLength: 32, } -func createHash(password string) (string, error) { +func CreateHash(password string) (string, error) { hash, err := argon2id.CreateHash(password, &defaultParams) if err != nil { @@ -26,6 +27,10 @@ func createHash(password string) (string, error) { return hash, nil } +func createHash(password string) (string, error) { + return CreateHash(password) +} + func comparePasswordAndHash(password, hash string) bool { match, err := argon2id.ComparePasswordAndHash(password, hash) diff --git a/service/internal/config/sanitize.go b/service/internal/config/sanitize.go index 2a93759..9040ade 100644 --- a/service/internal/config/sanitize.go +++ b/service/internal/config/sanitize.go @@ -1,15 +1,18 @@ package config import ( + "strings" + "github.com/google/uuid" log "github.com/sirupsen/logrus" - "strings" ) // Sanitize will look for common configuration issues, and fix them. For example, // populating undefined fields - name -> title, etc. func (cfg *Config) Sanitize() { cfg.sanitizeLogLevel() + cfg.sanitizeAuthRequireGuestsToLogin() + cfg.sanitizeLogHistoryPageSize() // log.Infof("cfg %p", cfg) @@ -41,12 +44,9 @@ func (action *Action) sanitize(cfg *Config) { for idx := range action.Arguments { action.Arguments[idx].sanitize() } - - sanitizeAuthRequireGuestsToLogin(cfg) - sanitizeLogHistoryPageSize(cfg) } -func sanitizeAuthRequireGuestsToLogin(cfg *Config) { +func (cfg *Config) sanitizeAuthRequireGuestsToLogin() { if cfg.AuthRequireGuestsToLogin { log.Infof("AuthRequireGuestsToLogin is enabled. All defaultPermissions will be set to false") @@ -56,7 +56,7 @@ func sanitizeAuthRequireGuestsToLogin(cfg *Config) { } } -func sanitizeLogHistoryPageSize(cfg *Config) { +func (cfg *Config) sanitizeLogHistoryPageSize() { if cfg.LogHistoryPageSize < 10 { log.Warnf("LogsHistoryLimit is too low, setting it to 10") cfg.LogHistoryPageSize = 10 From ca0a134acd34c1a404fa4ad08bfc83a07f13764f Mon Sep 17 00:00:00 2001 From: jamesread Date: Sat, 22 Nov 2025 09:34:37 +0000 Subject: [PATCH 2/4] chore: write back to the same config path in config-tool --- service/cmd/config-tool/main.go | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/service/cmd/config-tool/main.go b/service/cmd/config-tool/main.go index 2a92633..7e68be1 100644 --- a/service/cmd/config-tool/main.go +++ b/service/cmd/config-tool/main.go @@ -56,7 +56,7 @@ func main() { resetAllPasswords(k, cfg) } - saveConfig(k) + saveConfig(configPath, k) } func backupOriginalConfig(configPath string) { @@ -132,23 +132,17 @@ func resetAllPasswords(k *koanf.Koanf, cfg *config.Config) { log.Infof("Reset %d password(s) to 'password'", len(cfg.AuthLocalUsers.Users)) } -func saveConfig(k *koanf.Koanf) { - pwd, err := os.Getwd() - if err != nil { - log.Fatalf("Error getting working directory: %v", err) - } - fullPath := filepath.Join(filepath.Dir(filepath.Dir(filepath.Dir(pwd))), "config.yaml") - +func saveConfig(configPath string, k *koanf.Koanf) { out, err := k.Marshal(yaml.Parser()) if err != nil { log.Fatalf("Error marshalling config: %v", err) } - err = os.WriteFile(fullPath, out, 0644) + err = os.WriteFile(configPath, out, 0644) if err != nil { log.Fatalf("Error saving config: %v", err) } - log.Infof("Config saved to %s", fullPath) + log.Infof("Config saved to %s", configPath) } From 49b8c2c4f252e678ff6f46c38d45017153a83884 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sat, 22 Nov 2025 10:43:56 +0000 Subject: [PATCH 3/4] chore: fix broken datetime test --- frontend/resources/vue/views/ArgumentForm.vue | 27 ++++++++++++++----- .../configs/datetime/config.yaml | 1 - integration-tests/test/datetime.mjs | 4 --- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/frontend/resources/vue/views/ArgumentForm.vue b/frontend/resources/vue/views/ArgumentForm.vue index 6aa1e86..5cd3b00 100644 --- a/frontend/resources/vue/views/ArgumentForm.vue +++ b/frontend/resources/vue/views/ArgumentForm.vue @@ -29,7 +29,7 @@ :list="arg.suggestions ? `${arg.name}-choices` : undefined" :type="getInputComponent(arg) !== 'select' ? getInputType(arg) : undefined" :rows="arg.type === 'raw_string_multiline' ? 5 : undefined" - :step="arg.type === 'datetime' ? 1 : undefined" :pattern="getPattern(arg)" :required="arg.required" + :step="arg.type === 'datetime' ? 1 : undefined" :pattern="getPattern(arg)" @input="handleInput(arg, $event)" @change="handleChange(arg, $event)" /> @@ -202,6 +202,16 @@ async function validateArgument(arg, value) { return } + // Skip validation for datetime - backend will handle mangling values without seconds + if (arg.type === 'datetime') { + const inputElement = document.getElementById(arg.name) + if (inputElement) { + inputElement.setCustomValidity('') + } + delete formErrors.value[arg.name] + return + } + try { const validateArgumentTypeArgs = { value: value, @@ -286,10 +296,12 @@ async function startAction(actionArgs) { } try { - await window.client.startAction(startActionArgs) - console.log('Action started successfully with tracking ID:', startActionArgs.uniqueTrackingId) + const response = await window.client.startAction(startActionArgs) + console.log('Action started successfully with tracking ID:', response.executionTrackingId) + return response } catch (err) { console.error('Failed to start action:', err) + throw err } } @@ -319,9 +331,12 @@ async function handleSubmit(event) { const argvs = getArgumentValues() console.log('argument form has elements that passed validation') - await startAction(argvs) - - router.back() + try { + const response = await startAction(argvs) + router.push(`/logs/${response.executionTrackingId}`) + } catch (err) { + console.error('Failed to start action:', err) + } } function handleCancel() { diff --git a/integration-tests/configs/datetime/config.yaml b/integration-tests/configs/datetime/config.yaml index 34eb1ad..8647e15 100644 --- a/integration-tests/configs/datetime/config.yaml +++ b/integration-tests/configs/datetime/config.yaml @@ -12,6 +12,5 @@ actions: - name: datetime title: Select a date and time type: datetime - required: true description: Choose a date and time for the action diff --git a/integration-tests/test/datetime.mjs b/integration-tests/test/datetime.mjs index d90a077..3f799fc 100644 --- a/integration-tests/test/datetime.mjs +++ b/integration-tests/test/datetime.mjs @@ -47,10 +47,6 @@ describe('config: datetime', function () { const step = await datetimeInput.getAttribute('step') expect(step).to.equal('1', 'Step attribute should be 1') - // Verify it's required - const required = await datetimeInput.getAttribute('required') - expect(required).to.not.be.null - // Verify the label is present const label = await webdriver.findElement(By.css('label[for="datetime"]')) expect(await label.getText()).to.contain('Select a date and time') From 3e6a7511325596eaf78efb69098edbee31ee2296 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sat, 22 Nov 2025 10:49:48 +0000 Subject: [PATCH 4/4] chore: config-tool wont overwrite original config --- service/cmd/config-tool/main.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/service/cmd/config-tool/main.go b/service/cmd/config-tool/main.go index 7e68be1..971f2f7 100644 --- a/service/cmd/config-tool/main.go +++ b/service/cmd/config-tool/main.go @@ -61,6 +61,16 @@ func main() { func backupOriginalConfig(configPath string) { originalConfigPath := filepath.Join(filepath.Dir(configPath), "config.original.yaml") + + _, err := os.Stat(originalConfigPath) + if err == nil { + log.Infof("Backup already exists at %s, skipping backup to preserve original", originalConfigPath) + return + } + if !os.IsNotExist(err) { + log.Fatalf("Error checking backup file: %v", err) + } + data, err := os.ReadFile(configPath) if err != nil { log.Fatalf("Error reading config for backup: %v", err)