diff --git a/backend/go1.26.2.linux-amd64.tar.gz b/backend/go1.26.2.linux-amd64.tar.gz deleted file mode 100644 index 8d027d1..0000000 Binary files a/backend/go1.26.2.linux-amd64.tar.gz and /dev/null differ diff --git a/backend/handlers/auth_handler.go b/backend/handlers/auth_handler.go deleted file mode 100644 index 3a8578e..0000000 --- a/backend/handlers/auth_handler.go +++ /dev/null @@ -1,82 +0,0 @@ -package handlers - -import ( - "net/http" - "os" - "time" - - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" - "golang.org/x/crypto/bcrypt" - - "viplight-mrp/database" - "viplight-mrp/models" -) - -func Login(c *gin.Context) { - jwtKey := []byte(os.Getenv("JWT_SECRET")) - var input struct { - Username string `json:"username"` - Password string `json:"password"` - } - - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) - return - } - - var user models.User - - err := database.DB.Where("username = ?", input.Username).First(&user).Error - - passErr := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)) - - if err != nil || passErr != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid login or password"}) - return - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "user_id": user.ID, - "role": user.Role, - "exp": time.Now().Add(time.Hour * 24).Unix(), - }) - - tokenString, _ := token.SignedString(jwtKey) - - 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/handlers/user_handler.go b/backend/handlers/user_handler.go new file mode 100644 index 0000000..efcf1f9 --- /dev/null +++ b/backend/handlers/user_handler.go @@ -0,0 +1,122 @@ +package handlers + +import ( + "net/http" + "os" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" + + "viplight-mrp/database" + "viplight-mrp/models" +) + +func Login(c *gin.Context) { + jwtKey := []byte(os.Getenv("JWT_SECRET")) + var input struct { + Username string `json:"username"` + Password string `json:"password"` + } + + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) + return + } + + var user models.User + + err := database.DB.Where("username = ?", input.Username).First(&user).Error + + passErr := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)) + + if err != nil || passErr != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid login or password"}) + return + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "user_id": user.ID, + "role": user.Role, + "exp": time.Now().Add(time.Hour * 24).Unix(), + }) + + tokenString, _ := token.SignedString(jwtKey) + + c.JSON(http.StatusOK, gin.H{"token": tokenString, "user": gin.H{ + "id": user.ID, + "first_name": user.FirstName, + "last_name": user.LastName, + "phone": user.Phone, + "department": user.Department, + "role": user.Role, + }}) +} + +func Register(c *gin.Context) { + var input struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required,min=8"` + FirstName string `json:"first_name" binding:"required"` + LastName string `json:"last_name" binding:"required"` + Phone string `json:"phone"` + Department string `json:"department" binding:"required"` + Role string `json:"role" binding:"required"` + } + + // 1. Проверяем входящие данные + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Ошибка валидации ", "details": err.Error()}) + return + } + + // 2. Хешируем пароль (чтобы не хранить его в открытом виде) + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка при шифровании пароля"}) + return + } + + // 3. Создаем объект пользователя + user := models.User{ + Username: input.Username, + Password: string(hashedPassword), + FirstName: input.FirstName, + LastName: input.LastName, + Phone: input.Phone, + Department: input.Department, + Role: input.Role, // по умолчанию + } + + // 4. Сохраняем в базу через GORM + if err := database.DB.Create(&user).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка при регистрации пользователя", "details": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "Пользователь успешно зарегистрирован"}) +} + +func UpdateProfile(c *gin.Context) { + var input models.UpdateUserInput + + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userID := c.MustGet("user_id") + + if err := database.DB.Model(&models.User{}).Where("id = ?", userID).Updates(models.User{ + FirstName: input.FirstName, + LastName: input.LastName, + Phone: input.Phone, + Department: input.Department, + Role: input.Role, + }).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Не удалось обновить данные", "details": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Данные успешно обновлены"}) +} diff --git a/backend/main.go b/backend/main.go index 23bc8cd..d453209 100644 --- a/backend/main.go +++ b/backend/main.go @@ -35,9 +35,7 @@ func main() { api := r.Group("/api") { - api.POST("/register", handlers.Register) api.POST("/login", handlers.Login) - protected := api.Group("/") protected.Use(middleware.AuthRequired()) { @@ -47,8 +45,14 @@ func main() { protected.POST("/parts", handlers.CreatePart) protected.POST("/parts/bulk", handlers.ImportParts) protected.PATCH("/parts/:id/status", handlers.UpdateStatus) + protected.PATCH("/user/update", handlers.UpdateProfile) protected.DELETE("/parts/:id", handlers.DeletePart) + } + admin := protected.Group("/") + admin.Use(middleware.AdminOnly()) + { + admin.POST("/register", handlers.Register) } } diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go index aa3570a..30da8a9 100644 --- a/backend/middleware/auth.go +++ b/backend/middleware/auth.go @@ -35,14 +35,29 @@ func AuthRequired() gin.HandlerFunc { 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"]) + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + if id, ok := claims["user_id"].(float64); ok { + c.Set("user_id", uint(id)) + } c.Set("role", claims["role"]) } c.Next() } } + +func AdminOnly() gin.HandlerFunc { + return func(c *gin.Context) { + role, exists := c.Get("role") + + if !exists || role != "Админ" { + c.JSON(http.StatusForbidden, gin.H{"error": "Доступ ограничен"}) + c.Abort() + return + } + + c.Next() + } +} diff --git a/backend/models/update_user.go b/backend/models/update_user.go new file mode 100644 index 0000000..01a75cf --- /dev/null +++ b/backend/models/update_user.go @@ -0,0 +1,9 @@ +package models + +type UpdateUserInput struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Phone string `json:"phone"` + Department string `json:"department"` + Role string `json:"role"` +} diff --git a/backend/models/user.go b/backend/models/user.go index e5b2979..ecf4dae 100644 --- a/backend/models/user.go +++ b/backend/models/user.go @@ -4,7 +4,11 @@ import "gorm.io/gorm" type User struct { gorm.Model `json:"-"` - Username string `gorm:"unique;not null"` - Password string `gorm:"not null" json:"-"` + Username string `gorm:"unique;not null" binding:"required"` + Password string `gorm:"not null" json:"-" binding:"required,min=8"` Role string `gorm:"default:'Рабочий'" json:"role"` + FirstName string `json:"first_name" binding:"required"` + LastName string `gorm:"index" json:"last_name" binding:"required"` + Phone string `gorm:"index" json:"phone"` + Department string `json:"department" binding:"required"` } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2649a2a..5efafe0 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,7 +2,9 @@ import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import Dashboard from "./pages/Dashboard"; import PartDetail from "./pages/PartDetail"; import AddPartPage from "./pages/AddPartPage"; +import ProfilePage from "./pages/ProfilePage"; import AuthForm from "./components/AuthForm"; +import RegisterForm from "./components/RegisterForm"; const PrivateRoute = ({ children }) => { const token = localStorage.getItem("token"); @@ -19,6 +21,14 @@ export default function App() { } /> + + + + } + /> { }); }); }; + +export const registerUser = (userData) => { + axios.post(`${API_URL}/register`, userData); +}; +export const getMe = () => { + axios.post(`${API_URL}/me`); +}; diff --git a/frontend/src/components/AuthForm.jsx b/frontend/src/components/AuthForm.jsx index bb302c2..d9ab8e6 100644 --- a/frontend/src/components/AuthForm.jsx +++ b/frontend/src/components/AuthForm.jsx @@ -14,10 +14,12 @@ const AuthForm = () => { setMessage({ text: "", isError: false }); try { - const { data } = await axios.post(`${API_BASE}/login`, formData); + const response = await axios.post(`${API_BASE}/login`, formData); + const data = response.data; if (data.token) { localStorage.setItem("token", data.token); + localStorage.setItem("user", JSON.stringify(data.user)); navigate("/"); } } catch (err) { diff --git a/frontend/src/components/CsvImporter.jsx b/frontend/src/components/CsvImporter.jsx index 52fcf1f..beddaf6 100644 --- a/frontend/src/components/CsvImporter.jsx +++ b/frontend/src/components/CsvImporter.jsx @@ -1,4 +1,4 @@ -import React, { useRef } from "react"; +import { useRef } from "react"; import { parseAndImportCsv } from "../api"; export default function CsvImporter({ onImported }) { diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 2f26991..2dc42e3 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -4,6 +4,18 @@ import { useNavigate } from "react-router-dom"; export default function Header({ onRefresh, onOpenScanner }) { const navigate = useNavigate(); + const handleLogout = () => { + localStorage.clear(); + navigate("/login"); + }; + + const handleUser = () => { + const savedUser = localStorage.getItem("user"); + return savedUser ? JSON.parse(savedUser) : {}; + }; + + const user = handleUser(); + return (
@@ -29,6 +41,44 @@ export default function Header({ onRefresh, onOpenScanner }) { > 📷 СКАНЕР + + {/* Блок пользователя */} +
navigate("/profile")} + className="flex items-center gap-4 border-l border-slate-800 pl-6" + > +
+

