feat: improve security

This commit is contained in:
Patryk Hegenberg 2026-01-11 10:23:03 +01:00
parent 99fb97dff3
commit 5b16cef525
8 changed files with 181 additions and 46 deletions

77
app.go
View file

@ -5,11 +5,13 @@ import (
"log/slog" "log/slog"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"strings" "strings"
"time" "time"
"github.com/charmbracelet/huh" "github.com/charmbracelet/huh"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
) )
type App struct { type App struct {
@ -42,8 +44,34 @@ func (a *App) Close() error {
return nil return nil
} }
func (a *App) connect(withoutTimew bool) (*SSHConnection, error) { // Rückgabetyp geändert func (a *App) StartTracking(tag string, withoutTimew bool) error {
if err := a.timeStore.StartTracking(TagWork, withoutTimew); err != nil { if err := a.timeStore.StartTracking(tag); err != nil {
return err
}
if !withoutTimew {
if err := a.runCommand("timew", "start", tag); err != nil {
slog.Warn("Failed to start timewarrior (ignoring)", "error", err)
}
}
return nil
}
func (a *App) StopTracking(withoutTimew bool) error {
if err := a.timeStore.StopTracking(); err != nil {
return err
}
if !withoutTimew {
if err := a.runCommand("timew", "stop"); err != nil {
slog.Warn("Failed to stop timewarrior (ignoring)", "error", err)
}
}
return nil
}
func (a *App) connect(withoutTimew bool) (*SSHConnection, error) {
if err := a.StartTracking(TagWork, withoutTimew); err != nil {
slog.Warn(fmt.Sprintf("Failed to start time tracking for '%s': %v", TagWork, err)) slog.Warn(fmt.Sprintf("Failed to start time tracking for '%s': %v", TagWork, err))
} }
@ -69,7 +97,6 @@ func (a *App) connect(withoutTimew bool) (*SSHConnection, error) { // Rückgabet
go func() { go func() {
slog.Info("Starting RDP forwarder (local :6000 -> remote workstation:3389)") slog.Info("Starting RDP forwarder (local :6000 -> remote workstation:3389)")
if err := rdpForwarder.forward(); err != nil { if err := rdpForwarder.forward(); err != nil {
// slog.Error(fmt.Sprintf("RDP forwarder failed: %v", err)
slog.Error(fmt.Sprintf("ERROR: RDP forwarder failed: %v", err)) slog.Error(fmt.Sprintf("ERROR: RDP forwarder failed: %v", err))
} }
slog.Info("RDP forwarder stopped.") slog.Info("RDP forwarder stopped.")
@ -137,16 +164,24 @@ func (a *App) connectToWorkstation() {
fmt.Sprintf("%s@127.0.0.1", a.cfg.WorkstationUser), fmt.Sprintf("%s@127.0.0.1", a.cfg.WorkstationUser),
} }
if err := a.runCommand("ssh", sshArgs...); err != nil { if err := a.runCommand("ssh", sshArgs...); err != nil {
return
} }
} }
func (a *App) startRDPConnection() { func (a *App) startRDPConnection() {
slog.Info("Starting RDP connection to localhost:6000...") slog.Info("Starting RDP connection to localhost:6000...")
rdpCommand := fmt.Sprintf("xfreerdp /u:%s /p:%s /v:127.0.0.1:6000 /size:3000x1350 +clipboard /dynamic-resolution",
a.cfg.RDPUser, args := []string{
a.cfg.SSHPassword, fmt.Sprintf("/u:%s", a.cfg.RDPUser),
) fmt.Sprintf("/p:%s", a.cfg.SSHPassword),
if err := a.runCommand("bash", "-c", rdpCommand); err != nil { "/v:127.0.0.1:6000",
"/size:3000x1350",
"+clipboard",
"/dynamic-resolution",
}
if err := a.runCommand("xfreerdp", args...); err != nil {
return
} }
} }
@ -191,18 +226,18 @@ func (a *App) makeChoice() {
case "start work": case "start work":
a.connect(withoutTimew) a.connect(withoutTimew)
case "stop work": case "stop work":
if err := a.timeStore.StopTracking(withoutTimew); err != nil { if err := a.StopTracking(withoutTimew); err != nil {
slog.Error(fmt.Sprintf("Failed to stop time tracking: %v", err)) slog.Error(fmt.Sprintf("Failed to stop time tracking: %v", err))
} }
if err := a.killForwardings(); err != nil { if err := a.killForwardings(); err != nil {
slog.Warn(fmt.Sprintf("Could not kill all forwardings: %v", err)) slog.Warn(fmt.Sprintf("Could not kill all forwardings: %v", err))
} }
case "start break": case "start break":
if err := a.timeStore.StartTracking(TagBreak, withoutTimew); err != nil { if err := a.StartTracking(TagBreak, withoutTimew); err != nil {
slog.Error(fmt.Sprintf("Failed to start break tracking: %v", err)) slog.Error(fmt.Sprintf("Failed to start break tracking: %v", err))
} }
case "stop break": case "stop break":
if err := a.timeStore.StartTracking(TagWork, withoutTimew); err != nil { if err := a.StartTracking(TagWork, withoutTimew); err != nil {
slog.Error(fmt.Sprintf("Failed to stop break (start work): %v", err)) slog.Error(fmt.Sprintf("Failed to stop break (start work): %v", err))
} }
case "show day summary": case "show day summary":
@ -288,11 +323,23 @@ func (a *App) newSSHConnection() (*SSHConnection, error) {
return nil, fmt.Errorf("SSH authentication method could not be obtained") return nil, fmt.Errorf("SSH authentication method could not be obtained")
} }
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("failed to get user home dir: %w", err)
}
knownHostsPath := filepath.Join(homeDir, ".ssh", "known_hosts")
hostKeyCallback, err := knownhosts.New(knownHostsPath)
if err != nil {
slog.Warn("Could not load known_hosts file. Please ensure you have connected to the host manually once to populate it.", "path", knownHostsPath)
return nil, fmt.Errorf("failed to create host key callback (check your known_hosts file): %w", err)
}
sshConfig := &ssh.ClientConfig{ sshConfig := &ssh.ClientConfig{
User: a.cfg.SSHUser, User: a.cfg.SSHUser,
Auth: []ssh.AuthMethod{authMethod}, Auth: []ssh.AuthMethod{authMethod},
HostKeyCallback: ssh.InsecureIgnoreHostKey(), HostKeyCallback: hostKeyCallback,
Timeout: 10 * time.Second, // Etwas längerer Timeout Timeout: 10 * time.Second,
} }
target := fmt.Sprintf("%s:%d", a.cfg.SSHHost, a.cfg.SSHPort) target := fmt.Sprintf("%s:%d", a.cfg.SSHHost, a.cfg.SSHPort)
@ -306,8 +353,8 @@ func (a *App) newSSHConnection() (*SSHConnection, error) {
session, err := client.NewSession() session, err := client.NewSession()
if err != nil { if err != nil {
client.Close() // Client schließen, wenn Session fehlschlägt client.Close()
return nil, fmt.Errorf("failed to create SSH session: %w", err) return nil, fmt.Errorf("failed to create SSH session check: %w", err)
} }
session.Close() session.Close()

69
cmd.go
View file

@ -9,6 +9,7 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/charmbracelet/huh"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -30,12 +31,70 @@ and other utilities.`,
rootCmd.AddCommand(a.connectCommands()) rootCmd.AddCommand(a.connectCommands())
rootCmd.AddCommand(a.wakeCommand()) rootCmd.AddCommand(a.wakeCommand())
rootCmd.AddCommand(a.importTimewarriorCommand()) rootCmd.AddCommand(a.importTimewarriorCommand())
rootCmd.AddCommand(a.configCommand())
rootCmd.CompletionOptions.DisableDefaultCmd = true rootCmd.CompletionOptions.DisableDefaultCmd = true
return rootCmd return rootCmd
} }
func (a *App) configCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Short: "Manage configuration and secrets",
}
cmd.AddCommand(&cobra.Command{
Use: "set-secrets",
Short: "Interactively set passwords in the system keyring",
Long: "Prompts for SSH and RDP passwords and stores them securely in the operating system's keychain/keyring.",
RunE: func(cmd *cobra.Command, args []string) error {
var sshPw, rdpPw string
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("SSH Password").
Description("Leave empty to keep existing").
EchoMode(huh.EchoModePassword).
Value(&sshPw),
huh.NewInput().
Title("RDP Password").
Description("Leave empty to keep existing").
EchoMode(huh.EchoModePassword).
Value(&rdpPw),
),
)
if err := form.Run(); err != nil {
return err
}
if sshPw != "" {
if err := setSecret(keySSHPassword, sshPw); err != nil {
return fmt.Errorf("failed to save SSH password: %w", err)
}
fmt.Println("✓ SSH password saved to keyring.")
}
if rdpPw != "" {
if err := setSecret(keyRDPPassword, rdpPw); err != nil {
return fmt.Errorf("failed to save RDP password: %w", err)
}
fmt.Println("✓ RDP password saved to keyring.")
}
if sshPw == "" && rdpPw == "" {
fmt.Println("No changes made.")
}
return nil
},
})
return cmd
}
func (a *App) startCommand() *cobra.Command { func (a *App) startCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "start", Use: "start",
@ -73,7 +132,7 @@ Use --background (-b) to keep tunnels running in the background without auto-con
fmt.Println("\nINFO: Received interrupt signal. Shutting down background process...") fmt.Println("\nINFO: Received interrupt signal. Shutting down background process...")
slog.Info("Received signal, cleanup via defer sshCon.Close() will run.") slog.Info("Received signal, cleanup via defer sshCon.Close() will run.")
if err := a.timeStore.StopTracking(withoutTimew); err != nil { if err := a.StopTracking(withoutTimew); err != nil {
slog.Warn(fmt.Sprintf("Failed to stop time tracking: %v", err)) slog.Warn(fmt.Sprintf("Failed to stop time tracking: %v", err))
} else { } else {
slog.Info("Time tracking stopped.") slog.Info("Time tracking stopped.")
@ -86,7 +145,7 @@ Use --background (-b) to keep tunnels running in the background without auto-con
fmt.Println("Workstation SSH session finished.") fmt.Println("Workstation SSH session finished.")
slog.Info("Foreground session ended, cleanup via defer sshCon.Close() will run.") slog.Info("Foreground session ended, cleanup via defer sshCon.Close() will run.")
if err := a.timeStore.StopTracking(withoutTimew); err != nil { if err := a.StopTracking(withoutTimew); err != nil {
slog.Warn(fmt.Sprintf("Failed to stop time tracking: %v", err)) slog.Warn(fmt.Sprintf("Failed to stop time tracking: %v", err))
} else { } else {
slog.Info("Time tracking stopped.") slog.Info("Time tracking stopped.")
@ -110,7 +169,7 @@ func (a *App) stopCommand() *cobra.Command {
Long: "Stops the current time tracking entry and attempts to kill active SSH tunnels.", Long: "Stops the current time tracking entry and attempts to kill active SSH tunnels.",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Stopping workday procedures...") fmt.Println("Stopping workday procedures...")
if err := a.timeStore.StopTracking(withoutTimew); err != nil { if err := a.StopTracking(withoutTimew); err != nil {
slog.Error(fmt.Sprintf("Failed to stop time tracking: %v", err)) slog.Error(fmt.Sprintf("Failed to stop time tracking: %v", err))
} else { } else {
fmt.Println("Time tracking stopped.") fmt.Println("Time tracking stopped.")
@ -164,7 +223,7 @@ This also stops any currently running timer.`,
default: default:
fmt.Printf("Attempting to start tracking interval '%s'...\n", tag) fmt.Printf("Attempting to start tracking interval '%s'...\n", tag)
if err := a.timeStore.StartTracking(tag, withoutTimew); err != nil { if err := a.StartTracking(tag, withoutTimew); err != nil {
slog.Error(fmt.Sprintf("Failed to start tracking '%s': %v", tag, err)) slog.Error(fmt.Sprintf("Failed to start tracking '%s': %v", tag, err))
return fmt.Errorf("could not start tracking '%s': %w", tag, err) return fmt.Errorf("could not start tracking '%s': %w", tag, err)
} }
@ -178,7 +237,7 @@ This also stops any currently running timer.`,
Short: "Start tracking 'break'", Short: "Start tracking 'break'",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Starting break...") fmt.Println("Starting break...")
if err := a.timeStore.StartTracking(TagBreak, withoutTimew); err != nil { if err := a.StartTracking(TagBreak, withoutTimew); err != nil {
slog.Error(fmt.Sprintf("Failed to start break tracking: %v", err)) slog.Error(fmt.Sprintf("Failed to start break tracking: %v", err))
return fmt.Errorf("could not start break: %w", err) return fmt.Errorf("could not start break: %w", err)
} }

View file

@ -68,5 +68,18 @@ func loadConfig() (Config, error) {
cfg.SSHPort = 22 cfg.SSHPort = 22
} }
if cfg.SSHPassword == "" {
if secret, err := getSecret(keySSHPassword); err == nil {
cfg.SSHPassword = secret
slog.Debug("Loaded SSH password from keyring.")
}
}
if cfg.RDPPassword == "" {
if secret, err := getSecret(keyRDPPassword); err == nil {
cfg.RDPPassword = secret
slog.Debug("Loaded RDP password from keyring.")
}
}
return cfg, nil return cfg, nil
} }

4
go.mod
View file

@ -13,6 +13,7 @@ require (
) )
require ( require (
al.essio.dev/pkg/shellescape v1.5.1 // indirect
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect github.com/catppuccin/go v0.3.0 // indirect
@ -27,10 +28,12 @@ require (
github.com/clipperhouse/displaywidth v0.6.2 // indirect github.com/clipperhouse/displaywidth v0.6.2 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/danieljoos/wincred v1.2.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
@ -57,6 +60,7 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/xuri/efp v0.0.1 // indirect github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
github.com/zalando/go-keyring v0.2.6 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect

8
go.sum
View file

@ -1,3 +1,5 @@
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
@ -47,6 +49,8 @@ github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEX
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -64,6 +68,8 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
@ -173,6 +179,8 @@ github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=

View file

@ -1,18 +0,0 @@
package main
import (
"log/slog"
"os"
"os/exec"
)
func runCommand(name string, args ...string) {
cmd := exec.Command(name, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
err := cmd.Run()
if err != nil {
slog.Error("Command execution error", "command", name, "args", args, "error", err)
}
}

28
secrets.go Normal file
View file

@ -0,0 +1,28 @@
package main
import (
"fmt"
"github.com/zalando/go-keyring"
)
const (
serviceName = "workctl"
keySSHPassword = "ssh-password"
keyRDPPassword = "rdp-password"
)
func getSecret(key string) (string, error) {
val, err := keyring.Get(serviceName, key)
if err != nil {
return "", err
}
return val, nil
}
func setSecret(key, value string) error {
if value == "" {
return fmt.Errorf("secret cannot be empty")
}
return keyring.Set(serviceName, key, value)
}

View file

@ -113,7 +113,7 @@ func (ts *TimeStore) stopCurrentEntry(now time.Time) (bool, error) {
return rowsAffected > 0, nil return rowsAffected > 0, nil
} }
func (ts *TimeStore) StartTracking(tag string, withoutTimew bool) error { func (ts *TimeStore) StartTracking(tag string) error {
if tag == "" { if tag == "" {
return fmt.Errorf("cannot start tracking with an empty tag") return fmt.Errorf("cannot start tracking with an empty tag")
} }
@ -126,9 +126,6 @@ func (ts *TimeStore) StartTracking(tag string, withoutTimew bool) error {
if stopped { if stopped {
slog.Info("Stopped previous time entry.") slog.Info("Stopped previous time entry.")
} }
if !withoutTimew {
runCommand("timew", "start", "work")
}
query := `INSERT INTO time_entries (tag, start_time, end_time) VALUES (?, ?, NULL);` query := `INSERT INTO time_entries (tag, start_time, end_time) VALUES (?, ?, NULL);`
_, err = ts.db.Exec(query, tag, now) _, err = ts.db.Exec(query, tag, now)
@ -139,15 +136,12 @@ func (ts *TimeStore) StartTracking(tag string, withoutTimew bool) error {
return nil return nil
} }
func (ts *TimeStore) StopTracking(withoutTimew bool) error { func (ts *TimeStore) StopTracking() error {
now := time.Now() now := time.Now()
stopped, err := ts.stopCurrentEntry(now) stopped, err := ts.stopCurrentEntry(now)
if err != nil { if err != nil {
return err return err
} }
if !withoutTimew {
runCommand("timew", "stop", "work")
}
if stopped { if stopped {
slog.Info(fmt.Sprintf("Stopped tracking at %s", now.Format(time.RFC3339))) slog.Info(fmt.Sprintf("Stopped tracking at %s", now.Format(time.RFC3339)))
} else { } else {