338 lines
7.6 KiB
Go
338 lines
7.6 KiB
Go
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
|
|
}
|