+ {user.first_name} {user.last_name} +

+

+ {user.department} |{" "} + {user.role} +

+
+ +
+
+ {user.first_name.charAt(0).toUpperCase()} +
+ + {/* Меню при наведении */} +
+
+

+ {user.first_name} +

+

{user.role}

+
+ +
+
+
diff --git a/frontend/src/components/RegisterForm.jsx b/frontend/src/components/RegisterForm.jsx new file mode 100644 index 0000000..5b17d02 --- /dev/null +++ b/frontend/src/components/RegisterForm.jsx @@ -0,0 +1,149 @@ +import { useState } from "react"; +import { registerUser } from "../api"; // Используем твой метод из index.js +import { useNavigate } from "react-router-dom"; + +export default function RegisterForm() { + const navigate = useNavigate(); + const [formData, setFormData] = useState({ + username: "", + password: "", + first_name: "", + last_name: "", + phone: "", + department: "", + role: "Рабочий", + }); + const [message, setMessage] = useState({ text: "", isError: false }); + + const handleSubmit = async (e) => { + e.preventDefault(); + setMessage({ text: "", isError: false }); + + try { + await registerUser(formData); + setMessage({ text: "Сотрудник успешно создан!", isError: false }); + // Небольшая задержка перед переходом, чтобы админ увидел успех + setTimeout(() => navigate("/"), 1500); + } catch (err) { + setMessage({ + text: err.response?.data?.error || "Ошибка регистрации", + isError: true, + }); + } + }; + + return ( +
+
+
+

