Files
futriix/internal/api/webui_credentials.go

426 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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"
"time"
)
// UserCredential представляет учётные данные одного пользователя
type UserCredential struct {
Password string `json:"password"` // хранится в виде хеша
Avatar string `json:"avatar,omitempty"` // base64 encoded avatar
IsAdmin bool `json:"is_admin"` // является ли администратором
CreatedAt int64 `json:"created_at"` // время создания
LastLogin int64 `json:"last_login,omitempty"` // время последнего входа
}
// Credentials структура для хранения всех учётных данных
type Credentials struct {
Users map[string]*UserCredential `json:"users"`
Settings map[string]interface{} `json:"settings,omitempty"`
}
// CredentialManager управляет учётными данными веб-интерфейса
type CredentialManager struct {
mu sync.RWMutex
credentials *Credentials
credFile string
currentUser string
}
// 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{
Users: make(map[string]*UserCredential),
Settings: make(map[string]interface{}),
},
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.Users == nil {
cm.credentials.Users = make(map[string]*UserCredential)
}
if cm.credentials.Settings == nil {
cm.credentials.Settings = make(map[string]interface{})
}
return nil
}
// createDefaultFile создаёт файл с учётными данными по умолчанию
func (cm *CredentialManager) createDefaultFile() error {
// Создаём пользователя admin по умолчанию
cm.credentials.Users["admin"] = &UserCredential{
Password: cm.hashPassword("admin"),
IsAdmin: true,
CreatedAt: time.Now().Unix(),
}
cm.credentials.Settings["version"] = "1.0"
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.Users["admin"] = &UserCredential{
Password: cm.hashPassword("admin"),
IsAdmin: true,
CreatedAt: time.Now().Unix(),
}
cm.credentials.Settings["version"] = "1.0"
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 || cm.credentials.Users == nil {
return false
}
user, exists := cm.credentials.Users[username]
if !exists {
return false
}
return user.Password == cm.hashPassword(password)
}
// IsAdmin проверяет, является ли пользователь администратором
func (cm *CredentialManager) IsAdmin(username string) bool {
cm.mu.RLock()
defer cm.mu.RUnlock()
if cm.credentials == nil || cm.credentials.Users == nil {
return false
}
user, exists := cm.credentials.Users[username]
if !exists {
return false
}
return user.IsAdmin
}
// CreateUser создаёт нового пользователя (только для администраторов)
func (cm *CredentialManager) CreateUser(username, password string, isAdmin bool) error {
if username == "" {
return fmt.Errorf("username cannot be empty")
}
if len(password) < 4 {
return fmt.Errorf("password must be at least 4 characters")
}
cm.mu.Lock()
defer cm.mu.Unlock()
if cm.credentials == nil {
cm.credentials = &Credentials{
Users: make(map[string]*UserCredential),
Settings: make(map[string]interface{}),
}
}
if _, exists := cm.credentials.Users[username]; exists {
return fmt.Errorf("user %s already exists", username)
}
cm.credentials.Users[username] = &UserCredential{
Password: cm.hashPassword(password),
IsAdmin: isAdmin,
CreatedAt: time.Now().Unix(),
}
return cm.save()
}
// DeleteUser удаляет пользователя (только для администраторов)
func (cm *CredentialManager) DeleteUser(username string) error {
if username == "admin" {
return fmt.Errorf("cannot delete default admin user")
}
cm.mu.Lock()
defer cm.mu.Unlock()
if cm.credentials == nil || cm.credentials.Users == nil {
return fmt.Errorf("no users found")
}
if _, exists := cm.credentials.Users[username]; !exists {
return fmt.Errorf("user %s not found", username)
}
delete(cm.credentials.Users, username)
return cm.save()
}
// ListUsers возвращает список всех пользователей (только для администраторов)
func (cm *CredentialManager) ListUsers() []map[string]interface{} {
cm.mu.RLock()
defer cm.mu.RUnlock()
users := make([]map[string]interface{}, 0)
for username, user := range cm.credentials.Users {
users = append(users, map[string]interface{}{
"username": username,
"is_admin": user.IsAdmin,
"created_at": user.CreatedAt,
"last_login": user.LastLogin,
"has_avatar": user.Avatar != "",
})
}
return users
}
// ChangePassword изменяет пароль пользователя
func (cm *CredentialManager) ChangePassword(username, currentPassword, newPassword string) error {
cm.mu.Lock()
defer cm.mu.Unlock()
if cm.credentials == nil || cm.credentials.Users == nil {
return fmt.Errorf("credentials not loaded")
}
user, exists := cm.credentials.Users[username]
if !exists {
return fmt.Errorf("user not found")
}
if user.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")
}
user.Password = cm.hashPassword(newPassword)
return cm.save()
}
// AdminChangePassword изменяет пароль пользователя (без проверки старого, только для админов)
func (cm *CredentialManager) AdminChangePassword(username, newPassword string) error {
cm.mu.Lock()
defer cm.mu.Unlock()
if cm.credentials == nil || cm.credentials.Users == nil {
return fmt.Errorf("credentials not loaded")
}
user, exists := cm.credentials.Users[username]
if !exists {
return fmt.Errorf("user not found")
}
if len(newPassword) < 4 {
return fmt.Errorf("password must be at least 4 characters")
}
user.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 || cm.credentials.Users == nil {
return fmt.Errorf("credentials not loaded")
}
user, exists := cm.credentials.Users[username]
if !exists {
return fmt.Errorf("user not found")
}
user.Avatar = avatarBase64
return cm.save()
}
// GetAvatar возвращает аватар пользователя
func (cm *CredentialManager) GetAvatar(username string) (string, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
if cm.credentials == nil || cm.credentials.Users == nil {
return "", fmt.Errorf("credentials not loaded")
}
user, exists := cm.credentials.Users[username]
if !exists {
return "", fmt.Errorf("user not found")
}
return user.Avatar, nil
}
// DeleteAvatar удаляет аватар пользователя
func (cm *CredentialManager) DeleteAvatar(username string) error {
cm.mu.Lock()
defer cm.mu.Unlock()
if cm.credentials == nil || cm.credentials.Users == nil {
return fmt.Errorf("credentials not loaded")
}
user, exists := cm.credentials.Users[username]
if !exists {
return fmt.Errorf("user not found")
}
user.Avatar = ""
return cm.save()
}
// UpdateLastLogin обновляет время последнего входа пользователя
func (cm *CredentialManager) UpdateLastLogin(username string) {
cm.mu.Lock()
defer cm.mu.Unlock()
if cm.credentials == nil || cm.credentials.Users == nil {
return
}
user, exists := cm.credentials.Users[username]
if !exists {
return
}
user.LastLogin = time.Now().Unix()
cm.save() // игнорируем ошибку, т.к. это не критично
}
// GetCurrentUsername возвращает имя текущего пользователя
func (cm *CredentialManager) GetCurrentUsername() string {
cm.mu.RLock()
defer cm.mu.RUnlock()
if cm.credentials == nil {
return ""
}
// Возвращаем последнего вошедшего пользователя или admin по умолчанию
if cm.currentUser != "" {
return cm.currentUser
}
return "admin"
}
// SetCurrentUsername устанавливает имя текущего пользователя
func (cm *CredentialManager) SetCurrentUsername(username string) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.currentUser = username
}
// GetCredentialsFile возвращает путь к файлу с учётными данными
func (cm *CredentialManager) GetCredentialsFile() string {
return cm.credFile
}