Upload files to "internal/api"

This commit is contained in:
2026-05-17 14:30:46 +00:00
parent 1fb8b8f7a2
commit a05377748b
2 changed files with 3053 additions and 0 deletions

2628
internal/api/webui.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,425 @@
/*
* 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
}