304 lines
9.1 KiB
Go
304 lines
9.1 KiB
Go
package server
|
||
|
||
import (
|
||
"crypto/rsa"
|
||
"encoding/json"
|
||
"html/template"
|
||
"log"
|
||
"math/rand"
|
||
"net/http"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
|
||
"oreshnik/internal/app/auth"
|
||
"oreshnik/internal/app/db"
|
||
"oreshnik/internal/app/models"
|
||
|
||
"golang.org/x/crypto/bcrypt"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
type Server struct {
|
||
DB *gorm.DB
|
||
PrivKey *rsa.PrivateKey
|
||
PubKey *rsa.PublicKey
|
||
Templates *template.Template
|
||
Flag string
|
||
rand *rand.Rand
|
||
}
|
||
|
||
func New() (*Server, error) {
|
||
s := &Server{Flag: os.Getenv("FLAG")}
|
||
if s.Flag == "" {
|
||
s.Flag = "goidactf{fake_flag}"
|
||
}
|
||
|
||
s.Templates = template.Must(template.ParseGlob("templates/*.html"))
|
||
|
||
var err error
|
||
s.DB, err = db.ConnectAndMigrate(&models.User{}, &models.RevokedToken{}, &models.Product{}, &models.Purchase{})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if _, err := os.Stat("public"); os.IsNotExist(err) {
|
||
_ = os.Mkdir("public", 0755)
|
||
}
|
||
|
||
s.PrivKey, s.PubKey, err = auth.LoadKeys()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
s.rand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||
if err := s.seed(); err != nil {
|
||
return nil, err
|
||
}
|
||
return s, nil
|
||
}
|
||
|
||
func (s *Server) Router() *http.ServeMux {
|
||
mux := http.NewServeMux()
|
||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||
mux.HandleFunc("/", s.homeHandler)
|
||
mux.HandleFunc("/register", s.registerHandler)
|
||
mux.HandleFunc("/login", s.loginHandler)
|
||
mux.HandleFunc("/admin", s.adminHandler)
|
||
mux.HandleFunc("/revoked", s.revokedHandler)
|
||
mux.HandleFunc("/product/", s.productHandler)
|
||
mux.HandleFunc("/buy/", s.buyHandler)
|
||
mux.HandleFunc("/logout", s.logoutHandler)
|
||
mux.HandleFunc("/my-purchases", s.myPurchasesHandler)
|
||
mux.HandleFunc("/order/", s.orderHandler)
|
||
return mux
|
||
}
|
||
|
||
func (s *Server) randomPassword(n int) string {
|
||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||
b := make([]byte, n)
|
||
for i := range b {
|
||
b[i] = charset[s.rand.Intn(len(charset))]
|
||
}
|
||
return string(b)
|
||
}
|
||
|
||
func (s *Server) seed() error {
|
||
s.DB.Exec("DELETE FROM revoked_tokens")
|
||
s.DB.Exec("DELETE FROM products")
|
||
s.DB.Exec("DELETE FROM users")
|
||
|
||
products := []models.Product{
|
||
{Name: "Грецкий орех", Description: "Классический орех, богат омега-3. Отлично подходит для выпечки и салатов.", Price: 250.0},
|
||
{Name: "Миндаль", Description: "Полезен для сердца и кожи. Идеален в качестве перекуса или добавки в мюсли.", Price: 400.0},
|
||
{Name: "Кешью", Description: "Сладкий и маслянистый, идеален для закусок и азиатских блюд.", Price: 550.0},
|
||
{Name: "Фисташки", Description: "Соленые и хрустящие, прекрасная закуска к напиткам.", Price: 600.0},
|
||
{Name: "Фундук", Description: "Лесной орех с насыщенным вкусом, хорош в шоколаде и десертах.", Price: 450.0},
|
||
}
|
||
if err := s.DB.Create(&products).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
adminPass := s.randomPassword(16)
|
||
hashAdmin, _ := bcrypt.GenerateFromPassword([]byte(adminPass), bcrypt.DefaultCost)
|
||
log.Printf("Создан администратор с паролем: %s", adminPass)
|
||
admin := models.User{Username: "админ", Password: string(hashAdmin), IsAdmin: true}
|
||
if err := s.DB.Create(&admin).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
users := []models.User{{Username: "пользователь1"}, {Username: "тест"}}
|
||
for i := range users {
|
||
p := s.randomPassword(16)
|
||
h, _ := bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost)
|
||
users[i].Password = string(h)
|
||
if err := s.DB.Create(&users[i]).Error; err != nil {
|
||
return err
|
||
}
|
||
log.Printf("Создан пользователь '%s' с паролем: %s", users[i].Username, p)
|
||
}
|
||
|
||
adminTok, _ := auth.GenerateJWT(admin.ID, admin.IsAdmin, s.PrivKey)
|
||
if err := s.DB.Create(&models.RevokedToken{Token: adminTok}).Error; err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *Server) homeHandler(w http.ResponseWriter, r *http.Request) {
|
||
data := s.getTemplateData(r)
|
||
if data["LoggedIn"].(bool) {
|
||
var products []models.Product
|
||
s.DB.Find(&products)
|
||
data["Products"] = products
|
||
}
|
||
_ = s.Templates.ExecuteTemplate(w, "index.html", data)
|
||
}
|
||
|
||
func (s *Server) registerHandler(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method == http.MethodGet {
|
||
_ = s.Templates.ExecuteTemplate(w, "register.html", s.getTemplateData(r))
|
||
return
|
||
}
|
||
_ = r.ParseForm()
|
||
username := r.FormValue("username")
|
||
password := r.FormValue("password")
|
||
h, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||
u := models.User{Username: username, Password: string(h)}
|
||
if err := s.DB.Create(&u).Error; err != nil {
|
||
http.Error(w, "Username already exists", http.StatusConflict)
|
||
return
|
||
}
|
||
http.Redirect(w, r, "/login", http.StatusFound)
|
||
}
|
||
|
||
func (s *Server) loginHandler(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method == http.MethodGet {
|
||
_ = s.Templates.ExecuteTemplate(w, "login.html", s.getTemplateData(r))
|
||
return
|
||
}
|
||
_ = r.ParseForm()
|
||
username := r.FormValue("username")
|
||
password := r.FormValue("password")
|
||
var user models.User
|
||
s.DB.First(&user, "username = ?", username)
|
||
if user.ID == 0 || bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) != nil {
|
||
http.Redirect(w, r, "/login", http.StatusFound)
|
||
return
|
||
}
|
||
tok, _ := auth.GenerateJWT(user.ID, user.IsAdmin, s.PrivKey)
|
||
http.SetCookie(w, &http.Cookie{Name: "token", Value: tok, Path: "/"})
|
||
http.Redirect(w, r, "/", http.StatusFound)
|
||
}
|
||
|
||
func (s *Server) adminHandler(w http.ResponseWriter, r *http.Request) {
|
||
cookie, err := r.Cookie("token")
|
||
if err != nil {
|
||
http.Redirect(w, r, "/login", http.StatusFound)
|
||
return
|
||
}
|
||
tokenString := cookie.Value
|
||
var revoked models.RevokedToken
|
||
if s.DB.First(&revoked, "token = ?", tokenString).Error == nil {
|
||
http.Error(w, "Token has been revoked", http.StatusForbidden)
|
||
return
|
||
}
|
||
_, claims, err := auth.Parse(tokenString, s.PubKey)
|
||
if err != nil || !claims["is_admin"].(bool) {
|
||
http.Error(w, "Invalid token or not admin", http.StatusForbidden)
|
||
return
|
||
}
|
||
data := s.getTemplateData(r)
|
||
data["Flag"] = s.Flag
|
||
_ = s.Templates.ExecuteTemplate(w, "admin.html", data)
|
||
}
|
||
|
||
func (s *Server) revokedHandler(w http.ResponseWriter, r *http.Request) {
|
||
var tokens []models.RevokedToken
|
||
s.DB.Find(&tokens)
|
||
var tokenStrings []string
|
||
for _, t := range tokens {
|
||
tokenStrings = append(tokenStrings, t.Token)
|
||
}
|
||
w.Header().Set("Content-Type", "application/json")
|
||
_ = json.NewEncoder(w).Encode(tokenStrings)
|
||
}
|
||
|
||
func (s *Server) productHandler(w http.ResponseWriter, r *http.Request) {
|
||
id := strings.TrimPrefix(r.URL.Path, "/product/")
|
||
var product models.Product
|
||
s.DB.First(&product, id)
|
||
if product.ID == 0 {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
data := s.getTemplateData(r)
|
||
data["Product"] = product
|
||
_ = s.Templates.ExecuteTemplate(w, "product.html", data)
|
||
}
|
||
|
||
func (s *Server) buyHandler(w http.ResponseWriter, r *http.Request) {
|
||
cookie, err := r.Cookie("token")
|
||
if err != nil {
|
||
http.Redirect(w, r, "/login", http.StatusFound)
|
||
return
|
||
}
|
||
|
||
_, claims, err := auth.Parse(cookie.Value, s.PubKey)
|
||
if err != nil {
|
||
http.Redirect(w, r, "/login", http.StatusFound)
|
||
return
|
||
}
|
||
|
||
userID := uint(claims["user_id"].(float64))
|
||
productID := strings.TrimPrefix(r.URL.Path, "/buy/")
|
||
|
||
purchase := models.Purchase{UserID: userID, ProductID: 0}
|
||
s.DB.First(&models.Product{}, productID).Scan(&purchase.Product)
|
||
if purchase.Product.ID == 0 {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
purchase.ProductID = purchase.Product.ID
|
||
|
||
s.DB.Create(&purchase)
|
||
|
||
_ = s.Templates.ExecuteTemplate(w, "purchase_success.html", s.getTemplateData(r))
|
||
}
|
||
|
||
func (s *Server) logoutHandler(w http.ResponseWriter, r *http.Request) {
|
||
http.SetCookie(w, &http.Cookie{Name: "token", Value: "", Path: "/", Expires: time.Unix(0, 0)})
|
||
http.Redirect(w, r, "/", http.StatusFound)
|
||
}
|
||
|
||
func (s *Server) myPurchasesHandler(w http.ResponseWriter, r *http.Request) {
|
||
data := s.getTemplateData(r)
|
||
if !data["LoggedIn"].(bool) {
|
||
http.Redirect(w, r, "/login", http.StatusFound)
|
||
return
|
||
}
|
||
|
||
var purchases []models.Purchase
|
||
s.DB.Preload("Product").Where("user_id = ?", data["UserID"]).Find(&purchases)
|
||
data["Purchases"] = purchases
|
||
|
||
_ = s.Templates.ExecuteTemplate(w, "my_purchases.html", data)
|
||
}
|
||
|
||
func (s *Server) orderHandler(w http.ResponseWriter, r *http.Request) {
|
||
data := s.getTemplateData(r)
|
||
if !data["LoggedIn"].(bool) {
|
||
http.Redirect(w, r, "/login", http.StatusFound)
|
||
return
|
||
}
|
||
|
||
orderID := strings.TrimPrefix(r.URL.Path, "/order/")
|
||
var purchase models.Purchase
|
||
s.DB.Preload("Product").First(&purchase, orderID)
|
||
|
||
data["Purchase"] = purchase
|
||
_ = s.Templates.ExecuteTemplate(w, "order_detail.html", data)
|
||
}
|
||
|
||
func (s *Server) getTemplateData(r *http.Request) map[string]interface{} {
|
||
data := map[string]interface{}{
|
||
"LoggedIn": false,
|
||
}
|
||
cookie, err := r.Cookie("token")
|
||
if err != nil {
|
||
return data
|
||
}
|
||
_, claims, err := auth.Parse(cookie.Value, s.PubKey)
|
||
if err != nil {
|
||
return data
|
||
}
|
||
var user models.User
|
||
s.DB.First(&user, claims["user_id"])
|
||
if user.ID != 0 {
|
||
data["LoggedIn"] = true
|
||
data["Username"] = user.Username
|
||
data["UserID"] = user.ID
|
||
}
|
||
return data
|
||
}
|