+ VIPLIGHT MRP +

+

+ Регистрация сотрудника +

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

+ {message.text} +

+ )} + +
+ + +
+
+
+
+ ); +} diff --git a/frontend/src/pages/ProfilePage.jsx b/frontend/src/pages/ProfilePage.jsx new file mode 100644 index 0000000..023a35d --- /dev/null +++ b/frontend/src/pages/ProfilePage.jsx @@ -0,0 +1,127 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import axios from "axios"; + +export default function ProfilePage() { + const navigate = useNavigate(); + // Инициализируем состояние данными из localStorage + const [userData, setUserData] = useState(() => { + const user = JSON.parse(localStorage.getItem("user") || "{}"); + return user; + }); + + const handleChange = (e) => { + setUserData({ ...userData, [e.target.name]: e.target.value }); + }; + + const handleUpdate = async (e) => { + e.preventDefault(); + try { + const token = localStorage.getItem("token"); + + // Отправляем PATCH или PUT запрос на бэкенд для обновления профиля + await axios.patch("/api/user/update", userData, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + const updatedUser = { + ...JSON.parse(localStorage.getItem("user") || "{}"), + ...userData, + }; + + localStorage.setItem("user", JSON.stringify(updatedUser)); + alert("Данные успешно обновлены!"); + } catch (err) { + alert("Ошибка обновления: " + err.response?.data?.error); + } + }; + + return ( +
+
+

+ Мой профиль +

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+
+
+
+ ); +}