Инициализация проекта
Всем привет :)
This commit is contained in:
548
internal/server/handlers.go
Normal file
548
internal/server/handlers.go
Normal file
@@ -0,0 +1,548 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"wg-panel/internal/database"
|
||||
"wg-panel/internal/wireguard"
|
||||
)
|
||||
|
||||
var (
|
||||
DB *database.Database
|
||||
Config *database.Config
|
||||
)
|
||||
|
||||
// authMiddleware проверяет авторизацию
|
||||
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Проверяем cookie
|
||||
cookie, err := r.Cookie("auth")
|
||||
if err != nil || cookie.Value != "authenticated" {
|
||||
if r.URL.Path == "/" {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// handleLogin обрабатывает страницу входа
|
||||
func HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
tmplData, err := TemplatesFS.ReadFile("templates/login.html")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
tmpl, err := template.New("login").Parse(string(tmplData))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
tmpl.Execute(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "POST" {
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
|
||||
if username == Config.Username && password == Config.Password {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "auth",
|
||||
Value: "authenticated",
|
||||
Path: "/",
|
||||
MaxAge: 86400, // 24 часа
|
||||
HttpOnly: true,
|
||||
})
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/login?error=1", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
// handleLogout обрабатывает выход
|
||||
func HandleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "auth",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
})
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleIndex обрабатывает главную страницу
|
||||
func HandleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
tmplData, err := TemplatesFS.ReadFile("templates/index.html")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
tmpl, err := template.New("index").Parse(string(tmplData))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
tmpl.Execute(w, nil)
|
||||
}
|
||||
|
||||
// === ОБРАБОТЧИКИ СЕРВЕРОВ ===
|
||||
|
||||
// HandleServers возвращает список серверов
|
||||
func HandleServers(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if DB.Servers == nil {
|
||||
json.NewEncoder(w).Encode([]database.Server{})
|
||||
} else {
|
||||
json.NewEncoder(w).Encode(DB.Servers)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleCreateServer создает новый WireGuard сервер
|
||||
func HandleCreateServer(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
name := r.FormValue("name")
|
||||
address := r.FormValue("address")
|
||||
portStr := r.FormValue("port")
|
||||
dns := r.FormValue("dns")
|
||||
|
||||
if name == "" || address == "" || portStr == "" {
|
||||
http.Error(w, "Missing required fields", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Добавляем /24 если не указано
|
||||
if !strings.Contains(address, "/") {
|
||||
address = address + "/24"
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid port", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Валидируем конфигурацию
|
||||
if err := database.ValidateServerConfig(DB, address, port); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
server, err := wireguard.CreateServer(DB, name, address, port, dns)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create server: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
DB.Servers = append(DB.Servers, *server)
|
||||
database.SaveDatabase(DB)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(server)
|
||||
}
|
||||
|
||||
// HandleUpdateServer обновляет настройки сервера
|
||||
func HandleUpdateServer(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
id := r.FormValue("id")
|
||||
name := r.FormValue("name")
|
||||
portStr := r.FormValue("port")
|
||||
dns := r.FormValue("dns")
|
||||
|
||||
for i, server := range DB.Servers {
|
||||
if server.ID == id {
|
||||
if name != "" {
|
||||
DB.Servers[i].Name = name
|
||||
}
|
||||
if portStr != "" {
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err == nil {
|
||||
DB.Servers[i].ListenPort = port
|
||||
}
|
||||
}
|
||||
if dns != "" {
|
||||
DB.Servers[i].DNS = dns
|
||||
}
|
||||
|
||||
// Обновляем конфиг файл
|
||||
wireguard.UpdateServerConfig(&DB.Servers[i], DB)
|
||||
|
||||
database.SaveDatabase(DB)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(DB.Servers[i])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(w, "Server not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
// HandleDeleteServer удаляет сервер
|
||||
func HandleDeleteServer(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
id := r.FormValue("id")
|
||||
|
||||
for i, server := range DB.Servers {
|
||||
if server.ID == id {
|
||||
// Удаляем сервер
|
||||
if err := wireguard.DeleteServer(&server); err != nil {
|
||||
http.Error(w, "Failed to delete server", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Удаляем всех клиентов этого сервера
|
||||
var newClients []database.Client
|
||||
for _, client := range DB.Clients {
|
||||
if client.ServerID != id {
|
||||
newClients = append(newClients, client)
|
||||
}
|
||||
}
|
||||
DB.Clients = newClients
|
||||
|
||||
// Удаляем сервер из базы
|
||||
DB.Servers = append(DB.Servers[:i], DB.Servers[i+1:]...)
|
||||
database.SaveDatabase(DB)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(w, "Server not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
// HandleToggleServer включает/выключает сервер
|
||||
func HandleToggleServer(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
id := r.FormValue("id")
|
||||
|
||||
for i, server := range DB.Servers {
|
||||
if server.ID == id {
|
||||
if err := wireguard.ToggleServer(&DB.Servers[i]); err != nil {
|
||||
http.Error(w, "Failed to toggle server: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
database.SaveDatabase(DB)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(DB.Servers[i])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(w, "Server not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
// === ОБРАБОТЧИКИ КЛИЕНТОВ ===
|
||||
|
||||
// HandleClients возвращает список клиентов
|
||||
func HandleClients(w http.ResponseWriter, r *http.Request) {
|
||||
serverID := r.URL.Query().Get("server_id")
|
||||
|
||||
var clients []database.Client
|
||||
for _, client := range DB.Clients {
|
||||
if serverID == "" || client.ServerID == serverID {
|
||||
clients = append(clients, client)
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if clients == nil {
|
||||
json.NewEncoder(w).Encode([]database.Client{})
|
||||
} else {
|
||||
json.NewEncoder(w).Encode(clients)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleCreateClient создает новый конфиг клиента
|
||||
func HandleCreateClient(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
serverID := r.FormValue("server_id")
|
||||
name := r.FormValue("name")
|
||||
comment := r.FormValue("comment")
|
||||
|
||||
if serverID == "" || name == "" {
|
||||
http.Error(w, "Missing required fields", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := wireguard.CreateClient(DB, serverID, name, comment)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create client: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
DB.Clients = append(DB.Clients, *client)
|
||||
database.SaveDatabase(DB)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(client)
|
||||
}
|
||||
|
||||
// HandleDeleteClient удаляет клиента
|
||||
func HandleDeleteClient(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
id := r.FormValue("id")
|
||||
|
||||
for i, client := range DB.Clients {
|
||||
if client.ID == id {
|
||||
// Удаляем клиента
|
||||
if err := wireguard.DeleteClient(DB, &client); err != nil {
|
||||
http.Error(w, "Failed to delete client", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Удаляем из базы
|
||||
DB.Clients = append(DB.Clients[:i], DB.Clients[i+1:]...)
|
||||
database.SaveDatabase(DB)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(w, "Client not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
// HandleToggleClient включает/выключает клиента
|
||||
func HandleToggleClient(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
id := r.FormValue("id")
|
||||
|
||||
for i, client := range DB.Clients {
|
||||
if client.ID == id {
|
||||
if err := wireguard.ToggleClient(DB, &DB.Clients[i]); err != nil {
|
||||
http.Error(w, "Failed to toggle client", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
database.SaveDatabase(DB)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(DB.Clients[i])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(w, "Client not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
// HandleUpdateClient обновляет имя и комментарий клиента
|
||||
func HandleUpdateClient(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
id := r.FormValue("id")
|
||||
name := r.FormValue("name")
|
||||
comment := r.FormValue("comment")
|
||||
|
||||
for i, client := range DB.Clients {
|
||||
if client.ID == id {
|
||||
if name != "" {
|
||||
DB.Clients[i].Name = name
|
||||
}
|
||||
DB.Clients[i].Comment = comment
|
||||
database.SaveDatabase(DB)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(DB.Clients[i])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(w, "Client not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
// HandleAddPortForward добавляет проброс порта
|
||||
func HandleAddPortForward(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
clientID := r.FormValue("client_id")
|
||||
portStr := r.FormValue("port")
|
||||
protocol := r.FormValue("protocol")
|
||||
description := r.FormValue("description")
|
||||
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid port", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if protocol != "tcp" && protocol != "udp" && protocol != "both" {
|
||||
http.Error(w, "Protocol must be tcp, udp or both", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
for i, client := range DB.Clients {
|
||||
if client.ID == clientID {
|
||||
if err := wireguard.AddPortForward(DB, &DB.Clients[i], port, protocol, description); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
database.SaveDatabase(DB)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(DB.Clients[i])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(w, "Client not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
// HandleRemovePortForward удаляет проброс порта
|
||||
func HandleRemovePortForward(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
clientID := r.FormValue("client_id")
|
||||
portStr := r.FormValue("port")
|
||||
protocol := r.FormValue("protocol")
|
||||
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid port", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
for i, client := range DB.Clients {
|
||||
if client.ID == clientID {
|
||||
if err := wireguard.RemovePortForward(&DB.Clients[i], port, protocol); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
database.SaveDatabase(DB)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(DB.Clients[i])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(w, "Client not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
// HandleDownloadConfig скачивает конфиг клиента
|
||||
func HandleDownloadConfig(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
|
||||
for _, client := range DB.Clients {
|
||||
if client.ID == id {
|
||||
// Находим сервер
|
||||
var server *database.Server
|
||||
for i := range DB.Servers {
|
||||
if DB.Servers[i].ID == client.ServerID {
|
||||
server = &DB.Servers[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if server == nil {
|
||||
http.Error(w, "Server not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
config := wireguard.GenerateClientConfig(client, server)
|
||||
|
||||
// Создаем безопасное имя файла (без пробелов и спецсимволов)
|
||||
safeName := database.SanitizeFilename(client.Name)
|
||||
|
||||
w.Header().Set("Content-Type", "application/x-wireguard-profile")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.conf", safeName))
|
||||
w.Write([]byte(config))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(w, "Client not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
// HandleQRCode генерирует QR код для конфига
|
||||
func HandleQRCode(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
|
||||
for _, client := range DB.Clients {
|
||||
if client.ID == id {
|
||||
// Находим сервер
|
||||
var server *database.Server
|
||||
for i := range DB.Servers {
|
||||
if DB.Servers[i].ID == client.ServerID {
|
||||
server = &DB.Servers[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if server == nil {
|
||||
http.Error(w, "Server not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
config := wireguard.GenerateClientConfig(client, server)
|
||||
|
||||
// Генерируем QR код
|
||||
png, err := wireguard.GenerateQRCode(config)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to generate QR code", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.Write(png)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(w, "Client not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
// HandleStats возвращает статистику
|
||||
func HandleStats(w http.ResponseWriter, r *http.Request) {
|
||||
wireguard.UpdateStats(DB)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(DB.Clients)
|
||||
}
|
34
internal/server/routes.go
Normal file
34
internal/server/routes.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// SetupRoutes настраивает маршруты HTTP сервера
|
||||
func SetupRoutes() {
|
||||
// Главная страница
|
||||
http.HandleFunc("/", authMiddleware(HandleIndex))
|
||||
|
||||
// API для серверов
|
||||
http.HandleFunc("/api/servers", authMiddleware(HandleServers))
|
||||
http.HandleFunc("/api/server/create", authMiddleware(HandleCreateServer))
|
||||
http.HandleFunc("/api/server/update", authMiddleware(HandleUpdateServer))
|
||||
http.HandleFunc("/api/server/delete", authMiddleware(HandleDeleteServer))
|
||||
http.HandleFunc("/api/server/toggle", authMiddleware(HandleToggleServer))
|
||||
|
||||
// API для клиентов
|
||||
http.HandleFunc("/api/clients", authMiddleware(HandleClients))
|
||||
http.HandleFunc("/api/client/create", authMiddleware(HandleCreateClient))
|
||||
http.HandleFunc("/api/client/delete", authMiddleware(HandleDeleteClient))
|
||||
http.HandleFunc("/api/client/toggle", authMiddleware(HandleToggleClient))
|
||||
http.HandleFunc("/api/client/update", authMiddleware(HandleUpdateClient))
|
||||
http.HandleFunc("/api/client/download", authMiddleware(HandleDownloadConfig))
|
||||
http.HandleFunc("/api/client/qr", authMiddleware(HandleQRCode))
|
||||
http.HandleFunc("/api/client/portforward/add", authMiddleware(HandleAddPortForward))
|
||||
http.HandleFunc("/api/client/portforward/remove", authMiddleware(HandleRemovePortForward))
|
||||
http.HandleFunc("/api/stats", authMiddleware(HandleStats))
|
||||
|
||||
// Авторизация
|
||||
http.HandleFunc("/login", HandleLogin)
|
||||
http.HandleFunc("/logout", HandleLogout)
|
||||
}
|
6
internal/server/static.go
Normal file
6
internal/server/static.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package server
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed templates/*
|
||||
var TemplatesFS embed.FS
|
1122
internal/server/templates/index.html
Normal file
1122
internal/server/templates/index.html
Normal file
File diff suppressed because it is too large
Load Diff
130
internal/server/templates/login.html
Normal file
130
internal/server/templates/login.html
Normal file
@@ -0,0 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WireGuard Panel - Вход</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
font-size: 28px;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.logo p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #fee;
|
||||
border: 1px solid #fcc;
|
||||
color: #c33;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="logo">
|
||||
<h1>WireGuard Panel</h1>
|
||||
<p>Панель управления VPN</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('error')) {
|
||||
document.write('<div class="error">Неверный логин или пароль</div>');
|
||||
}
|
||||
</script>
|
||||
|
||||
<form method="POST" action="/login">
|
||||
<div class="form-group">
|
||||
<label for="username">Логин</label>
|
||||
<input type="text" id="username" name="username" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Пароль</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn">Войти</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
Reference in New Issue
Block a user