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 }