From 87ec792fd5cd58e4adae6f3f188824de92b813ba Mon Sep 17 00:00:00 2001 From: LeonG11 Date: Thu, 7 May 2026 17:57:49 +0300 Subject: [PATCH] Remake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавил авторизацию в приложение, сделал регистарцию, добавил мидлы --- .gitea/workflows/ci.yaml | 51 --------------- backend/handlers/auth_handler.go | 34 ++++++++++ backend/main.go | 26 +++++--- backend/middleware/auth.go | 48 ++++++++++++++ frontend/Dockerfile | 17 +++++ frontend/src/App.jsx | 41 ++++++++++-- frontend/src/api/index.js | 24 +++++++ frontend/src/components/AuthForm.jsx | 98 ++++++++++++++++++++++++++++ 8 files changed, 275 insertions(+), 64 deletions(-) delete mode 100644 .gitea/workflows/ci.yaml create mode 100644 backend/middleware/auth.go create mode 100644 frontend/src/components/AuthForm.jsx diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml deleted file mode 100644 index cfe22b6..0000000 --- a/.gitea/workflows/ci.yaml +++ /dev/null @@ -1,51 +0,0 @@ -name: MRP Full Stack CI/CD - -on: - push: - branches: [main, master] - -jobs: - test-backend: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 # Используем v5 (стабильнее) - with: - go-version: "1.24" - - - name: Run Go tests - run: | - cd backend - go test ./... -v - - test-frontend: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Install & Build - run: | - cd frontend - npm install --legacy-peer-deps - npm run build - - build-and-deploy: - runs-on: ubuntu-latest - needs: [test-backend, test-frontend] - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Deploy full stack - run: | - # Мы не используем Buildx, так как работаем напрямую с Docker хоста - docker compose up -d --build --remove-orphans diff --git a/backend/handlers/auth_handler.go b/backend/handlers/auth_handler.go index 265655c..3a8578e 100644 --- a/backend/handlers/auth_handler.go +++ b/backend/handlers/auth_handler.go @@ -46,3 +46,37 @@ func Login(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"token": tokenString}) } + +func Register(c *gin.Context) { + var input struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + } + + // 1. Проверяем входящие данные + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) + return + } + + // 2. Хешируем пароль (чтобы не хранить его в открытом виде) + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"}) + return + } + + // 3. Создаем объект пользователя + user := models.User{ + Username: input.Username, + Password: string(hashedPassword), + Role: "user", // по умолчанию + } + + // 4. Сохраняем в базу через GORM + if err := database.DB.Create(&user).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Could not create user maybe username exists?"}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "Registration successful"}) +} diff --git a/backend/main.go b/backend/main.go index a3425c0..9eb0704 100644 --- a/backend/main.go +++ b/backend/main.go @@ -8,6 +8,7 @@ import ( "viplight-mrp/database" "viplight-mrp/handlers" + "viplight-mrp/middleware" ) func main() { @@ -24,7 +25,7 @@ func main() { r.Use(func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "https://mrp.kkhome.ru") c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS") - c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return @@ -32,14 +33,21 @@ func main() { c.Next() }) - // Роуты теперь вызывают функции из handlers - r.GET("/api/parts/:id", handlers.GetPart) - r.GET("/api/parts", handlers.GetAllParts) - r.GET("/api/orders", handlers.GetsOrders) - r.POST("/api/parts", handlers.CreatePart) - r.POST("/api/parts/bulk", handlers.ImportParts) - r.PATCH("/api/parts/:id/status", handlers.UpdateStatus) - r.DELETE("/api/parts/:id", handlers.DeletePart) + r.POST("/register", handlers.Register) + r.POST("/login", handlers.Login) + + protected := r.Group("/api") + protected.Use(middleware.AuthRequired()) + { + protected.GET("/parts/:id", handlers.GetPart) + protected.GET("/parts", handlers.GetAllParts) + protected.GET("/orders", handlers.GetsOrders) + protected.POST("/parts", handlers.CreatePart) + protected.POST("/parts/bulk", handlers.ImportParts) + protected.PATCH("/parts/:id/status", handlers.UpdateStatus) + protected.DELETE("/parts/:id", handlers.DeletePart) + + } r.Run(":8090") } diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go new file mode 100644 index 0000000..aa3570a --- /dev/null +++ b/backend/middleware/auth.go @@ -0,0 +1,48 @@ +package middleware + +import ( + "net/http" + "os" + "strings" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt" +) + +func AuthRequired() gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) + c.Abort() + return + } + + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + if tokenString == authHeader { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token format"}) + c.Abort() + return + } + + jwtKey := []byte(os.Getenv("JWT_SECRET")) + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return jwtKey, nil + }) + + if err != nil || !token.Valid { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"}) + c.Abort() + return + + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok { + c.Set("user_id", claims["user_id"]) + c.Set("role", claims["role"]) + } + + c.Next() + } +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 0809b05..62a7898 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -13,9 +13,26 @@ FROM nginx:stable-alpine COPY --from=build /app/dist /usr/share/nginx/html + COPY < { + const token = localStorage.getItem("token"); + + if (!token) { + return ; + } + + return children; +}; export default function App() { return ( - } /> - } /> - } /> + } /> + + + + } + /> + + + + } + /> + + + + } + /> ); diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index c3d9e32..d2697dc 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -3,6 +3,30 @@ import Papa from "papaparse"; const API_URL = "/api"; +axios.interceptors.request.use( + (config) => { + const token = localStorage.getItem("token"); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + }, +); + +axios.interceptors.response.use( + (response) => response, + (error) => { + if (error.response && error.response.status === 401) { + localStorage.removeItem("token"); + window.location.href = "/login"; + } + return Promise.reject(error); + }, +); + // Базовые CRUD операции export const getAllParts = () => axios.get(`${API_URL}/parts`); export const getAllOrders = () => axios.get(`${API_URL}/orders`); diff --git a/frontend/src/components/AuthForm.jsx b/frontend/src/components/AuthForm.jsx new file mode 100644 index 0000000..67635d2 --- /dev/null +++ b/frontend/src/components/AuthForm.jsx @@ -0,0 +1,98 @@ +import { useState } from "react"; +import axios from "axios"; +import { useNavigate } from "react-router-dom"; + +const AuthForm = () => { + const [isLogin, setIsLogin] = useState(true); + const [formData, setFormData] = useState({ username: "", password: "" }); + const [message, setMessage] = useState({ text: "", isError: false }); + const navigate = useNavigate(); + + const API_BASE = ""; + + const handleSubmit = async (e) => { + e.preventDefault(); + setMessage({ text: "", isError: false }); + + const endpoint = isLogin ? "/login" : "/register"; + + try { + const { data } = await axios.post(`${API_BASE}${endpoint}`, formData); + + if (isLogin) { + localStorage.setItem("token", data.token); + navigate("/"); + } else { + setMessage({ + text: "Регистрация успешна! Теперь войдите", + isError: false, + }); + setIsLogin(true); + } + } catch (err) { + setMessage({ + text: err.response?.data?.error || "Ошибка сервера", + isError: true, + }); + } + }; + + return ( +
+
+

+ {isLogin ? "Вход в MRP" : "Регистрация"} +

+ +
+
+ + setFormData({ ...formData, username: e.target.value }) + } + /> + + setFormData({ ...formData, password: e.target.value }) + } + /> +
+ + {message.text && ( +

+ {message.text} +

+ )} + + +
+ +
+ +
+
+
+ ); +}; + +export default AuthForm;