Загрузить файлы в «/»

This commit is contained in:
2026-04-23 06:40:34 +00:00
parent 9caa996d31
commit 1a5c27dbe9
5 changed files with 383 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
version: "3.9"
services:
remnawave-panel:
build: .
container_name: remnawave-panel
ports:
- "8080:8080"
environment:
APP_ADDR: ":8080"
API_BASE_URL: "${API_BASE_URL}"
API_BUY_PATH: "${API_BUY_PATH:-/v1/configs/buy}"
API_KEY: "${API_KEY}"
restart: unless-stopped
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
echo "== RemnaWave Panel: install/start =="
if [[ ! -f .env ]]; then
cp .env.example .env
echo "Created .env from .env.example"
echo "Edit .env and set API_BASE_URL/API_KEY before first real use."
fi
docker compose up -d --build
echo "Panel started: http://localhost:8080"
+337
View File
@@ -0,0 +1,337 @@
package main
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"html/template"
"log"
"net/http"
"os"
"strings"
"sync"
"time"
)
type User struct {
Email string
Password string
}
type Purchase struct {
Email string
Plan string
ConfigRaw string
CreatedAt time.Time
}
type Server struct {
mu sync.RWMutex
users map[string]User
sessions map[string]string
purchases map[string][]Purchase
tpls *template.Template
apiClient *APIClient
}
type APIClient struct {
BaseURL string
BuyPath string
APIKey string
Client *http.Client
}
type BuyConfigRequest struct {
Email string `json:"email"`
Plan string `json:"plan"`
}
type BuyConfigResponse struct {
Config string `json:"config"`
}
func main() {
addr := getenv("APP_ADDR", ":8080")
baseURL := getenv("API_BASE_URL", "https://example.com")
buyPath := getenv("API_BUY_PATH", "/v1/configs/buy")
apiKey := getenv("API_KEY", "")
tpls, err := template.ParseGlob("templates/*.html")
if err != nil {
log.Fatalf("parse templates: %v", err)
}
s := &Server{
users: map[string]User{},
sessions: map[string]string{},
purchases: map[string][]Purchase{},
tpls: tpls,
apiClient: &APIClient{
BaseURL: strings.TrimRight(baseURL, "/"),
BuyPath: "/" + strings.TrimLeft(buyPath, "/"),
APIKey: apiKey,
Client: &http.Client{
Timeout: 15 * time.Second,
},
},
}
mux := http.NewServeMux()
mux.HandleFunc("/", s.homeHandler)
mux.HandleFunc("/register", s.registerHandler)
mux.HandleFunc("/login", s.loginHandler)
mux.HandleFunc("/logout", s.logoutHandler)
mux.HandleFunc("/cabinet", s.cabinetHandler)
mux.HandleFunc("/buy", s.buyHandler)
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
log.Printf("server started at %s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatal(err)
}
}
func (s *Server) homeHandler(w http.ResponseWriter, r *http.Request) {
email, _ := s.currentUser(r)
_ = s.render(w, "home.html", map[string]any{
"Title": "RemnaWave VPN",
"Email": email,
})
}
func (s *Server) registerHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
_ = s.render(w, "register.html", map[string]any{"Title": "Registration"})
return
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
email := strings.TrimSpace(r.FormValue("email"))
password := strings.TrimSpace(r.FormValue("password"))
if email == "" || password == "" {
_ = s.render(w, "register.html", map[string]any{
"Title": "Registration", "Error": "Fill all fields.",
})
return
}
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.users[email]; ok {
_ = s.render(w, "register.html", map[string]any{
"Title": "Registration", "Error": "User already exists.",
})
return
}
s.users[email] = User{Email: email, Password: password}
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
func (s *Server) loginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
_ = s.render(w, "login.html", map[string]any{"Title": "Authorization"})
return
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
email := strings.TrimSpace(r.FormValue("email"))
password := strings.TrimSpace(r.FormValue("password"))
s.mu.RLock()
user, ok := s.users[email]
s.mu.RUnlock()
if !ok || user.Password != password {
_ = s.render(w, "login.html", map[string]any{
"Title": "Authorization", "Error": "Invalid credentials.",
})
return
}
token, err := newToken()
if err != nil {
http.Error(w, "token error", http.StatusInternalServerError)
return
}
s.mu.Lock()
s.sessions[token] = email
s.mu.Unlock()
http.SetCookie(w, &http.Cookie{
Name: "session_token",
Value: token,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
MaxAge: 86400 * 7,
})
http.Redirect(w, r, "/cabinet", http.StatusSeeOther)
}
func (s *Server) logoutHandler(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session_token")
if err == nil {
s.mu.Lock()
delete(s.sessions, cookie.Value)
s.mu.Unlock()
}
http.SetCookie(w, &http.Cookie{
Name: "session_token",
Value: "",
Path: "/",
HttpOnly: true,
MaxAge: -1,
})
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (s *Server) cabinetHandler(w http.ResponseWriter, r *http.Request) {
email, ok := s.currentUser(r)
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
s.mu.RLock()
userPurchases := s.purchases[email]
s.mu.RUnlock()
_ = s.render(w, "cabinet.html", map[string]any{
"Title": "Personal cabinet",
"Email": email,
"Purchases": userPurchases,
})
}
func (s *Server) buyHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
email, ok := s.currentUser(r)
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
plan := strings.TrimSpace(r.FormValue("plan"))
if plan == "" {
http.Redirect(w, r, "/cabinet", http.StatusSeeOther)
return
}
config, err := s.apiClient.BuyConfig(email, plan)
if err != nil {
s.mu.RLock()
userPurchases := s.purchases[email]
s.mu.RUnlock()
_ = s.render(w, "cabinet.html", map[string]any{
"Title": "Personal cabinet",
"Email": email,
"Purchases": userPurchases,
"Error": "API error: " + err.Error(),
})
return
}
p := Purchase{
Email: email,
Plan: plan,
ConfigRaw: config,
CreatedAt: time.Now(),
}
s.mu.Lock()
s.purchases[email] = append(s.purchases[email], p)
s.mu.Unlock()
http.Redirect(w, r, "/cabinet", http.StatusSeeOther)
}
func (s *Server) currentUser(r *http.Request) (string, bool) {
cookie, err := r.Cookie("session_token")
if err != nil {
return "", false
}
s.mu.RLock()
email, ok := s.sessions[cookie.Value]
s.mu.RUnlock()
return email, ok
}
func (s *Server) render(w http.ResponseWriter, page string, data any) error {
if err := s.tpls.ExecuteTemplate(w, page, data); err != nil {
http.Error(w, "template error", http.StatusInternalServerError)
return err
}
return nil
}
func (a *APIClient) BuyConfig(email, plan string) (string, error) {
if a.APIKey == "" {
return "", errors.New("API_KEY is empty")
}
reqBody, err := json.Marshal(BuyConfigRequest{
Email: email,
Plan: plan,
})
if err != nil {
return "", err
}
req, err := http.NewRequest(http.MethodPost, a.BaseURL+a.BuyPath, bytes.NewBuffer(reqBody))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+a.APIKey)
resp, err := a.Client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return "", errors.New("remote status: " + resp.Status)
}
var out BuyConfigResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return "", err
}
if out.Config == "" {
return "", errors.New("empty config in response")
}
return out.Config, nil
}
func newToken() (string, error) {
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return hex.EncodeToString(buf), nil
}
func getenv(k, fallback string) string {
v := strings.TrimSpace(os.Getenv(k))
if v == "" {
return fallback
}
return v
}
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
echo "== RemnaWave Panel: reinstall =="
docker compose down --remove-orphans
docker compose up -d --build
echo "Panel reinstalled: http://localhost:8080"
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
echo "== RemnaWave Panel: uninstall/stop =="
docker compose down --remove-orphans
read -r -p "Remove built Docker images too? (y/N): " remove_images
if [[ "${remove_images:-N}" =~ ^[Yy]$ ]]; then
docker compose down --rmi local --remove-orphans
fi
echo "Panel removed."