Add simple Flask calculator service
This commit is contained in:
parent
311aa3219a
commit
5b83926768
25
Dockerfile
25
Dockerfile
@ -1,23 +1,16 @@
|
||||
FROM golang:1.25-alpine
|
||||
FROM python:3.12-slim
|
||||
|
||||
#RUN apk add --no-cache openssl
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
COPY app.py ./
|
||||
COPY templates ./templates
|
||||
|
||||
COPY templates/ ./templates/
|
||||
COPY static/ ./static/
|
||||
EXPOSE 5000
|
||||
|
||||
RUN go build -o /oreshnik ./cmd/oreshnik
|
||||
|
||||
RUN mkdir public
|
||||
RUN openssl genrsa -out public/private.pem 2048
|
||||
RUN openssl rsa -in public/private.pem -pubout -out public/public.pem
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD [ "/oreshnik" ]
|
||||
CMD ["python", "app.py"]
|
||||
|
||||
50
app.py
Normal file
50
app.py
Normal file
@ -0,0 +1,50 @@
|
||||
from flask import Flask, render_template, request
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def _calculate(a: float, b: float, op: str) -> float:
|
||||
if op == "+":
|
||||
return a + b
|
||||
if op == "-":
|
||||
return a - b
|
||||
if op == "*":
|
||||
return a * b
|
||||
if op == "/":
|
||||
if b == 0:
|
||||
raise ZeroDivisionError("Division by zero")
|
||||
return a / b
|
||||
raise ValueError("Unknown operation")
|
||||
|
||||
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
def index():
|
||||
result = None
|
||||
error = None
|
||||
a_val = ""
|
||||
b_val = ""
|
||||
op = "+"
|
||||
|
||||
if request.method == "POST":
|
||||
a_val = request.form.get("a", "")
|
||||
b_val = request.form.get("b", "")
|
||||
op = request.form.get("op", "+")
|
||||
try:
|
||||
a = float(a_val)
|
||||
b = float(b_val)
|
||||
result = _calculate(a, b, op)
|
||||
except Exception as exc: # noqa: BLE001 - show a friendly message
|
||||
error = str(exc)
|
||||
|
||||
return render_template(
|
||||
"index.html",
|
||||
result=result,
|
||||
error=error,
|
||||
a_val=a_val,
|
||||
b_val=b_val,
|
||||
op=op,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000, debug=False)
|
||||
@ -1,18 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"oreshnik/internal/app/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
s, err := server.New()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to init server: %v", err)
|
||||
}
|
||||
fmt.Println("Server started on :8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", s.Router()))
|
||||
}
|
||||
@ -1,21 +1,6 @@
|
||||
services:
|
||||
app:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "8099:8080"
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- DB_HOST=db
|
||||
- DB_USER=goidactf
|
||||
- DB_PASSWORD=goidactf
|
||||
- DB_NAME=goidactf
|
||||
- DB_PORT=5432
|
||||
- FLAG=${FLAG}
|
||||
|
||||
db:
|
||||
image: postgres:13-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=goidactf
|
||||
- POSTGRES_PASSWORD=goidactf
|
||||
- POSTGRES_DB=goidactf
|
||||
- "5000:5000"
|
||||
restart: unless-stopped
|
||||
|
||||
21
go.mod
21
go.mod
@ -1,21 +0,0 @@
|
||||
module oreshnik
|
||||
|
||||
go 1.25.4
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
golang.org/x/crypto v0.47.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
)
|
||||
38
go.sum
38
go.sum
@ -1,38 +0,0 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
@ -1,58 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
func LoadKeys() (*rsa.PrivateKey, *rsa.PublicKey, error) {
|
||||
privBytes, err := os.ReadFile("public/private.pem")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("read private key: %w", err)
|
||||
}
|
||||
priv, err := jwt.ParseRSAPrivateKeyFromPEM(privBytes)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("parse private key: %w", err)
|
||||
}
|
||||
|
||||
pubBytes, err := os.ReadFile("public/public.pem")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("read public key: %w", err)
|
||||
}
|
||||
pub, err := jwt.ParseRSAPublicKeyFromPEM(pubBytes)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("parse public key: %w", err)
|
||||
}
|
||||
return priv, pub, nil
|
||||
}
|
||||
|
||||
func GenerateJWT(userID uint, isAdmin bool, privateKey *rsa.PrivateKey) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"user_id": userID,
|
||||
"is_admin": isAdmin,
|
||||
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
return token.SignedString(privateKey)
|
||||
}
|
||||
|
||||
func Parse(tokenString string, publicKey *rsa.PublicKey) (*jwt.Token, jwt.MapClaims, error) {
|
||||
tok, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
return publicKey, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
claims, ok := tok.Claims.(jwt.MapClaims)
|
||||
if !ok || !tok.Valid {
|
||||
return nil, nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
return tok, claims, nil
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func ConnectAndMigrate(models ...interface{}) (*gorm.DB, error) {
|
||||
dsn := fmt.Sprintf(
|
||||
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
|
||||
os.Getenv("DB_HOST"),
|
||||
os.Getenv("DB_USER"),
|
||||
os.Getenv("DB_PASSWORD"),
|
||||
os.Getenv("DB_NAME"),
|
||||
os.Getenv("DB_PORT"),
|
||||
)
|
||||
|
||||
var database *gorm.DB
|
||||
var err error
|
||||
for i := 0; i < 10; i++ {
|
||||
database, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
log.Println("Failed to connect to database, retrying...")
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect database: %w", err)
|
||||
}
|
||||
|
||||
if err := database.AutoMigrate(models...); err != nil {
|
||||
return nil, fmt.Errorf("auto migrate: %w", err)
|
||||
}
|
||||
return database, nil
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type RevokedToken struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Token string `gorm:"unique"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Username string `gorm:"unique"`
|
||||
Password string
|
||||
IsAdmin bool `gorm:"default:false"`
|
||||
}
|
||||
|
||||
type Product struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string
|
||||
Description string
|
||||
Price float64
|
||||
}
|
||||
|
||||
type Purchase struct {
|
||||
gorm.Model
|
||||
UserID uint
|
||||
ProductID uint
|
||||
Product Product `gorm:"foreignKey:ProductID"`
|
||||
}
|
||||
|
||||
var _ = gorm.Model{}
|
||||
@ -1,303 +0,0 @@
|
||||
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
|
||||
}
|
||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
Flask==3.0.3
|
||||
@ -1,14 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('Магазин "Орешник" загружен и готов к работе!');
|
||||
|
||||
const productCards = document.querySelectorAll('.product-card-link');
|
||||
productCards.forEach(card => {
|
||||
card.addEventListener('mouseover', () => {
|
||||
card.style.transform = 'scale(1.05)';
|
||||
card.style.transition = 'transform 0.2s';
|
||||
});
|
||||
card.addEventListener('mouseout', () => {
|
||||
card.style.transform = 'scale(1)';
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,95 +0,0 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #4a3f35; /* A warmer, nut-brown color */
|
||||
color: #fff;
|
||||
padding: 0 40px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 70px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
margin-left: 20px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
background-color: #6d5c4b;
|
||||
}
|
||||
|
||||
nav span {
|
||||
margin-left: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.product-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-weight: bold;
|
||||
color: #0056b3;
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.auth-form input {
|
||||
margin-bottom: 10px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.flag {
|
||||
font-family: monospace;
|
||||
background-color: #eee;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Админ-панель</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1><a href="/" class="logo">Магазин "Орешник"</a></h1>
|
||||
<nav>
|
||||
<a href="/">Главная</a>
|
||||
{{if .LoggedIn}}
|
||||
<span>Привет, {{.Username}}!</span>
|
||||
<a href="/my-purchases">Мои покупки</a>
|
||||
<a href="/logout">Выйти</a>
|
||||
{{else}}
|
||||
<a href="/login">Войти</a>
|
||||
<a href="/register">Регистрация</a>
|
||||
{{end}}
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<h2>Секретная информация</h2>
|
||||
<p class="flag">Флаг: {{.Flag}}</p>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,45 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Магазин "Орешник"</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1><a href="/" class="logo">Магазин "Орешник"</a></h1>
|
||||
<nav>
|
||||
<a href="/">Главная</a>
|
||||
{{if .LoggedIn}}
|
||||
<span>Привет, {{.Username}}!</span>
|
||||
<a href="/my-purchases">Мои покупки</a>
|
||||
<a href="/logout">Выйти</a>
|
||||
{{else}}
|
||||
<a href="/login">Войти</a>
|
||||
<a href="/register">Регистрация</a>
|
||||
{{end}}
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{{if .LoggedIn}}
|
||||
<h2>Наши товары</h2>
|
||||
<div class="product-grid">
|
||||
{{range .Products}}
|
||||
<a href="/product/{{.ID}}" class="product-card-link">
|
||||
<div class="product-card">
|
||||
<h3>{{.Name}}</h3>
|
||||
<p>{{.Description}}</p>
|
||||
<p class="price">Цена: {{.Price}} руб.</p>
|
||||
</div>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<h2>Пожалуйста, войдите, чтобы увидеть наши товары.</h2>
|
||||
{{end}}
|
||||
</main>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Flask Calculator</title>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
body {
|
||||
font-family: "Trebuchet MS", "Verdana", sans-serif;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #f7f0e8, #e6f2f5);
|
||||
color: #1f2d33;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 720px;
|
||||
margin: 8vh auto;
|
||||
padding: 32px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
|
||||
border-radius: 18px;
|
||||
}
|
||||
h1 { margin: 0 0 12px; }
|
||||
p { margin: 0 0 24px; color: #3d4c52; }
|
||||
form {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 1fr 120px 1fr;
|
||||
align-items: center;
|
||||
}
|
||||
input, select, button {
|
||||
font-size: 16px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #c4d3d9;
|
||||
}
|
||||
button {
|
||||
grid-column: span 3;
|
||||
background: #1f6f78;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.result {
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background: #f0f7f8;
|
||||
}
|
||||
.error { color: #b22020; }
|
||||
@media (max-width: 640px) {
|
||||
form { grid-template-columns: 1fr; }
|
||||
button { grid-column: span 1; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>Calculator</h1>
|
||||
<p>Simple Flask calculator. Enter two numbers and choose an operation.</p>
|
||||
<form method="post">
|
||||
<input name="a" type="number" step="any" placeholder="First number" value="{{ a_val }}" required />
|
||||
<select name="op">
|
||||
<option value="+" {% if op == "+" %}selected{% endif %}>+</option>
|
||||
<option value="-" {% if op == "-" %}selected{% endif %}>-</option>
|
||||
<option value="*" {% if op == "*" %}selected{% endif %}>*</option>
|
||||
<option value="/" {% if op == "/" %}selected{% endif %}>/</option>
|
||||
</select>
|
||||
<input name="b" type="number" step="any" placeholder="Second number" value="{{ b_val }}" required />
|
||||
<button type="submit">Calculate</button>
|
||||
</form>
|
||||
|
||||
{% if result is not none %}
|
||||
<div class="result">Result: <strong>{{ result }}</strong></div>
|
||||
{% endif %}
|
||||
{% if error %}
|
||||
<div class="result error">Error: {{ error }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Вход - Магазин "Орешник"</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1><a href="/" class="logo">Магазин "Орешник"</a></h1>
|
||||
<nav>
|
||||
<a href="/">Главная</a>
|
||||
{{if .LoggedIn}}
|
||||
<span>Привет, {{.Username}}!</span>
|
||||
<a href="/my-purchases">Мои покупки</a>
|
||||
<a href="/logout">Выйти</a>
|
||||
{{else}}
|
||||
<a href="/login">Войти</a>
|
||||
<a href="/register">Регистрация</a>
|
||||
{{end}}
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<form action="/login" method="post" class="auth-form">
|
||||
<label for="username">Имя пользователя:</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
<label for="password">Пароль:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
<button type="submit">Войти</button>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,44 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Мои покупки - Магазин "Орешник"</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1><a href="/" class="logo">Магазин "Орешник"</a></h1>
|
||||
<nav>
|
||||
<a href="/">Главная</a>
|
||||
{{if .LoggedIn}}
|
||||
<span>Привет, {{.Username}}!</span>
|
||||
<a href="/my-purchases">Мои покупки</a>
|
||||
<a href="/logout">Выйти</a>
|
||||
{{else}}
|
||||
<a href="/login">Войти</a>
|
||||
<a href="/register">Регистрация</a>
|
||||
{{end}}
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<h2>Мои покупки</h2>
|
||||
{{if .Purchases}}
|
||||
<div class="product-grid">
|
||||
{{range .Purchases}}
|
||||
<a href="/order/{{.ID}}" class="product-card-link">
|
||||
<div class="product-card">
|
||||
<h3>{{.Product.Name}}</h3>
|
||||
<p>Заказ #{{.ID}}</p>
|
||||
</div>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p>Вы еще ничего не купили.</p>
|
||||
{{end}}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,40 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Детали заказа - Магазин "Орешник"</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1><a href="/" class="logo">Магазин "Орешник"</a></h1>
|
||||
<nav>
|
||||
<a href="/">Главная</a>
|
||||
{{if .LoggedIn}}
|
||||
<span>Привет, {{.Username}}!</span>
|
||||
<a href="/my-purchases">Мои покупки</a>
|
||||
<a href="/logout">Выйти</a>
|
||||
{{else}}
|
||||
<a href="/login">Войти</a>
|
||||
<a href="/register">Регистрация</a>
|
||||
{{end}}
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<h2>Детали заказа #{{.Purchase.ID}}</h2>
|
||||
{{if .Purchase}}
|
||||
<div class="product-card">
|
||||
<h3>{{.Purchase.Product.Name}}</h3>
|
||||
<p>{{.Purchase.Product.Description}}</p>
|
||||
<p class="price">Цена: {{.Purchase.Product.Price}} руб.</p>
|
||||
<p>Куплено: {{.Purchase.CreatedAt.Format "02.01.2006 15:04"}}</p>
|
||||
</div>
|
||||
{{else}}
|
||||
<p>Заказ не найден.</p>
|
||||
{{end}}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,36 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{.Product.Name}} - Магазин "Орешник"</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1><a href="/" class="logo">Магазин "Орешник"</a></h1>
|
||||
<nav>
|
||||
<a href="/">Главная</a>
|
||||
{{if .LoggedIn}}
|
||||
<span>Привет, {{.Username}}!</span>
|
||||
<a href="/my-purchases">Мои покупки</a>
|
||||
<a href="/logout">Выйти</a>
|
||||
{{else}}
|
||||
<a href="/login">Войти</a>
|
||||
<a href="/register">Регистрация</a>
|
||||
{{end}}
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<div class="product-detail">
|
||||
<p>{{.Product.Description}}</p>
|
||||
<p class="price">Цена: {{.Product.Price}} руб.</p>
|
||||
<form action="/buy/{{.Product.ID}}" method="post">
|
||||
<button type="submit">Купить</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,30 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Спасибо за покупку!</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1><a href="/" class="logo">Магазин "Орешник"</a></h1>
|
||||
<nav>
|
||||
<a href="/">Вернуться на главную</a>
|
||||
{{if .LoggedIn}}
|
||||
<span>Привет, {{.Username}}!</span>
|
||||
<a href="/my-purchases">Мои покупки</a>
|
||||
<a href="/logout">Выйти</a>
|
||||
{{else}}
|
||||
<a href="/login">Войти</a>
|
||||
<a href="/register">Регистрация</a>
|
||||
{{end}}
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<p>Ваш заказ успешно оформлен.</p>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,36 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Регистрация - Магазин "Орешник"</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1><a href="/" class="logo">Магазин "Орешник"</a></h1>
|
||||
<nav>
|
||||
<a href="/">Главная</a>
|
||||
{{if .LoggedIn}}
|
||||
<span>Привет, {{.Username}}!</span>
|
||||
<a href="/my-purchases">Мои покупки</a>
|
||||
<a href="/logout">Выйти</a>
|
||||
{{else}}
|
||||
<a href="/login">Войти</a>
|
||||
<a href="/register">Регистрация</a>
|
||||
{{end}}
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<form action="/register" method="post" class="auth-form">
|
||||
<label for="username">Имя пользователя:</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
<label for="password">Пароль:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
<button type="submit">Зарегистрироваться</button>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user