/* * Copyright 2026 Safronov Grigorii * * Licensed under the CDDL, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * * You may obtain a copy of the License at * https://opensource.org/licenses/CDDL-1.0 */ // Файл: internal/api/webui_credentials.go // Назначение: Управление учётными данными для веб-интерфейса // Хранит логин/пароль в скрытом файле .credentials в директории futriis package api import ( "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "os" "path/filepath" "sync" ) // CredentialManager управляет учётными данными веб-интерфейса type CredentialManager struct { mu sync.RWMutex credentials *Credentials credFile string currentUser string } // Credentials структура для хранения учётных данных type Credentials struct { Username string `json:"username"` Password string `json:"password"` // хранится в виде хеша Avatar map[string]string `json:"avatar,omitempty"` // username -> base64 avatar } // NewCredentialManager создаёт новый менеджер учётных данных func NewCredentialManager() *CredentialManager { // Определяем путь к директории futriis execPath, err := os.Executable() if err != nil { execPath = "." } futriisDir := filepath.Dir(execPath) // Ищем директорию futriis (поднимаемся вверх, если нужно) for { if _, err := os.Stat(filepath.Join(futriisDir, "futriis")); err == nil { futriisDir = filepath.Join(futriisDir, "futriis") break } parent := filepath.Dir(futriisDir) if parent == futriisDir { // Не нашли директорию futriis, создаём в текущей futriisDir = filepath.Join(execPath, "futriis") os.MkdirAll(futriisDir, 0700) break } futriisDir = parent } credFile := filepath.Join(futriisDir, ".credentials") return &CredentialManager{ credentials: &Credentials{ Username: "admin", Password: "", Avatar: make(map[string]string), }, credFile: credFile, } } // hashPassword создаёт хеш пароля func (cm *CredentialManager) hashPassword(password string) string { hash := sha256.Sum256([]byte(password)) return base64.StdEncoding.EncodeToString(hash[:]) } // Load загружает учётные данные из файла func (cm *CredentialManager) Load() error { cm.mu.Lock() defer cm.mu.Unlock() data, err := os.ReadFile(cm.credFile) if err != nil { if os.IsNotExist(err) { return cm.createDefaultFile() } return err } var creds Credentials if err := json.Unmarshal(data, &creds); err != nil { return err } cm.credentials = &creds if cm.credentials.Avatar == nil { cm.credentials.Avatar = make(map[string]string) } return nil } // createDefaultFile создаёт файл с учётными данными по умолчанию func (cm *CredentialManager) createDefaultFile() error { cm.credentials.Username = "admin" cm.credentials.Password = cm.hashPassword("admin") cm.credentials.Avatar = make(map[string]string) return cm.save() } // CreateDefault создаёт учётные данные по умолчанию (если файл не существует) func (cm *CredentialManager) CreateDefault() error { cm.mu.Lock() defer cm.mu.Unlock() // Проверяем, существует ли файл if _, err := os.Stat(cm.credFile); err == nil { return nil // файл уже существует } cm.credentials.Username = "admin" cm.credentials.Password = cm.hashPassword("admin") cm.credentials.Avatar = make(map[string]string) return cm.save() } // save сохраняет учётные данные в файл func (cm *CredentialManager) save() error { data, err := json.MarshalIndent(cm.credentials, "", " ") if err != nil { return err } // Устанавливаем права доступа 0600 (только владелец может читать/писать) return os.WriteFile(cm.credFile, data, 0600) } // Validate проверяет учётные данные func (cm *CredentialManager) Validate(username, password string) bool { cm.mu.RLock() defer cm.mu.RUnlock() if cm.credentials == nil { return false } if cm.credentials.Username != username { return false } return cm.credentials.Password == cm.hashPassword(password) } // ChangePassword изменяет пароль пользователя func (cm *CredentialManager) ChangePassword(username, currentPassword, newPassword string) error { cm.mu.Lock() defer cm.mu.Unlock() if cm.credentials == nil { return fmt.Errorf("credentials not loaded") } if cm.credentials.Username != username { return fmt.Errorf("user not found") } if cm.credentials.Password != cm.hashPassword(currentPassword) { return fmt.Errorf("current password is incorrect") } if len(newPassword) < 4 { return fmt.Errorf("new password must be at least 4 characters") } cm.credentials.Password = cm.hashPassword(newPassword) return cm.save() } // SetAvatar устанавливает аватар для пользователя func (cm *CredentialManager) SetAvatar(username, avatarBase64 string) error { cm.mu.Lock() defer cm.mu.Unlock() if cm.credentials == nil { return fmt.Errorf("credentials not loaded") } if cm.credentials.Username != username { return fmt.Errorf("user not found") } if cm.credentials.Avatar == nil { cm.credentials.Avatar = make(map[string]string) } cm.credentials.Avatar[username] = avatarBase64 return cm.save() } // GetAvatar возвращает аватар пользователя func (cm *CredentialManager) GetAvatar(username string) (string, error) { cm.mu.RLock() defer cm.mu.RUnlock() if cm.credentials == nil { return "", fmt.Errorf("credentials not loaded") } if cm.credentials.Username != username { return "", fmt.Errorf("user not found") } if cm.credentials.Avatar == nil { return "", nil } avatar, ok := cm.credentials.Avatar[username] if !ok { return "", nil } return avatar, nil } // DeleteAvatar удаляет аватар пользователя func (cm *CredentialManager) DeleteAvatar(username string) error { cm.mu.Lock() defer cm.mu.Unlock() if cm.credentials == nil { return fmt.Errorf("credentials not loaded") } if cm.credentials.Username != username { return fmt.Errorf("user not found") } if cm.credentials.Avatar != nil { delete(cm.credentials.Avatar, username) } return cm.save() } // GetCurrentUsername возвращает имя текущего пользователя func (cm *CredentialManager) GetCurrentUsername() string { cm.mu.RLock() defer cm.mu.RUnlock() if cm.credentials == nil { return "" } return cm.credentials.Username } // GetCredentialsFile возвращает путь к файлу с учётными данными func (cm *CredentialManager) GetCredentialsFile() string { return cm.credFile }