All checks were successful
Tests / unit-tests (push) Successful in 45s
- Ispravljen deadlock: SetPassword zaključa mu pa pozove save() koji opet zaključa mu. Razdvojeno u saveLocked() (bez lock-a) i save() - Vraćeno normalno ponašanje kursora (Claude Code sam upravlja) - PTY okruženje: TERM=xterm-256color, COLORTERM=truecolor Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
117 lines
2.8 KiB
Go
117 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
type APIConfig struct {
|
|
Key string `json:"key"`
|
|
Model string `json:"model"`
|
|
}
|
|
|
|
type Config struct {
|
|
Port int `json:"port"`
|
|
Mode string `json:"mode"`
|
|
ProjectsPath string `json:"projects_path"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
SessionSecret string `json:"session_secret"`
|
|
API APIConfig `json:"api"`
|
|
|
|
path string // putanja do config fajla (ne serijalizuje se)
|
|
mu sync.Mutex // zaštita za upis
|
|
}
|
|
|
|
func LoadConfig(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading config: %w", err)
|
|
}
|
|
|
|
var cfg Config
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
return nil, fmt.Errorf("parsing config: %w", err)
|
|
}
|
|
|
|
if cfg.Port == 0 {
|
|
cfg.Port = 9100
|
|
}
|
|
if cfg.Mode == "" {
|
|
cfg.Mode = "cli"
|
|
}
|
|
if cfg.ProjectsPath == "" {
|
|
cfg.ProjectsPath = "/root/projects"
|
|
}
|
|
if cfg.Username == "" {
|
|
return nil, fmt.Errorf("username is required")
|
|
}
|
|
if cfg.Password == "" {
|
|
return nil, fmt.Errorf("password is required")
|
|
}
|
|
if cfg.SessionSecret == "" {
|
|
return nil, fmt.Errorf("session_secret is required")
|
|
}
|
|
|
|
cfg.path = path
|
|
|
|
// Ako šifra nije hešovana, heširaj je i sačuvaj
|
|
if !isHashed(cfg.Password) {
|
|
hashed, err := bcrypt.GenerateFromPassword([]byte(cfg.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hashing password: %w", err)
|
|
}
|
|
cfg.Password = string(hashed)
|
|
if err := cfg.save(); err != nil {
|
|
return nil, fmt.Errorf("saving hashed password: %w", err)
|
|
}
|
|
}
|
|
|
|
return &cfg, nil
|
|
}
|
|
|
|
// CheckPassword proverava da li se data šifra poklapa sa hešom.
|
|
func (c *Config) CheckPassword(password string) bool {
|
|
return bcrypt.CompareHashAndPassword([]byte(c.Password), []byte(password)) == nil
|
|
}
|
|
|
|
// SetPassword postavlja novu šifru (hešuje i čuva u config).
|
|
func (c *Config) SetPassword(newPassword string) error {
|
|
hashed, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return fmt.Errorf("hashing password: %w", err)
|
|
}
|
|
c.mu.Lock()
|
|
c.Password = string(hashed)
|
|
err = c.saveLocked()
|
|
c.mu.Unlock()
|
|
return err
|
|
}
|
|
|
|
// save upisuje trenutni config nazad u fajl (pozivalac mora držati c.mu).
|
|
func (c *Config) saveLocked() error {
|
|
data, err := json.MarshalIndent(c, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("marshaling config: %w", err)
|
|
}
|
|
data = append(data, '\n')
|
|
return os.WriteFile(c.path, data, 0600)
|
|
}
|
|
|
|
// save upisuje trenutni config nazad u fajl.
|
|
func (c *Config) save() error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.saveLocked()
|
|
}
|
|
|
|
// isHashed proverava da li je string bcrypt heš.
|
|
func isHashed(s string) bool {
|
|
return strings.HasPrefix(s, "$2a$") || strings.HasPrefix(s, "$2b$")
|
|
}
|