feat: improve security
This commit is contained in:
parent
99fb97dff3
commit
5b16cef525
8 changed files with 181 additions and 46 deletions
77
app.go
77
app.go
|
|
@ -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
69
cmd.go
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
config.go
13
config.go
|
|
@ -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
4
go.mod
|
|
@ -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
8
go.sum
|
|
@ -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=
|
||||||
|
|
|
||||||
18
helpers.go
18
helpers.go
|
|
@ -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
28
secrets.go
Normal 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)
|
||||||
|
}
|
||||||
10
store.go
10
store.go
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue