mirror of
https://github.com/Direct-Dev-Ru/go-lcg.git
synced 2025-11-16 01:29:55 +00:00
Исправления в ветке auth-feature
This commit is contained in:
269
serve/auth.go
Normal file
269
serve/auth.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// JWTClaims представляет claims для JWT токена
|
||||
type JWTClaims struct {
|
||||
Username string `json:"username"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// AuthRequest представляет запрос на аутентификацию
|
||||
type AuthRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// AuthResponse представляет ответ на аутентификацию
|
||||
type AuthResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// JWTSecretKey генерирует или загружает секретный ключ для JWT
|
||||
func getJWTSecretKey() ([]byte, error) {
|
||||
// Пытаемся загрузить из переменной окружения
|
||||
if secret := os.Getenv("LCG_JWT_SECRET"); secret != "" {
|
||||
return []byte(secret), nil
|
||||
}
|
||||
|
||||
// Пытаемся загрузить из файла
|
||||
secretFile := fmt.Sprintf("%s/server/jwt_secret", config.AppConfig.Server.ConfigFolder)
|
||||
if data, err := os.ReadFile(secretFile); err == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Генерируем новый секретный ключ
|
||||
secret := make([]byte, 32)
|
||||
if _, err := rand.Read(secret); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate JWT secret: %v", err)
|
||||
}
|
||||
|
||||
// Создаем директорию если не существует
|
||||
if err := os.MkdirAll(fmt.Sprintf("%s/server", config.AppConfig.Server.ConfigFolder), 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create config directory: %v", err)
|
||||
}
|
||||
|
||||
// Сохраняем секретный ключ в файл
|
||||
if err := os.WriteFile(secretFile, secret, 0600); err != nil {
|
||||
return nil, fmt.Errorf("failed to save JWT secret: %v", err)
|
||||
}
|
||||
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
// generateJWTToken создает JWT токен для пользователя
|
||||
func generateJWTToken(username string) (string, error) {
|
||||
secret, err := getJWTSecretKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Создаем claims
|
||||
claims := JWTClaims{
|
||||
Username: username,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // Токен действителен 24 часа
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "lcg-server",
|
||||
},
|
||||
}
|
||||
|
||||
// Создаем токен
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(secret)
|
||||
}
|
||||
|
||||
// validateJWTToken проверяет JWT токен
|
||||
func validateJWTToken(tokenString string) (*JWTClaims, error) {
|
||||
secret, err := getJWTSecretKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Парсим токен
|
||||
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
// Проверяем метод подписи
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return secret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем валидность токена
|
||||
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
// getTokenFromCookie извлекает JWT токен из cookies
|
||||
func getTokenFromCookie(r *http.Request) (string, error) {
|
||||
cookie, err := r.Cookie("auth_token")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return cookie.Value, nil
|
||||
}
|
||||
|
||||
// setAuthCookie устанавливает HTTP-only cookie с JWT токеном
|
||||
func setAuthCookie(w http.ResponseWriter, token string) {
|
||||
cookie := &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Domain: config.AppConfig.Server.Domain,
|
||||
Value: token,
|
||||
Path: config.AppConfig.Server.CookiePath,
|
||||
HttpOnly: true,
|
||||
Secure: config.AppConfig.Server.CookieSecure,
|
||||
SameSite: http.SameSiteLaxMode, // Более мягкий режим для reverse proxy
|
||||
MaxAge: config.AppConfig.Server.CookieTTLHours * 60 * 60,
|
||||
}
|
||||
|
||||
// Добавляем домен если указан
|
||||
if config.AppConfig.Server.Domain != "" {
|
||||
cookie.Domain = config.AppConfig.Server.Domain
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
// clearAuthCookie удаляет cookie с токеном
|
||||
func clearAuthCookie(w http.ResponseWriter) {
|
||||
cookie := &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Value: "",
|
||||
Path: config.AppConfig.Server.CookiePath,
|
||||
HttpOnly: true,
|
||||
Secure: config.AppConfig.Server.CookieSecure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: -1, // Удаляем cookie
|
||||
}
|
||||
|
||||
// Добавляем домен если указан
|
||||
if config.AppConfig.Server.Domain != "" {
|
||||
cookie.Domain = config.AppConfig.Server.Domain
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
// handleLogin обрабатывает запрос на вход
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req AuthRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
apiJsonResponse(w, AuthResponse{
|
||||
Success: false,
|
||||
Error: "Invalid request body",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем пароль
|
||||
if req.Password != config.AppConfig.Server.Password {
|
||||
apiJsonResponse(w, AuthResponse{
|
||||
Success: false,
|
||||
Error: "Неверный пароль",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Генерируем JWT токен
|
||||
token, err := generateJWTToken(req.Username)
|
||||
if err != nil {
|
||||
apiJsonResponse(w, AuthResponse{
|
||||
Success: false,
|
||||
Error: "Failed to generate token",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Устанавливаем cookie
|
||||
setAuthCookie(w, token)
|
||||
|
||||
apiJsonResponse(w, AuthResponse{
|
||||
Success: true,
|
||||
Message: "Успешная авторизация",
|
||||
})
|
||||
}
|
||||
|
||||
// handleLogout обрабатывает запрос на выход
|
||||
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
clearAuthCookie(w)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleValidateToken обрабатывает проверку валидности токена
|
||||
func handleValidateToken(w http.ResponseWriter, r *http.Request) {
|
||||
token, err := getTokenFromCookie(r)
|
||||
if err != nil {
|
||||
apiJsonResponse(w, AuthResponse{
|
||||
Success: false,
|
||||
Error: "Token not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
_, err = validateJWTToken(token)
|
||||
if err != nil {
|
||||
apiJsonResponse(w, AuthResponse{
|
||||
Success: false,
|
||||
Error: "Invalid token",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
apiJsonResponse(w, AuthResponse{
|
||||
Success: true,
|
||||
Message: "Token is valid",
|
||||
})
|
||||
}
|
||||
|
||||
// requireAuth middleware проверяет аутентификацию
|
||||
func requireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Проверяем, требуется ли аутентификация
|
||||
if !config.AppConfig.Server.RequireAuth {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем токен из cookie
|
||||
token, err := getTokenFromCookie(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем валидность токена
|
||||
_, err = validateJWTToken(token)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Токен валиден, продолжаем
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
263
serve/csrf.go
Normal file
263
serve/csrf.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
)
|
||||
|
||||
// CSRFManager управляет CSRF токенами
|
||||
type CSRFManager struct {
|
||||
secretKey []byte
|
||||
}
|
||||
|
||||
// CSRFData содержит данные для CSRF токена
|
||||
type CSRFData struct {
|
||||
Token string
|
||||
Timestamp int64
|
||||
UserID string
|
||||
}
|
||||
|
||||
// NewCSRFManager создает новый менеджер CSRF
|
||||
func NewCSRFManager() (*CSRFManager, error) {
|
||||
secret, err := getCSRFSecretKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &CSRFManager{secretKey: secret}, nil
|
||||
}
|
||||
|
||||
// getCSRFSecretKey получает или генерирует секретный ключ для CSRF
|
||||
func getCSRFSecretKey() ([]byte, error) {
|
||||
// Пытаемся загрузить из переменной окружения
|
||||
if secret := os.Getenv("LCG_CSRF_SECRET"); secret != "" {
|
||||
return []byte(secret), nil
|
||||
}
|
||||
|
||||
// Пытаемся загрузить из файла
|
||||
secretFile := fmt.Sprintf("%s/server/csrf_secret", config.AppConfig.Server.ConfigFolder)
|
||||
if data, err := os.ReadFile(secretFile); err == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Генерируем новый секретный ключ
|
||||
secret := make([]byte, 32)
|
||||
if _, err := rand.Read(secret); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate CSRF secret: %v", err)
|
||||
}
|
||||
|
||||
// Создаем директорию если не существует
|
||||
if err := os.MkdirAll(fmt.Sprintf("%s/server", config.AppConfig.Server.ConfigFolder), 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create config directory: %v", err)
|
||||
}
|
||||
|
||||
// Сохраняем секретный ключ в файл
|
||||
if err := os.WriteFile(secretFile, secret, 0600); err != nil {
|
||||
return nil, fmt.Errorf("failed to save CSRF secret: %v", err)
|
||||
}
|
||||
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
// GenerateToken генерирует CSRF токен для пользователя
|
||||
func (c *CSRFManager) GenerateToken(userID string) (string, error) {
|
||||
// Создаем данные токена
|
||||
data := CSRFData{
|
||||
Token: generateRandomString(32),
|
||||
Timestamp: time.Now().Unix(),
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
// Создаем подпись
|
||||
signature := c.createSignature(data)
|
||||
|
||||
// Кодируем данные в base64
|
||||
encodedData := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%d:%s", data.Token, data.Timestamp, data.UserID)))
|
||||
|
||||
return fmt.Sprintf("%s.%s", encodedData, signature), nil
|
||||
}
|
||||
|
||||
// ValidateToken проверяет CSRF токен
|
||||
func (c *CSRFManager) ValidateToken(token, userID string) bool {
|
||||
// Разделяем токен на данные и подпись
|
||||
parts := splitToken(token)
|
||||
if len(parts) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
encodedData, signature := parts[0], parts[1]
|
||||
|
||||
// Декодируем данные
|
||||
dataBytes, err := base64.StdEncoding.DecodeString(encodedData)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Парсим данные
|
||||
dataParts := splitString(string(dataBytes), ":")
|
||||
if len(dataParts) != 3 {
|
||||
return false
|
||||
}
|
||||
|
||||
tokenValue, timestampStr, tokenUserID := dataParts[0], dataParts[1], dataParts[2]
|
||||
|
||||
// Проверяем пользователя
|
||||
if tokenUserID != userID {
|
||||
return false
|
||||
}
|
||||
|
||||
// Проверяем время жизни токена (24 часа)
|
||||
timestamp, err := parseInt64(timestampStr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if time.Now().Unix()-timestamp > 24*60*60 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Создаем данные для проверки подписи
|
||||
data := CSRFData{
|
||||
Token: tokenValue,
|
||||
Timestamp: timestamp,
|
||||
UserID: tokenUserID,
|
||||
}
|
||||
|
||||
// Проверяем подпись
|
||||
expectedSignature := c.createSignature(data)
|
||||
return signature == expectedSignature
|
||||
}
|
||||
|
||||
// createSignature создает подпись для данных
|
||||
func (c *CSRFManager) createSignature(data CSRFData) string {
|
||||
message := fmt.Sprintf("%s:%d:%s", data.Token, data.Timestamp, data.UserID)
|
||||
hash := sha256.Sum256(append(c.secretKey, []byte(message)...))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// getTokenFromCookie извлекает CSRF токен из cookie
|
||||
func GetCSRFTokenFromCookie(r *http.Request) string {
|
||||
cookie, err := r.Cookie("csrf_token")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return cookie.Value
|
||||
}
|
||||
|
||||
// setCSRFCookie устанавливает CSRF токен в cookie
|
||||
func setCSRFCookie(w http.ResponseWriter, token string) {
|
||||
cookie := &http.Cookie{
|
||||
Name: "csrf_token",
|
||||
Value: token,
|
||||
Path: config.AppConfig.Server.CookiePath,
|
||||
HttpOnly: true,
|
||||
Secure: config.AppConfig.Server.CookieSecure,
|
||||
SameSite: http.SameSiteLaxMode, // Более мягкий режим для reverse proxy
|
||||
MaxAge: 1 * 60 * 60,
|
||||
}
|
||||
|
||||
// Добавляем домен если указан
|
||||
if config.AppConfig.Server.Domain != "" {
|
||||
cookie.Domain = config.AppConfig.Server.Domain
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
// clearCSRFCookie удаляет CSRF cookie
|
||||
func СlearCSRFCookie(w http.ResponseWriter) {
|
||||
cookie := &http.Cookie{
|
||||
Name: "csrf_token",
|
||||
Value: "",
|
||||
Path: config.AppConfig.Server.CookiePath,
|
||||
HttpOnly: true,
|
||||
Secure: config.AppConfig.Server.CookieSecure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: -1,
|
||||
}
|
||||
|
||||
// Добавляем домен если указан
|
||||
if config.AppConfig.Server.Domain != "" {
|
||||
cookie.Domain = config.AppConfig.Server.Domain
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
// generateRandomString генерирует случайную строку
|
||||
func generateRandomString(length int) string {
|
||||
bytes := make([]byte, length)
|
||||
rand.Read(bytes)
|
||||
return base64.URLEncoding.EncodeToString(bytes)[:length]
|
||||
}
|
||||
|
||||
// splitToken разделяет токен на части
|
||||
func splitToken(token string) []string {
|
||||
// Ищем последнюю точку
|
||||
lastDot := -1
|
||||
for i := len(token) - 1; i >= 0; i-- {
|
||||
if token[i] == '.' {
|
||||
lastDot = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if lastDot == -1 {
|
||||
return []string{token}
|
||||
}
|
||||
|
||||
return []string{token[:lastDot], token[lastDot+1:]}
|
||||
}
|
||||
|
||||
// splitString разделяет строку по разделителю
|
||||
func splitString(s, sep string) []string {
|
||||
if s == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
var result []string
|
||||
start := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
if i+len(sep) <= len(s) && s[i:i+len(sep)] == sep {
|
||||
result = append(result, s[start:i])
|
||||
start = i + len(sep)
|
||||
i += len(sep) - 1
|
||||
}
|
||||
}
|
||||
result = append(result, s[start:])
|
||||
return result
|
||||
}
|
||||
|
||||
// parseInt64 парсит строку в int64
|
||||
func parseInt64(s string) (int64, error) {
|
||||
var result int64
|
||||
for _, char := range s {
|
||||
if char < '0' || char > '9' {
|
||||
return 0, fmt.Errorf("invalid number: %s", s)
|
||||
}
|
||||
result = result*10 + int64(char-'0')
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Глобальный экземпляр CSRF менеджера
|
||||
var csrfManager *CSRFManager
|
||||
|
||||
// InitCSRFManager инициализирует глобальный CSRF менеджер
|
||||
func InitCSRFManager() error {
|
||||
var err error
|
||||
csrfManager, err = NewCSRFManager()
|
||||
return err
|
||||
}
|
||||
|
||||
// GetCSRFManager возвращает глобальный CSRF менеджер
|
||||
func GetCSRFManager() *CSRFManager {
|
||||
return csrfManager
|
||||
}
|
||||
@@ -23,6 +23,12 @@ type ExecutePageData struct {
|
||||
ResultSection template.HTML
|
||||
VerboseButtons template.HTML
|
||||
ActionButtons template.HTML
|
||||
CSRFToken string
|
||||
ProviderType string
|
||||
Model string
|
||||
Host string
|
||||
BasePath string
|
||||
AppName string
|
||||
// Поля конфигурации для валидации
|
||||
MaxUserMessageLength int
|
||||
}
|
||||
@@ -50,7 +56,7 @@ func handleExecutePage(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
// Показываем форму
|
||||
showExecuteForm(w)
|
||||
showExecuteForm(w, r)
|
||||
case http.MethodPost:
|
||||
// Обрабатываем выполнение
|
||||
handleExecuteRequest(w, r)
|
||||
@@ -60,7 +66,25 @@ func handleExecutePage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// showExecuteForm показывает форму выполнения
|
||||
func showExecuteForm(w http.ResponseWriter) {
|
||||
func showExecuteForm(w http.ResponseWriter, r *http.Request) {
|
||||
// Генерируем CSRF токен
|
||||
csrfManager := GetCSRFManager()
|
||||
if csrfManager == nil {
|
||||
http.Error(w, "CSRF manager not initialized", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем сессионный ID
|
||||
sessionID := getSessionID(r)
|
||||
csrfToken, err := csrfManager.GenerateToken(sessionID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Устанавливаем CSRF токен в cookie
|
||||
setCSRFCookie(w, csrfToken)
|
||||
|
||||
// Получаем системные промпты
|
||||
pm := gpt.NewPromptManager(config.AppConfig.PromptFolder)
|
||||
|
||||
@@ -84,6 +108,12 @@ func showExecuteForm(w http.ResponseWriter) {
|
||||
ResultSection: template.HTML(""),
|
||||
VerboseButtons: template.HTML(""),
|
||||
ActionButtons: template.HTML(""),
|
||||
CSRFToken: csrfToken,
|
||||
ProviderType: config.AppConfig.ProviderType,
|
||||
Model: config.AppConfig.Model,
|
||||
Host: config.AppConfig.Host,
|
||||
BasePath: getBasePath(),
|
||||
AppName: config.AppConfig.AppName,
|
||||
MaxUserMessageLength: config.AppConfig.Validation.MaxUserMessageLength,
|
||||
}
|
||||
|
||||
@@ -194,6 +224,15 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Генерируем CSRF токен для результата
|
||||
csrfManager := GetCSRFManager()
|
||||
sessionID := getSessionID(r)
|
||||
csrfToken, err := csrfManager.GenerateToken(sessionID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := ExecutePageData{
|
||||
Title: "Результат выполнения",
|
||||
Header: "Результат выполнения",
|
||||
@@ -202,6 +241,12 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
|
||||
ResultSection: template.HTML(formatResultSection(result)),
|
||||
VerboseButtons: template.HTML(formatVerboseButtons(result)),
|
||||
ActionButtons: template.HTML(formatActionButtons(result)),
|
||||
CSRFToken: csrfToken,
|
||||
ProviderType: config.AppConfig.ProviderType,
|
||||
Model: config.AppConfig.Model,
|
||||
Host: config.AppConfig.Host,
|
||||
BasePath: getBasePath(),
|
||||
AppName: config.AppConfig.AppName,
|
||||
MaxUserMessageLength: config.AppConfig.Validation.MaxUserMessageLength,
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -38,9 +39,13 @@ func handleHistoryPage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Entries []HistoryEntryInfo
|
||||
Entries []HistoryEntryInfo
|
||||
BasePath string
|
||||
AppName string
|
||||
}{
|
||||
Entries: historyEntries,
|
||||
Entries: historyEntries,
|
||||
BasePath: getBasePath(),
|
||||
AppName: config.AppConfig.AppName,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
@@ -54,6 +59,11 @@ func readHistoryEntries() ([]HistoryEntryInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Сортируем записи по времени в убывающем порядке (новые сначала)
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return entries[i].Timestamp.After(entries[j].Timestamp)
|
||||
})
|
||||
|
||||
var result []HistoryEntryInfo
|
||||
for _, entry := range entries {
|
||||
result = append(result, HistoryEntryInfo{
|
||||
@@ -74,7 +84,15 @@ func handleDeleteHistoryEntry(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
indexStr := strings.TrimPrefix(r.URL.Path, "/history/delete/")
|
||||
// Убираем BasePath из URL перед извлечением индекса
|
||||
basePath := config.AppConfig.Server.BasePath
|
||||
var indexStr string
|
||||
if basePath != "" && basePath != "/" {
|
||||
basePath = strings.TrimSuffix(basePath, "/")
|
||||
indexStr = strings.TrimPrefix(r.URL.Path, basePath+"/history/delete/")
|
||||
} else {
|
||||
indexStr = strings.TrimPrefix(r.URL.Path, "/history/delete/")
|
||||
}
|
||||
index, err := strconv.Atoi(indexStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid index", http.StatusBadRequest)
|
||||
@@ -110,8 +128,15 @@ func handleClearHistory(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// handleHistoryView обрабатывает просмотр записи истории
|
||||
func handleHistoryView(w http.ResponseWriter, r *http.Request) {
|
||||
// Получаем индекс из URL
|
||||
indexStr := strings.TrimPrefix(r.URL.Path, "/history/view/")
|
||||
// Получаем индекс из URL, учитывая BasePath
|
||||
basePath := config.AppConfig.Server.BasePath
|
||||
var indexStr string
|
||||
if basePath != "" && basePath != "/" {
|
||||
basePath = strings.TrimSuffix(basePath, "/")
|
||||
indexStr = strings.TrimPrefix(r.URL.Path, basePath+"/history/view/")
|
||||
} else {
|
||||
indexStr = strings.TrimPrefix(r.URL.Path, "/history/view/")
|
||||
}
|
||||
index, err := strconv.Atoi(indexStr)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
@@ -158,12 +183,14 @@ func handleHistoryView(w http.ResponseWriter, r *http.Request) {
|
||||
Command string
|
||||
Response string
|
||||
ExplanationHTML template.HTML
|
||||
BasePath string
|
||||
}{
|
||||
Index: index,
|
||||
Timestamp: targetEntry.Timestamp.Format("02.01.2006 15:04:05"),
|
||||
Command: targetEntry.Command,
|
||||
Response: targetEntry.Response,
|
||||
ExplanationHTML: template.HTML(explanationSection),
|
||||
BasePath: getBasePath(),
|
||||
}
|
||||
|
||||
// Парсим и выполняем шаблон
|
||||
|
||||
@@ -21,9 +21,20 @@ type HistoryEntry struct {
|
||||
// read читает записи истории из файла
|
||||
func Read(historyPath string) ([]HistoryEntry, error) {
|
||||
data, err := os.ReadFile(historyPath)
|
||||
if err != nil || len(data) == 0 {
|
||||
if err != nil {
|
||||
// Если файл не существует, создаем пустой файл истории
|
||||
if os.IsNotExist(err) {
|
||||
emptyHistory := []HistoryEntry{}
|
||||
if writeErr := Write(historyPath, emptyHistory); writeErr != nil {
|
||||
return nil, fmt.Errorf("не удалось создать файл истории: %v", writeErr)
|
||||
}
|
||||
return emptyHistory, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return []HistoryEntry{}, nil
|
||||
}
|
||||
var items []HistoryEntry
|
||||
if err := json.Unmarshal(data, &items); err != nil {
|
||||
return nil, err
|
||||
|
||||
103
serve/login.go
Normal file
103
serve/login.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
||||
)
|
||||
|
||||
// handleLoginPage обрабатывает страницу входа
|
||||
func handleLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
// Если пользователь уже авторизован, перенаправляем на главную
|
||||
if isAuthenticated(r) {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Генерируем CSRF токен
|
||||
csrfManager := GetCSRFManager()
|
||||
if csrfManager == nil {
|
||||
http.Error(w, "CSRF manager not initialized", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Для неавторизованных пользователей используем сессионный ID
|
||||
sessionID := getSessionID(r)
|
||||
csrfToken, err := csrfManager.GenerateToken(sessionID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Устанавливаем CSRF токен в cookie
|
||||
setCSRFCookie(w, csrfToken)
|
||||
|
||||
data := LoginPageData{
|
||||
Title: "Авторизация - LCG",
|
||||
Message: "",
|
||||
Error: "",
|
||||
CSRFToken: csrfToken,
|
||||
}
|
||||
|
||||
if err := RenderLoginPage(w, data); err != nil {
|
||||
http.Error(w, "Failed to render login page", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// isAuthenticated проверяет, авторизован ли пользователь
|
||||
func isAuthenticated(r *http.Request) bool {
|
||||
// Проверяем, требуется ли аутентификация
|
||||
if !config.AppConfig.Server.RequireAuth {
|
||||
return true
|
||||
}
|
||||
|
||||
// Получаем токен из cookie
|
||||
token, err := getTokenFromCookie(r)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Проверяем валидность токена
|
||||
_, err = validateJWTToken(token)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// LoginPageData представляет данные для страницы входа
|
||||
type LoginPageData struct {
|
||||
Title string
|
||||
Message string
|
||||
Error string
|
||||
CSRFToken string
|
||||
}
|
||||
|
||||
// RenderLoginPage рендерит страницу входа
|
||||
func RenderLoginPage(w http.ResponseWriter, data LoginPageData) error {
|
||||
tmpl, err := template.New("login").Parse(templates.LoginPageTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
return tmpl.Execute(w, data)
|
||||
}
|
||||
|
||||
// getSessionID получает или создает сессионный ID для пользователя
|
||||
func getSessionID(r *http.Request) string {
|
||||
// Пытаемся получить из cookie
|
||||
if cookie, err := r.Cookie("session_id"); err == nil {
|
||||
return cookie.Value
|
||||
}
|
||||
|
||||
// Если нет cookie, генерируем новый ID на основе IP и User-Agent
|
||||
ip := r.RemoteAddr
|
||||
userAgent := r.Header.Get("User-Agent")
|
||||
|
||||
// Создаем простой хеш для сессии
|
||||
hash := sha256.Sum256([]byte(ip + userAgent))
|
||||
return hex.EncodeToString(hash[:])[:16]
|
||||
}
|
||||
112
serve/middleware.go
Normal file
112
serve/middleware.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
)
|
||||
|
||||
// AuthMiddleware проверяет аутентификацию для всех запросов
|
||||
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Проверяем, требуется ли аутентификация
|
||||
if !config.AppConfig.Server.RequireAuth {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Исключаем страницу входа и API логина из проверки
|
||||
if r.URL.Path == "/login" || r.URL.Path == "/api/login" || r.URL.Path == "/api/validate-token" {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем аутентификацию
|
||||
if !isAuthenticated(r) {
|
||||
// Для API запросов возвращаем JSON ошибку
|
||||
if isAPIRequest(r) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(`{"success": false, "error": "Authentication required"}`))
|
||||
return
|
||||
}
|
||||
|
||||
// Для веб-запросов перенаправляем на страницу входа
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Пользователь аутентифицирован, продолжаем
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// CSRFMiddleware проверяет CSRF токены для POST/PUT/DELETE запросов
|
||||
func CSRFMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Проверяем только изменяющие запросы
|
||||
if r.Method == "GET" || r.Method == "HEAD" || r.Method == "OPTIONS" {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Исключаем некоторые API endpoints
|
||||
if r.URL.Path == "/api/login" || r.URL.Path == "/api/logout" {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем CSRF токен из заголовка или формы
|
||||
csrfToken := r.Header.Get("X-CSRF-Token")
|
||||
if csrfToken == "" {
|
||||
csrfToken = r.FormValue("csrf_token")
|
||||
}
|
||||
|
||||
if csrfToken == "" {
|
||||
// Для API запросов возвращаем JSON ошибку
|
||||
if isAPIRequest(r) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte(`{"success": false, "error": "CSRF token required"}`))
|
||||
return
|
||||
}
|
||||
|
||||
// Для веб-запросов возвращаем ошибку
|
||||
http.Error(w, "CSRF token required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем сессионный ID
|
||||
sessionID := getSessionID(r)
|
||||
|
||||
// Проверяем CSRF токен
|
||||
csrfManager := GetCSRFManager()
|
||||
if csrfManager == nil || !csrfManager.ValidateToken(csrfToken, sessionID) {
|
||||
// Для API запросов возвращаем JSON ошибку
|
||||
if isAPIRequest(r) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte(`{"success": false, "error": "Invalid CSRF token"}`))
|
||||
return
|
||||
}
|
||||
|
||||
// Для веб-запросов возвращаем ошибку
|
||||
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// CSRF токен валиден, продолжаем
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// isAPIRequest проверяет, является ли запрос API запросом
|
||||
func isAPIRequest(r *http.Request) bool {
|
||||
path := r.URL.Path
|
||||
return len(path) >= 4 && path[:4] == "/api"
|
||||
}
|
||||
|
||||
// RequireAuth обертка для requireAuth из auth.go
|
||||
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
return requireAuth(next)
|
||||
}
|
||||
@@ -90,6 +90,8 @@ func handlePromptsPage(w http.ResponseWriter, r *http.Request) {
|
||||
MaxSystemPromptLength int
|
||||
MaxPromptNameLength int
|
||||
MaxPromptDescLength int
|
||||
BasePath string
|
||||
AppName string
|
||||
}{
|
||||
Prompts: promptsWithDefault,
|
||||
VerbosePrompts: verbosePrompts,
|
||||
@@ -97,6 +99,8 @@ func handlePromptsPage(w http.ResponseWriter, r *http.Request) {
|
||||
MaxSystemPromptLength: config.AppConfig.Validation.MaxSystemPromptLength,
|
||||
MaxPromptNameLength: config.AppConfig.Validation.MaxPromptNameLength,
|
||||
MaxPromptDescLength: config.AppConfig.Validation.MaxPromptDescLength,
|
||||
BasePath: getBasePath(),
|
||||
AppName: config.AppConfig.AppName,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
@@ -8,18 +8,41 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
// generateAbbreviation создает аббревиатуру из первых букв слов в названии приложения
|
||||
func generateAbbreviation(appName string) string {
|
||||
words := strings.Fields(appName)
|
||||
var abbreviation strings.Builder
|
||||
|
||||
for _, word := range words {
|
||||
if len(word) > 0 {
|
||||
// Берем первую букву слова, если это буква
|
||||
firstRune := []rune(word)[0]
|
||||
if unicode.IsLetter(firstRune) {
|
||||
abbreviation.WriteRune(unicode.ToUpper(firstRune))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := abbreviation.String()
|
||||
if result == "" {
|
||||
return "LCG" // Fallback если не удалось сгенерировать аббревиатуру
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FileInfo содержит информацию о файле
|
||||
type FileInfo struct {
|
||||
Name string
|
||||
Size string
|
||||
ModTime string
|
||||
Preview string
|
||||
Preview template.HTML
|
||||
Content string // Полное содержимое для поиска
|
||||
}
|
||||
|
||||
@@ -52,13 +75,19 @@ func handleResultsPage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Files []FileInfo
|
||||
TotalFiles int
|
||||
RecentFiles int
|
||||
Files []FileInfo
|
||||
TotalFiles int
|
||||
RecentFiles int
|
||||
BasePath string
|
||||
AppName string
|
||||
AppAbbreviation string
|
||||
}{
|
||||
Files: files,
|
||||
TotalFiles: len(files),
|
||||
RecentFiles: recentCount,
|
||||
Files: files,
|
||||
TotalFiles: len(files),
|
||||
RecentFiles: recentCount,
|
||||
BasePath: getBasePath(),
|
||||
AppName: config.AppConfig.AppName,
|
||||
AppAbbreviation: generateAbbreviation(config.AppConfig.AppName),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
@@ -83,44 +112,15 @@ func getResultFiles() ([]FileInfo, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Читаем превью файла (первые 200 символов) и конвертируем Markdown
|
||||
// Читаем превью файла (первые 200 символов) как обычный текст
|
||||
preview := ""
|
||||
fullContent := ""
|
||||
if content, err := os.ReadFile(filepath.Join(config.AppConfig.ResultFolder, entry.Name())); err == nil {
|
||||
// Сохраняем полное содержимое для поиска
|
||||
fullContent = string(content)
|
||||
// Конвертируем Markdown в HTML для превью
|
||||
htmlContent := blackfriday.Run(content)
|
||||
preview = strings.TrimSpace(string(htmlContent))
|
||||
// Удаляем HTML теги для превью
|
||||
preview = strings.ReplaceAll(preview, "<h1>", "")
|
||||
preview = strings.ReplaceAll(preview, "</h1>", "")
|
||||
preview = strings.ReplaceAll(preview, "<h2>", "")
|
||||
preview = strings.ReplaceAll(preview, "</h2>", "")
|
||||
preview = strings.ReplaceAll(preview, "<h3>", "")
|
||||
preview = strings.ReplaceAll(preview, "</h3>", "")
|
||||
preview = strings.ReplaceAll(preview, "<p>", "")
|
||||
preview = strings.ReplaceAll(preview, "</p>", "")
|
||||
preview = strings.ReplaceAll(preview, "<code>", "")
|
||||
preview = strings.ReplaceAll(preview, "</code>", "")
|
||||
preview = strings.ReplaceAll(preview, "<pre>", "")
|
||||
preview = strings.ReplaceAll(preview, "</pre>", "")
|
||||
preview = strings.ReplaceAll(preview, "<strong>", "")
|
||||
preview = strings.ReplaceAll(preview, "</strong>", "")
|
||||
preview = strings.ReplaceAll(preview, "<em>", "")
|
||||
preview = strings.ReplaceAll(preview, "</em>", "")
|
||||
preview = strings.ReplaceAll(preview, "<ul>", "")
|
||||
preview = strings.ReplaceAll(preview, "</ul>", "")
|
||||
preview = strings.ReplaceAll(preview, "<li>", "• ")
|
||||
preview = strings.ReplaceAll(preview, "</li>", "")
|
||||
preview = strings.ReplaceAll(preview, "<ol>", "")
|
||||
preview = strings.ReplaceAll(preview, "</ol>", "")
|
||||
preview = strings.ReplaceAll(preview, "<blockquote>", "")
|
||||
preview = strings.ReplaceAll(preview, "</blockquote>", "")
|
||||
preview = strings.ReplaceAll(preview, "<br>", "")
|
||||
preview = strings.ReplaceAll(preview, "<br/>", "")
|
||||
preview = strings.ReplaceAll(preview, "<br />", "")
|
||||
|
||||
// Берем первые 200 символов как превью
|
||||
preview = string(content)
|
||||
// Очищаем от лишних пробелов и переносов
|
||||
preview = strings.ReplaceAll(preview, "\n", " ")
|
||||
preview = strings.ReplaceAll(preview, "\r", "")
|
||||
@@ -136,7 +136,7 @@ func getResultFiles() ([]FileInfo, error) {
|
||||
Name: entry.Name(),
|
||||
Size: formatFileSize(info.Size()),
|
||||
ModTime: info.ModTime().Format("02.01.2006 15:04"),
|
||||
Preview: preview,
|
||||
Preview: template.HTML(preview),
|
||||
Content: fullContent,
|
||||
})
|
||||
}
|
||||
|
||||
152
serve/serve.go
152
serve/serve.go
@@ -5,13 +5,49 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/direct-dev-ru/linux-command-gpt/config"
|
||||
"github.com/direct-dev-ru/linux-command-gpt/ssl"
|
||||
)
|
||||
|
||||
// makePath создает путь с учетом BasePath
|
||||
func makePath(path string) string {
|
||||
basePath := config.AppConfig.Server.BasePath
|
||||
if basePath == "" || basePath == "/" {
|
||||
return path
|
||||
}
|
||||
|
||||
// Убираем слэш в конце basePath если есть
|
||||
basePath = strings.TrimSuffix(basePath, "/")
|
||||
|
||||
// Убираем слэш в начале path если есть
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
|
||||
// Если path пустой, возвращаем basePath с слэшем в конце
|
||||
if path == "" {
|
||||
return basePath + "/"
|
||||
}
|
||||
|
||||
return basePath + "/" + path
|
||||
}
|
||||
|
||||
// getBasePath возвращает BasePath для использования в шаблонах
|
||||
func getBasePath() string {
|
||||
basePath := config.AppConfig.Server.BasePath
|
||||
if basePath == "" || basePath == "/" {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSuffix(basePath, "/")
|
||||
}
|
||||
|
||||
// StartResultServer запускает HTTP/HTTPS сервер для просмотра сохраненных результатов
|
||||
func StartResultServer(host, port string) error {
|
||||
// Инициализируем CSRF менеджер
|
||||
if err := InitCSRFManager(); err != nil {
|
||||
return fmt.Errorf("failed to initialize CSRF manager: %v", err)
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%s", host, port)
|
||||
|
||||
// Проверяем, нужно ли использовать HTTPS
|
||||
@@ -103,78 +139,116 @@ func registerHTTPSRoutes() {
|
||||
registerRoutesExceptHome()
|
||||
|
||||
// Регистрируем главную страницу с проверкой HTTPS
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.HandleFunc(makePath("/"), func(w http.ResponseWriter, r *http.Request) {
|
||||
// Проверяем, пришел ли запрос по HTTP (не HTTPS)
|
||||
if r.TLS == nil {
|
||||
handleHTTPSRedirect(w, r)
|
||||
return
|
||||
}
|
||||
// Если уже HTTPS, обрабатываем как обычно
|
||||
handleResultsPage(w, r)
|
||||
AuthMiddleware(handleResultsPage)(w, r)
|
||||
})
|
||||
|
||||
// Регистрируем главную страницу без слэша в конце для BasePath
|
||||
basePath := config.AppConfig.Server.BasePath
|
||||
if basePath != "" && basePath != "/" {
|
||||
basePath = strings.TrimSuffix(basePath, "/")
|
||||
http.HandleFunc(basePath, func(w http.ResponseWriter, r *http.Request) {
|
||||
// Проверяем, пришел ли запрос по HTTP (не HTTPS)
|
||||
if r.TLS == nil {
|
||||
handleHTTPSRedirect(w, r)
|
||||
return
|
||||
}
|
||||
// Если уже HTTPS, обрабатываем как обычно
|
||||
AuthMiddleware(handleResultsPage)(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// registerRoutesExceptHome регистрирует все маршруты кроме главной страницы
|
||||
func registerRoutesExceptHome() {
|
||||
// Страница входа (без аутентификации)
|
||||
http.HandleFunc(makePath("/login"), handleLoginPage)
|
||||
|
||||
// API для аутентификации (без аутентификации)
|
||||
http.HandleFunc(makePath("/api/login"), handleLogin)
|
||||
http.HandleFunc(makePath("/api/logout"), handleLogout)
|
||||
http.HandleFunc(makePath("/api/validate-token"), handleValidateToken)
|
||||
|
||||
// Файлы
|
||||
http.HandleFunc("/file/", handleFileView)
|
||||
http.HandleFunc("/delete/", handleDeleteFile)
|
||||
http.HandleFunc(makePath("/file/"), AuthMiddleware(handleFileView))
|
||||
http.HandleFunc(makePath("/delete/"), AuthMiddleware(handleDeleteFile))
|
||||
|
||||
// История запросов
|
||||
http.HandleFunc("/history", handleHistoryPage)
|
||||
http.HandleFunc("/history/view/", handleHistoryView)
|
||||
http.HandleFunc("/history/delete/", handleDeleteHistoryEntry)
|
||||
http.HandleFunc("/history/clear", handleClearHistory)
|
||||
http.HandleFunc(makePath("/history"), AuthMiddleware(handleHistoryPage))
|
||||
http.HandleFunc(makePath("/history/view/"), AuthMiddleware(handleHistoryView))
|
||||
http.HandleFunc(makePath("/history/delete/"), AuthMiddleware(handleDeleteHistoryEntry))
|
||||
http.HandleFunc(makePath("/history/clear"), AuthMiddleware(handleClearHistory))
|
||||
|
||||
// Управление промптами
|
||||
http.HandleFunc("/prompts", handlePromptsPage)
|
||||
http.HandleFunc("/prompts/add", handleAddPrompt)
|
||||
http.HandleFunc("/prompts/edit/", handleEditPrompt)
|
||||
http.HandleFunc("/prompts/edit-verbose/", handleEditVerbosePrompt)
|
||||
http.HandleFunc("/prompts/delete/", handleDeletePrompt)
|
||||
http.HandleFunc("/prompts/restore/", handleRestorePrompt)
|
||||
http.HandleFunc("/prompts/restore-verbose/", handleRestoreVerbosePrompt)
|
||||
http.HandleFunc("/prompts/save-lang", handleSaveLang)
|
||||
http.HandleFunc(makePath("/prompts"), AuthMiddleware(handlePromptsPage))
|
||||
http.HandleFunc(makePath("/prompts/add"), AuthMiddleware(handleAddPrompt))
|
||||
http.HandleFunc(makePath("/prompts/edit/"), AuthMiddleware(handleEditPrompt))
|
||||
http.HandleFunc(makePath("/prompts/edit-verbose/"), AuthMiddleware(handleEditVerbosePrompt))
|
||||
http.HandleFunc(makePath("/prompts/delete/"), AuthMiddleware(handleDeletePrompt))
|
||||
http.HandleFunc(makePath("/prompts/restore/"), AuthMiddleware(handleRestorePrompt))
|
||||
http.HandleFunc(makePath("/prompts/restore-verbose/"), AuthMiddleware(handleRestoreVerbosePrompt))
|
||||
http.HandleFunc(makePath("/prompts/save-lang"), AuthMiddleware(handleSaveLang))
|
||||
|
||||
// Веб-страница для выполнения запросов
|
||||
http.HandleFunc("/run", handleExecutePage)
|
||||
http.HandleFunc(makePath("/run"), AuthMiddleware(CSRFMiddleware(handleExecutePage)))
|
||||
|
||||
// API для выполнения запросов
|
||||
http.HandleFunc("/api/execute", handleExecute)
|
||||
http.HandleFunc(makePath("/api/execute"), AuthMiddleware(CSRFMiddleware(handleExecute)))
|
||||
// API для сохранения результатов и истории
|
||||
http.HandleFunc("/api/save-result", handleSaveResult)
|
||||
http.HandleFunc("/api/add-to-history", handleAddToHistory)
|
||||
http.HandleFunc(makePath("/api/save-result"), AuthMiddleware(CSRFMiddleware(handleSaveResult)))
|
||||
http.HandleFunc(makePath("/api/add-to-history"), AuthMiddleware(CSRFMiddleware(handleAddToHistory)))
|
||||
}
|
||||
|
||||
// registerRoutes регистрирует все маршруты сервера
|
||||
func registerRoutes() {
|
||||
// Страница входа (без аутентификации)
|
||||
http.HandleFunc(makePath("/login"), handleLoginPage)
|
||||
|
||||
// API для аутентификации (без аутентификации)
|
||||
http.HandleFunc(makePath("/api/login"), handleLogin)
|
||||
http.HandleFunc(makePath("/api/logout"), handleLogout)
|
||||
http.HandleFunc(makePath("/api/validate-token"), handleValidateToken)
|
||||
|
||||
// Главная страница и файлы
|
||||
http.HandleFunc("/", handleResultsPage)
|
||||
http.HandleFunc("/file/", handleFileView)
|
||||
http.HandleFunc("/delete/", handleDeleteFile)
|
||||
http.HandleFunc(makePath("/"), AuthMiddleware(handleResultsPage))
|
||||
http.HandleFunc(makePath("/file/"), AuthMiddleware(handleFileView))
|
||||
http.HandleFunc(makePath("/delete/"), AuthMiddleware(handleDeleteFile))
|
||||
|
||||
// История запросов
|
||||
http.HandleFunc("/history", handleHistoryPage)
|
||||
http.HandleFunc("/history/view/", handleHistoryView)
|
||||
http.HandleFunc("/history/delete/", handleDeleteHistoryEntry)
|
||||
http.HandleFunc("/history/clear", handleClearHistory)
|
||||
http.HandleFunc(makePath("/history"), AuthMiddleware(handleHistoryPage))
|
||||
http.HandleFunc(makePath("/history/view/"), AuthMiddleware(handleHistoryView))
|
||||
http.HandleFunc(makePath("/history/delete/"), AuthMiddleware(handleDeleteHistoryEntry))
|
||||
http.HandleFunc(makePath("/history/clear"), AuthMiddleware(handleClearHistory))
|
||||
|
||||
// Управление промптами
|
||||
http.HandleFunc("/prompts", handlePromptsPage)
|
||||
http.HandleFunc("/prompts/add", handleAddPrompt)
|
||||
http.HandleFunc("/prompts/edit/", handleEditPrompt)
|
||||
http.HandleFunc("/prompts/edit-verbose/", handleEditVerbosePrompt)
|
||||
http.HandleFunc("/prompts/delete/", handleDeletePrompt)
|
||||
http.HandleFunc("/prompts/restore/", handleRestorePrompt)
|
||||
http.HandleFunc("/prompts/restore-verbose/", handleRestoreVerbosePrompt)
|
||||
http.HandleFunc("/prompts/save-lang", handleSaveLang)
|
||||
http.HandleFunc(makePath("/prompts"), AuthMiddleware(handlePromptsPage))
|
||||
http.HandleFunc(makePath("/prompts/add"), AuthMiddleware(handleAddPrompt))
|
||||
http.HandleFunc(makePath("/prompts/edit/"), AuthMiddleware(handleEditPrompt))
|
||||
http.HandleFunc(makePath("/prompts/edit-verbose/"), AuthMiddleware(handleEditVerbosePrompt))
|
||||
http.HandleFunc(makePath("/prompts/delete/"), AuthMiddleware(handleDeletePrompt))
|
||||
http.HandleFunc(makePath("/prompts/restore/"), AuthMiddleware(handleRestorePrompt))
|
||||
http.HandleFunc(makePath("/prompts/restore-verbose/"), AuthMiddleware(handleRestoreVerbosePrompt))
|
||||
http.HandleFunc(makePath("/prompts/save-lang"), AuthMiddleware(handleSaveLang))
|
||||
|
||||
// Веб-страница для выполнения запросов
|
||||
http.HandleFunc("/run", handleExecutePage)
|
||||
http.HandleFunc(makePath("/run"), AuthMiddleware(CSRFMiddleware(handleExecutePage)))
|
||||
|
||||
// API для выполнения запросов
|
||||
http.HandleFunc("/api/execute", handleExecute)
|
||||
http.HandleFunc(makePath("/api/execute"), AuthMiddleware(CSRFMiddleware(handleExecute)))
|
||||
// API для сохранения результатов и истории
|
||||
http.HandleFunc("/api/save-result", handleSaveResult)
|
||||
http.HandleFunc("/api/add-to-history", handleAddToHistory)
|
||||
http.HandleFunc(makePath("/api/save-result"), AuthMiddleware(CSRFMiddleware(handleSaveResult)))
|
||||
http.HandleFunc(makePath("/api/add-to-history"), AuthMiddleware(CSRFMiddleware(handleAddToHistory)))
|
||||
|
||||
// Регистрируем главную страницу без слэша в конце для BasePath
|
||||
basePath := config.AppConfig.Server.BasePath
|
||||
if basePath != "" && basePath != "/" {
|
||||
basePath = strings.TrimSuffix(basePath, "/")
|
||||
http.HandleFunc(basePath, AuthMiddleware(handleResultsPage))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,14 @@ var ExecutePageCSSTemplate = template.Must(template.New("execute_css").Parse(`
|
||||
opacity: 0.9;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.config-info {
|
||||
margin: 5px 0 0 0 !important;
|
||||
opacity: 0.7 !important;
|
||||
font-size: 0.9em !important;
|
||||
font-style: italic;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ var ExecutePageTemplate = template.Must(template.New("execute").Parse(`<!DOCTYPE
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{.Title}} - Linux Command GPT</title>
|
||||
<title>{{.Title}} - {{.AppName}}</title>
|
||||
<style>
|
||||
{{template "execute_css" .}}
|
||||
</style>
|
||||
@@ -17,16 +17,19 @@ var ExecutePageTemplate = template.Must(template.New("execute").Parse(`<!DOCTYPE
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>{{.Header}}</h1>
|
||||
<p>Выполнение запросов к Linux Command GPT через веб-интерфейс</p>
|
||||
<p>Выполнение запросов к {{.AppName}} через веб-интерфейс</p>
|
||||
<p class="config-info">({{.ProviderType}} • {{.Model}} • {{.Host}})</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="nav-buttons">
|
||||
<a href="/" class="nav-btn">🏠 Главная</a>
|
||||
<a href="/history" class="nav-btn">📝 История</a>
|
||||
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
|
||||
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
|
||||
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||
</div>
|
||||
|
||||
<form method="POST" id="executeForm">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
|
||||
<div class="form-section">
|
||||
<div class="form-group">
|
||||
<label for="system_id">🤖 Системный промпт:</label>
|
||||
|
||||
@@ -89,6 +89,7 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
||||
function saveResult() {
|
||||
const resultDataField = document.getElementById('resultData');
|
||||
const prompt = document.getElementById('prompt').value;
|
||||
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||
|
||||
if (!resultDataField.value || !prompt.trim()) {
|
||||
alert('Нет данных для сохранения');
|
||||
@@ -104,10 +105,11 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
||||
model: resultData.model || 'Unknown'
|
||||
};
|
||||
|
||||
fetch('/api/save-result', {
|
||||
fetch('{{.BasePath}}/api/save-result', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
})
|
||||
@@ -134,6 +136,7 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
||||
const resultDataField = document.getElementById('resultData');
|
||||
const prompt = document.getElementById('prompt').value;
|
||||
const systemId = document.getElementById('system_id').value;
|
||||
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||
|
||||
if (!resultDataField.value || !prompt.trim()) {
|
||||
alert('Нет данных для сохранения в историю');
|
||||
@@ -152,10 +155,11 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
|
||||
system: systemName
|
||||
};
|
||||
|
||||
fetch('/api/add-to-history', {
|
||||
fetch('{{.BasePath}}/api/add-to-history', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken,
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
})
|
||||
|
||||
@@ -155,13 +155,13 @@ const HistoryPageTemplate = `
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📝 История запросов</h1>
|
||||
<p>Управление историей запросов Linux Command GPT</p>
|
||||
<p>Управление историей запросов {{.AppName}}</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="nav-buttons">
|
||||
<a href="/" class="nav-btn">🏠 Главная</a>
|
||||
<a href="/run" class="nav-btn">🚀 Выполнение</a>
|
||||
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
|
||||
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
|
||||
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||
<button class="nav-btn clear-btn" onclick="clearHistory()">🗑️ Очистить всю историю</button>
|
||||
</div>
|
||||
|
||||
@@ -197,12 +197,12 @@ const HistoryPageTemplate = `
|
||||
|
||||
<script>
|
||||
function viewHistoryEntry(index) {
|
||||
window.location.href = '/history/view/' + index;
|
||||
window.location.href = '{{.BasePath}}/history/view/' + index;
|
||||
}
|
||||
|
||||
function deleteHistoryEntry(index) {
|
||||
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
|
||||
fetch('/history/delete/' + index, {
|
||||
fetch('{{.BasePath}}/history/delete/' + index, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
@@ -221,7 +221,7 @@ const HistoryPageTemplate = `
|
||||
|
||||
function clearHistory() {
|
||||
if (confirm('Вы уверены, что хотите очистить всю историю?\\n\\nЭто действие нельзя отменить.')) {
|
||||
fetch('/history/clear', {
|
||||
fetch('{{.BasePath}}/history/clear', {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
|
||||
@@ -224,7 +224,7 @@ const HistoryViewTemplate = `
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📝 Запись #{{.Index}}</h1>
|
||||
<a href="/history" class="back-btn">← Назад к истории</a>
|
||||
<a href="{{.BasePath}}/history" class="back-btn">← Назад к истории</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="history-meta">
|
||||
@@ -249,7 +249,7 @@ const HistoryViewTemplate = `
|
||||
{{.ExplanationHTML}}
|
||||
|
||||
<div class="actions">
|
||||
<a href="/history" class="action-btn">📝 К истории</a>
|
||||
<a href="{{.BasePath}}/history" class="action-btn">📝 К истории</a>
|
||||
<button class="action-btn delete-btn" onclick="deleteHistoryEntry({{.Index}})">🗑️ Удалить запись</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -258,12 +258,12 @@ const HistoryViewTemplate = `
|
||||
<script>
|
||||
function deleteHistoryEntry(index) {
|
||||
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
|
||||
fetch('/history/delete/' + index, {
|
||||
fetch('{{.BasePath}}/history/delete/' + index, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
window.location.href = '/history';
|
||||
window.location.href = '{{.BasePath}}/history';
|
||||
} else {
|
||||
alert('Ошибка при удалении записи');
|
||||
}
|
||||
|
||||
323
serve/templates/login.go
Normal file
323
serve/templates/login.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package templates
|
||||
|
||||
// LoginPageTemplate шаблон страницы авторизации
|
||||
const LoginPageTemplate = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{.Title}}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(-45deg, #667eea, #764ba2, #f093fb, #f5576c, #4facfe, #00f2fe);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
/* Плавающие элементы */
|
||||
.floating-elements {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.floating-element {
|
||||
position: absolute;
|
||||
opacity: 0.1;
|
||||
animation: float 20s infinite linear;
|
||||
}
|
||||
|
||||
.floating-element:nth-child(1) { left: 10%; animation-delay: 0s; animation-duration: 25s; }
|
||||
.floating-element:nth-child(2) { left: 20%; animation-delay: 2s; animation-duration: 30s; }
|
||||
.floating-element:nth-child(3) { left: 30%; animation-delay: 4s; animation-duration: 20s; }
|
||||
.floating-element:nth-child(4) { left: 40%; animation-delay: 6s; animation-duration: 35s; }
|
||||
.floating-element:nth-child(5) { left: 50%; animation-delay: 8s; animation-duration: 28s; }
|
||||
.floating-element:nth-child(6) { left: 60%; animation-delay: 10s; animation-duration: 22s; }
|
||||
.floating-element:nth-child(7) { left: 70%; animation-delay: 12s; animation-duration: 32s; }
|
||||
.floating-element:nth-child(8) { left: 80%; animation-delay: 14s; animation-duration: 26s; }
|
||||
.floating-element:nth-child(9) { left: 90%; animation-delay: 16s; animation-duration: 24s; }
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
|
||||
10% { opacity: 0.1; }
|
||||
90% { opacity: 0.1; }
|
||||
100% { transform: translateY(-100px) rotate(360deg); opacity: 0; }
|
||||
}
|
||||
|
||||
.lock-icon {
|
||||
font-size: 2rem;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.key-icon {
|
||||
font-size: 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.shield-icon {
|
||||
font-size: 1.8rem;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.star-icon {
|
||||
font-size: 1.2rem;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 2rem;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
color: #333;
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2);
|
||||
background: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.login-button:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.login-button:active {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Плавающие элементы фона -->
|
||||
<div class="floating-elements">
|
||||
<div class="floating-element lock-icon">🔒</div>
|
||||
<div class="floating-element key-icon">🔑</div>
|
||||
<div class="floating-element shield-icon">🛡️</div>
|
||||
<div class="floating-element star-icon">⭐</div>
|
||||
<div class="floating-element lock-icon">🔐</div>
|
||||
<div class="floating-element key-icon">🗝️</div>
|
||||
<div class="floating-element shield-icon">🔒</div>
|
||||
<div class="floating-element star-icon">✨</div>
|
||||
<div class="floating-element lock-icon">🔒</div>
|
||||
</div>
|
||||
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h1>🔐 Авторизация</h1>
|
||||
<p>Войдите в систему для доступа к LCG</p>
|
||||
</div>
|
||||
|
||||
<form id="loginForm">
|
||||
<input type="hidden" id="csrf_token" name="csrf_token" value="{{.CSRFToken}}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">Имя пользователя:</label>
|
||||
<input type="text" id="username" name="username" required placeholder="Введите имя пользователя">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Пароль:</label>
|
||||
<input type="password" id="password" name="password" required placeholder="Введите пароль">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="login-button">Войти</button>
|
||||
</form>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Проверка авторизации...</p>
|
||||
</div>
|
||||
|
||||
<div id="message"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('loginForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const formData = new FormData(form);
|
||||
const username = formData.get('username');
|
||||
const password = formData.get('password');
|
||||
|
||||
// Показываем загрузку
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
document.getElementById('message').innerHTML = '';
|
||||
|
||||
try {
|
||||
const csrfToken = document.getElementById('csrf_token').value;
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password,
|
||||
csrf_token: csrfToken
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Успешная авторизация, перенаправляем на главную страницу
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
// Ошибка авторизации
|
||||
showMessage(data.error || 'Ошибка авторизации', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('Ошибка соединения с сервером', 'error');
|
||||
} finally {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
function showMessage(text, type) {
|
||||
const messageDiv = document.getElementById('message');
|
||||
messageDiv.innerHTML = '<div class="message ' + type + '">' + text + '</div>';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
@@ -235,13 +235,13 @@ const PromptsPageTemplate = `
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>⚙️ Системные промпты</h1>
|
||||
<p>Управление системными промптами Linux Command GPT</p>
|
||||
<p>Управление системными промптами {{.AppName}}</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="nav-buttons">
|
||||
<a href="/" class="nav-btn">🏠 Главная</a>
|
||||
<a href="/run" class="nav-btn">🚀 Выполнение</a>
|
||||
<a href="/history" class="nav-btn">📝 История</a>
|
||||
<a href="{{.BasePath}}/" class="nav-btn">🏠 Главная</a>
|
||||
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
|
||||
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
|
||||
<button class="nav-btn add-btn" onclick="showAddForm()">➕ Добавить промпт</button>
|
||||
<div class="lang-switcher">
|
||||
<button class="lang-btn {{if eq .Lang "ru"}}active{{end}}" onclick="switchLang('ru')">🇷🇺 RU</button>
|
||||
@@ -391,7 +391,7 @@ const PromptsPageTemplate = `
|
||||
|
||||
function saveCurrentPrompts(lang) {
|
||||
// Отправляем запрос для сохранения текущих промптов с новым языком
|
||||
fetch('/prompts/save-lang', {
|
||||
fetch('{{.BasePath}}/prompts/save-lang', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -417,7 +417,7 @@ const PromptsPageTemplate = `
|
||||
|
||||
function deletePrompt(id) {
|
||||
if (confirm('Вы уверены, что хотите удалить промпт #' + id + '?')) {
|
||||
fetch('/prompts/delete/' + id, {
|
||||
fetch('{{.BasePath}}/prompts/delete/' + id, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
@@ -467,10 +467,10 @@ const PromptsPageTemplate = `
|
||||
|
||||
let url, method;
|
||||
if (isVerbosePrompt) {
|
||||
url = '/prompts/edit-verbose/' + id;
|
||||
url = '{{.BasePath}}/prompts/edit-verbose/' + id;
|
||||
method = 'PUT';
|
||||
} else {
|
||||
url = id ? '/prompts/edit/' + id : '/prompts/add';
|
||||
url = id ? '{{.BasePath}}/prompts/edit/' + id : '{{.BasePath}}/prompts/add';
|
||||
method = id ? 'PUT' : 'POST';
|
||||
}
|
||||
|
||||
@@ -501,7 +501,7 @@ const PromptsPageTemplate = `
|
||||
// Функция восстановления системного промпта
|
||||
function restorePrompt(id) {
|
||||
if (confirm('Восстановить промпт к значению по умолчанию?')) {
|
||||
fetch('/prompts/restore/' + id, {
|
||||
fetch('{{.BasePath}}/prompts/restore/' + id, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -526,7 +526,7 @@ const PromptsPageTemplate = `
|
||||
// Функция восстановления verbose промпта
|
||||
function restoreVerbosePrompt(mode) {
|
||||
if (confirm('Восстановить промпт к значению по умолчанию?')) {
|
||||
fetch('/prompts/restore-verbose/' + mode, {
|
||||
fetch('{{.BasePath}}/prompts/restore-verbose/' + mode, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -7,7 +7,7 @@ const ResultsPageTemplate = `
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LCG Results - Linux Command GPT</title>
|
||||
<title>{{.AppAbbreviation}} Результаты - {{.AppName}}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
@@ -182,15 +182,15 @@ const ResultsPageTemplate = `
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🚀 LCG Results</h1>
|
||||
<p>Просмотр сохраненных результатов Linux Command GPT</p>
|
||||
<h1>🚀 {{.AppAbbreviation}} - {{.AppName}}</h1>
|
||||
<p>Просмотр сохраненных результатов {{.AppName}}</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="nav-buttons">
|
||||
<button class="nav-btn" onclick="location.reload()">🔄 Обновить</button>
|
||||
<a href="/run" class="nav-btn">🚀 Выполнение</a>
|
||||
<a href="/history" class="nav-btn">📝 История</a>
|
||||
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||
<a href="{{.BasePath}}/run" class="nav-btn">🚀 Выполнение</a>
|
||||
<a href="{{.BasePath}}/history" class="nav-btn">📝 История</a>
|
||||
<a href="{{.BasePath}}/prompts" class="nav-btn">⚙️ Промпты</a>
|
||||
</div>
|
||||
|
||||
<!-- Поиск -->
|
||||
@@ -218,7 +218,7 @@ const ResultsPageTemplate = `
|
||||
<div class="file-actions">
|
||||
<button class="delete-btn" onclick="deleteFile('{{.Name}}')" title="Удалить файл">🗑️</button>
|
||||
</div>
|
||||
<div class="file-card-content" onclick="window.location.href='/file/{{.Name}}'">
|
||||
<div class="file-card-content" onclick="window.location.href='{{$.BasePath}}/file/{{.Name}}'">
|
||||
<div class="file-name">{{.Name}}</div>
|
||||
<div class="file-info">
|
||||
📅 {{.ModTime}} | 📏 {{.Size}}
|
||||
@@ -240,7 +240,7 @@ const ResultsPageTemplate = `
|
||||
<script>
|
||||
function deleteFile(filename) {
|
||||
if (confirm('Вы уверены, что хотите удалить файл "' + filename + '"?\\n\\nЭто действие нельзя отменить.')) {
|
||||
fetch('/delete/' + encodeURIComponent(filename), {
|
||||
fetch('{{.BasePath}}/delete/' + encodeURIComponent(filename), {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => {
|
||||
|
||||
Reference in New Issue
Block a user