1079 lines
26 KiB
Go
1079 lines
26 KiB
Go
package handler
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/wmms/wmms/internal/middleware"
|
|
"github.com/wmms/wmms/internal/model"
|
|
"github.com/wmms/wmms/internal/service"
|
|
"github.com/wmms/wmms/pkg/response"
|
|
)
|
|
|
|
type Handler struct {
|
|
materialSvc *service.MaterialService
|
|
inboundSvc *service.InboundService
|
|
outboundSvc *service.OutboundService
|
|
stockSvc *service.StockService
|
|
logSvc *service.MaterialLogService
|
|
authSvc *service.AuthService
|
|
appSvc *service.ApplicationService
|
|
}
|
|
|
|
func NewHandler() *Handler {
|
|
return &Handler{
|
|
materialSvc: service.NewMaterialService(),
|
|
inboundSvc: service.NewInboundService(),
|
|
outboundSvc: service.NewOutboundService(),
|
|
stockSvc: service.NewStockService(),
|
|
logSvc: service.NewMaterialLogService(),
|
|
authSvc: service.NewAuthService(),
|
|
appSvc: service.NewApplicationService(),
|
|
}
|
|
}
|
|
|
|
func (h *Handler) RegisterRoutes(r *gin.Engine) {
|
|
api := r.Group("/api")
|
|
{
|
|
// Protected upload route
|
|
upload := api.Group("/upload")
|
|
upload.Use(middleware.AuthMiddleware())
|
|
{
|
|
upload.POST("", h.UploadImage)
|
|
}
|
|
|
|
// Auth routes (public, with rate limiting)
|
|
auth := api.Group("/auth")
|
|
auth.Use(middleware.RateLimitMiddleware())
|
|
{
|
|
auth.POST("/login", h.Login)
|
|
}
|
|
|
|
// Protected routes
|
|
logs := api.Group("/logs")
|
|
logs.Use(middleware.AuthMiddleware())
|
|
{
|
|
logs.GET("", h.ListLogs)
|
|
logs.GET("/materials/:materialId", h.GetLogsByMaterial)
|
|
}
|
|
|
|
materials := api.Group("/materials")
|
|
materials.Use(middleware.AuthMiddleware())
|
|
{
|
|
materials.GET("", h.ListMaterials)
|
|
materials.GET("/:id", h.GetMaterial)
|
|
materials.POST("", h.CreateMaterial)
|
|
materials.PUT("/:id", h.UpdateMaterial)
|
|
materials.DELETE("/:id", h.DeleteMaterial)
|
|
}
|
|
|
|
inbound := api.Group("/inbound")
|
|
inbound.Use(middleware.AuthMiddleware())
|
|
{
|
|
inbound.GET("", h.ListInbound)
|
|
inbound.GET("/:id", h.GetInbound)
|
|
inbound.POST("", h.CreateInbound)
|
|
inbound.PUT("/:id", h.UpdateInbound)
|
|
inbound.DELETE("/:id", h.DeleteInbound)
|
|
inbound.GET("/materials/:materialId", h.GetInboundByMaterial)
|
|
}
|
|
|
|
outbound := api.Group("/outbound")
|
|
outbound.Use(middleware.AuthMiddleware())
|
|
{
|
|
outbound.GET("", h.ListOutbound)
|
|
outbound.GET("/:id", h.GetOutbound)
|
|
outbound.POST("", h.CreateOutbound)
|
|
outbound.PUT("/:id", h.UpdateOutbound)
|
|
outbound.DELETE("/:id", h.DeleteOutbound)
|
|
outbound.GET("/materials/:materialId", h.GetOutboundByMaterial)
|
|
}
|
|
|
|
stock := api.Group("/stock")
|
|
stock.Use(middleware.AuthMiddleware())
|
|
{
|
|
stock.GET("", h.ListStock)
|
|
stock.GET("/warning", h.GetWarningStock)
|
|
stock.GET("/logs", h.GetAllStockLogs)
|
|
stock.GET("/logs/:materialId", h.GetStockLogs)
|
|
stock.GET("/:materialId", h.GetStock)
|
|
}
|
|
|
|
// Admin only routes
|
|
users := api.Group("/users")
|
|
users.Use(middleware.AuthMiddleware(), middleware.AdminOnly())
|
|
{
|
|
users.GET("", h.ListUsers)
|
|
users.POST("", h.CreateUser)
|
|
users.PUT("/:id", h.UpdateUser)
|
|
users.DELETE("/:id", h.DeleteUser)
|
|
}
|
|
|
|
// Application routes (user: create/cancel own, list own; admin: list all, approve/reject)
|
|
applications := api.Group("/applications")
|
|
applications.Use(middleware.AuthMiddleware())
|
|
{
|
|
applications.POST("", h.CreateApplication)
|
|
applications.GET("/my", h.ListMyApplications)
|
|
applications.POST("/:id/cancel", h.CancelApplication)
|
|
applications.GET("", h.ListApplications) // admin only
|
|
applications.POST("/:id/approve", h.ApproveApplication) // admin only
|
|
applications.POST("/:id/reject", h.RejectApplication) // admin only
|
|
}
|
|
|
|
// Current user route
|
|
me := api.Group("/auth")
|
|
me.Use(middleware.AuthMiddleware())
|
|
{
|
|
me.GET("/me", h.GetCurrentUser)
|
|
me.POST("/logout", h.Logout)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Material handlers
|
|
|
|
func (h *Handler) ListMaterials(c *gin.Context) {
|
|
search := c.Query("search")
|
|
category := c.Query("category")
|
|
|
|
limit := 20
|
|
offset := 0
|
|
if l := c.Query("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
}
|
|
}
|
|
if o := c.Query("offset"); o != "" {
|
|
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
|
|
materials, total, err := h.materialSvc.List(search, category, limit, offset)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
if materials == nil {
|
|
materials = []model.Material{}
|
|
}
|
|
response.PaginatedSuccess(c, materials, limit, offset, total)
|
|
}
|
|
|
|
func (h *Handler) GetMaterial(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid material id")
|
|
return
|
|
}
|
|
|
|
material, err := h.materialSvc.GetByID(id)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
response.NotFound(c, "Material not found")
|
|
return
|
|
}
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
response.Success(c, material)
|
|
}
|
|
|
|
func (h *Handler) CreateMaterial(c *gin.Context) {
|
|
var req model.CreateMaterialRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
operator := middleware.GetCurrentUsername(c)
|
|
if operator == "" {
|
|
operator = "系统"
|
|
}
|
|
|
|
material, err := h.materialSvc.Create(&req, operator)
|
|
if err != nil {
|
|
if err.Error() == "material code already exists" {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
response.Created(c, "Material created successfully", material)
|
|
}
|
|
|
|
func (h *Handler) UpdateMaterial(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid material id")
|
|
return
|
|
}
|
|
|
|
var req model.UpdateMaterialRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
operator := middleware.GetCurrentUsername(c)
|
|
if operator == "" {
|
|
operator = "系统"
|
|
}
|
|
|
|
material, err := h.materialSvc.Update(id, &req, operator)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
response.NotFound(c, "Material not found")
|
|
return
|
|
}
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
response.SuccessWithMessage(c, "Material updated successfully", material)
|
|
}
|
|
|
|
func (h *Handler) DeleteMaterial(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid material id")
|
|
return
|
|
}
|
|
|
|
operator := middleware.GetCurrentUsername(c)
|
|
if operator == "" {
|
|
operator = "系统"
|
|
}
|
|
|
|
err = h.materialSvc.Delete(id, operator)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
response.SuccessWithMessage(c, "Material deleted successfully", nil)
|
|
}
|
|
|
|
// Inbound handlers
|
|
|
|
func (h *Handler) ListInbound(c *gin.Context) {
|
|
search := c.Query("search")
|
|
|
|
limit := 20
|
|
offset := 0
|
|
if l := c.Query("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
}
|
|
}
|
|
if o := c.Query("offset"); o != "" {
|
|
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
|
|
orders, total, err := h.inboundSvc.List(search, limit, offset)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
if orders == nil {
|
|
orders = []model.InboundOrder{}
|
|
}
|
|
response.PaginatedSuccess(c, orders, limit, offset, total)
|
|
}
|
|
|
|
func (h *Handler) GetInbound(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid inbound order id")
|
|
return
|
|
}
|
|
|
|
order, err := h.inboundSvc.GetByID(id)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
response.NotFound(c, "Inbound order not found")
|
|
return
|
|
}
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
response.Success(c, order)
|
|
}
|
|
|
|
func (h *Handler) CreateInbound(c *gin.Context) {
|
|
var req model.CreateInboundRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
order, err := h.inboundSvc.Create(&req)
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrMaterialNotFound) {
|
|
response.BadRequest(c, "material not found")
|
|
return
|
|
}
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
// Broadcast stock update to all connected clients
|
|
GetHub().BroadcastStockUpdate("inbound", order.ID, order)
|
|
response.Created(c, "Inbound order created successfully", order)
|
|
}
|
|
|
|
func (h *Handler) DeleteInbound(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid inbound order id")
|
|
return
|
|
}
|
|
|
|
err = h.inboundSvc.Delete(id)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
response.SuccessWithMessage(c, "Inbound order deleted successfully", nil)
|
|
}
|
|
|
|
func (h *Handler) UpdateInbound(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid inbound order id")
|
|
return
|
|
}
|
|
|
|
var req model.UpdateInboundRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
order, err := h.inboundSvc.Update(id, &req)
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrMaterialNotFound) {
|
|
response.BadRequest(c, "material not found")
|
|
return
|
|
}
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
GetHub().BroadcastStockUpdate("inbound", order.ID, order)
|
|
response.Success(c, order)
|
|
}
|
|
|
|
func (h *Handler) GetInboundByMaterial(c *gin.Context) {
|
|
materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid material id")
|
|
return
|
|
}
|
|
|
|
records, err := h.inboundSvc.GetByMaterialID(materialID)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
if records == nil {
|
|
records = []model.InboundRecord{}
|
|
}
|
|
response.Success(c, records)
|
|
}
|
|
|
|
// Outbound handlers
|
|
|
|
func (h *Handler) ListOutbound(c *gin.Context) {
|
|
search := c.Query("search")
|
|
|
|
limit := 20
|
|
offset := 0
|
|
if l := c.Query("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
}
|
|
}
|
|
if o := c.Query("offset"); o != "" {
|
|
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
|
|
orders, total, err := h.outboundSvc.List(search, limit, offset)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
if orders == nil {
|
|
orders = []model.OutboundOrder{}
|
|
}
|
|
response.PaginatedSuccess(c, orders, limit, offset, total)
|
|
}
|
|
|
|
func (h *Handler) GetOutbound(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid outbound order id")
|
|
return
|
|
}
|
|
|
|
order, err := h.outboundSvc.GetByID(id)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
response.NotFound(c, "Outbound order not found")
|
|
return
|
|
}
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
response.Success(c, order)
|
|
}
|
|
|
|
func (h *Handler) CreateOutbound(c *gin.Context) {
|
|
var req model.CreateOutboundRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
order, err := h.outboundSvc.Create(&req)
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrMaterialNotFound) {
|
|
response.BadRequest(c, "material not found")
|
|
return
|
|
}
|
|
if errors.Is(err, service.ErrInsufficientStock) {
|
|
response.BadRequest(c, "insufficient stock")
|
|
return
|
|
}
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
// Broadcast stock update to all connected clients
|
|
GetHub().BroadcastStockUpdate("outbound", order.ID, order)
|
|
response.Created(c, "Outbound order created successfully", order)
|
|
}
|
|
|
|
func (h *Handler) DeleteOutbound(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid outbound order id")
|
|
return
|
|
}
|
|
|
|
err = h.outboundSvc.Delete(id)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
response.SuccessWithMessage(c, "Outbound order deleted successfully", nil)
|
|
}
|
|
|
|
func (h *Handler) UpdateOutbound(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid outbound order id")
|
|
return
|
|
}
|
|
|
|
var req model.UpdateOutboundRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
order, err := h.outboundSvc.Update(id, &req)
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrMaterialNotFound) {
|
|
response.BadRequest(c, "material not found")
|
|
return
|
|
}
|
|
if errors.Is(err, service.ErrInsufficientStock) {
|
|
response.BadRequest(c, "insufficient stock")
|
|
return
|
|
}
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
GetHub().BroadcastStockUpdate("outbound", order.ID, order)
|
|
response.Success(c, order)
|
|
}
|
|
|
|
func (h *Handler) GetOutboundByMaterial(c *gin.Context) {
|
|
materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid material id")
|
|
return
|
|
}
|
|
|
|
records, err := h.outboundSvc.GetByMaterialID(materialID)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
if records == nil {
|
|
records = []model.OutboundRecord{}
|
|
}
|
|
response.Success(c, records)
|
|
}
|
|
|
|
// Stock handlers
|
|
|
|
func (h *Handler) ListStock(c *gin.Context) {
|
|
limit := 20
|
|
offset := 0
|
|
if l := c.Query("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
}
|
|
}
|
|
if o := c.Query("offset"); o != "" {
|
|
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
|
|
search := c.Query("search")
|
|
category := c.Query("category")
|
|
|
|
stocks, total, err := h.stockSvc.GetAllStock(search, category, limit, offset)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
if stocks == nil {
|
|
stocks = []model.Stock{}
|
|
}
|
|
response.PaginatedSuccess(c, stocks, limit, offset, total)
|
|
}
|
|
|
|
func (h *Handler) GetStock(c *gin.Context) {
|
|
materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid material id")
|
|
return
|
|
}
|
|
|
|
stock, err := h.stockSvc.GetByMaterialID(materialID)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
response.NotFound(c, "Material not found")
|
|
return
|
|
}
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
response.Success(c, stock)
|
|
}
|
|
|
|
func (h *Handler) GetWarningStock(c *gin.Context) {
|
|
limit := 20
|
|
offset := 0
|
|
if l := c.Query("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
}
|
|
}
|
|
if o := c.Query("offset"); o != "" {
|
|
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
|
|
stocks, total, err := h.stockSvc.GetWarningStock(limit, offset)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
if stocks == nil {
|
|
stocks = []model.Stock{}
|
|
}
|
|
response.PaginatedSuccess(c, stocks, limit, offset, total)
|
|
}
|
|
|
|
func (h *Handler) GetStockLogs(c *gin.Context) {
|
|
materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid material id")
|
|
return
|
|
}
|
|
|
|
exists, err := h.stockSvc.MaterialExists(materialID)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
if !exists {
|
|
response.NotFound(c, "Material not found")
|
|
return
|
|
}
|
|
|
|
limit := 20
|
|
offset := 0
|
|
if l := c.Query("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
}
|
|
}
|
|
if o := c.Query("offset"); o != "" {
|
|
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
|
|
logs, total, err := h.stockSvc.GetLogsByMaterialID(materialID, limit, offset)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
if logs == nil {
|
|
logs = []model.StockLogResponse{}
|
|
}
|
|
response.PaginatedSuccess(c, logs, limit, offset, total)
|
|
}
|
|
|
|
func (h *Handler) GetAllStockLogs(c *gin.Context) {
|
|
limit := 20
|
|
offset := 0
|
|
if l := c.Query("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
}
|
|
}
|
|
if o := c.Query("offset"); o != "" {
|
|
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
|
|
logs, total, err := h.stockSvc.GetAllLogs(limit, offset)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
if logs == nil {
|
|
logs = []model.StockLogResponse{}
|
|
}
|
|
response.PaginatedSuccess(c, logs, limit, offset, total)
|
|
}
|
|
|
|
// Material Log handlers
|
|
|
|
func (h *Handler) ListLogs(c *gin.Context) {
|
|
operationType := c.Query("operation_type")
|
|
materialId := c.Query("material_id")
|
|
startDate := c.Query("start_date")
|
|
endDate := c.Query("end_date")
|
|
|
|
limit := 20
|
|
offset := 0
|
|
if l := c.Query("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
}
|
|
}
|
|
if o := c.Query("offset"); o != "" {
|
|
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
|
|
logs, total, err := h.logSvc.List(operationType, materialId, startDate, endDate, limit, offset)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
if logs == nil {
|
|
logs = []model.MaterialLog{}
|
|
}
|
|
response.PaginatedSuccess(c, logs, limit, offset, total)
|
|
}
|
|
|
|
func (h *Handler) GetLogsByMaterial(c *gin.Context) {
|
|
materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid material id")
|
|
return
|
|
}
|
|
|
|
logs, err := h.logSvc.GetByMaterialID(materialID)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
if logs == nil {
|
|
logs = []model.MaterialLog{}
|
|
}
|
|
response.Success(c, logs)
|
|
}
|
|
|
|
// Auth handlers
|
|
|
|
func (h *Handler) Login(c *gin.Context) {
|
|
ip := c.ClientIP()
|
|
|
|
// Check if IP is rate limited before processing
|
|
if middleware.IsRateLimited(ip) {
|
|
response.Error(c, 429, "too many login attempts, please try again later")
|
|
return
|
|
}
|
|
|
|
var req model.LoginRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
result, err := h.authSvc.Login(req.Username, req.Password)
|
|
if err != nil {
|
|
// Record failed attempt
|
|
middleware.RecordFailedAttempt(ip)
|
|
response.Unauthorized(c, "invalid username or password")
|
|
return
|
|
}
|
|
|
|
// Reset attempts on successful login
|
|
middleware.ResetAttempts(ip)
|
|
response.Success(c, result)
|
|
}
|
|
|
|
func (h *Handler) Logout(c *gin.Context) {
|
|
// For simple token auth, logout is handled client-side by removing the token
|
|
response.SuccessWithMessage(c, "logout successful", nil)
|
|
}
|
|
|
|
func (h *Handler) GetCurrentUser(c *gin.Context) {
|
|
userID := middleware.GetCurrentUserID(c)
|
|
username := middleware.GetCurrentUsername(c)
|
|
role := middleware.GetCurrentRole(c)
|
|
|
|
user, err := h.authSvc.GetUserByID(userID)
|
|
if err != nil {
|
|
response.NotFound(c, "user not found")
|
|
return
|
|
}
|
|
|
|
// Return user info (password already hidden with json:"-" tag)
|
|
response.Success(c, gin.H{
|
|
"id": user.ID,
|
|
"username": username,
|
|
"role": role,
|
|
})
|
|
}
|
|
|
|
// User management handlers (Admin only)
|
|
|
|
func (h *Handler) ListUsers(c *gin.Context) {
|
|
limit := 20
|
|
offset := 0
|
|
if l := c.Query("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
}
|
|
}
|
|
if o := c.Query("offset"); o != "" {
|
|
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
search := c.Query("search")
|
|
|
|
users, total, err := h.authSvc.ListUsers(search, limit, offset)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
if users == nil {
|
|
users = []model.User{}
|
|
}
|
|
response.PaginatedSuccess(c, users, limit, offset, total)
|
|
}
|
|
|
|
func (h *Handler) CreateUser(c *gin.Context) {
|
|
var req struct {
|
|
Username string `json:"username" binding:"required"`
|
|
Password string `json:"password" binding:"required"`
|
|
Role string `json:"role" binding:"required"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
if req.Role != "admin" && req.Role != "user" {
|
|
response.BadRequest(c, "invalid role, must be admin or user")
|
|
return
|
|
}
|
|
|
|
user, err := h.authSvc.CreateUser(req.Username, req.Password, req.Role, req.Description)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
response.Created(c, "user created successfully", user)
|
|
}
|
|
|
|
func (h *Handler) UpdateUser(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid user id")
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
Role string `json:"role"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
if req.Role != "admin" && req.Role != "user" {
|
|
response.BadRequest(c, "invalid role, must be admin or user")
|
|
return
|
|
}
|
|
|
|
err = h.authSvc.UpdateUser(id, req.Username, req.Role, req.Description, req.Password)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
response.SuccessWithMessage(c, "user updated successfully", nil)
|
|
}
|
|
|
|
func (h *Handler) DeleteUser(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid user id")
|
|
return
|
|
}
|
|
|
|
// Prevent deleting yourself
|
|
currentUserID := middleware.GetCurrentUserID(c)
|
|
if currentUserID == id {
|
|
response.BadRequest(c, "cannot delete yourself")
|
|
return
|
|
}
|
|
|
|
if err := h.authSvc.DeleteUser(id); err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
response.SuccessWithMessage(c, "user deleted successfully", nil)
|
|
}
|
|
|
|
// Application handlers
|
|
|
|
func (h *Handler) CreateApplication(c *gin.Context) {
|
|
userID := middleware.GetCurrentUserID(c)
|
|
|
|
var req model.CreateApplicationRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
app, err := h.appSvc.Create(userID, &req)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
response.Created(c, "application created successfully", app)
|
|
}
|
|
|
|
func (h *Handler) ListMyApplications(c *gin.Context) {
|
|
userID := middleware.GetCurrentUserID(c)
|
|
status := c.Query("status")
|
|
|
|
limit := 20
|
|
offset := 0
|
|
if l := c.Query("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
}
|
|
}
|
|
if o := c.Query("offset"); o != "" {
|
|
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
|
|
apps, total, err := h.appSvc.ListByUser(userID, status, limit, offset)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
response.PaginatedSuccess(c, apps, limit, offset, int64(total))
|
|
}
|
|
|
|
func (h *Handler) ListApplications(c *gin.Context) {
|
|
// Check if admin
|
|
role := middleware.GetCurrentRole(c)
|
|
if role != "admin" {
|
|
response.Unauthorized(c, "admin only")
|
|
return
|
|
}
|
|
|
|
status := c.Query("status")
|
|
|
|
limit := 20
|
|
offset := 0
|
|
if l := c.Query("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
}
|
|
}
|
|
if o := c.Query("offset"); o != "" {
|
|
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
|
|
apps, total, err := h.appSvc.ListAll(status, limit, offset)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
response.PaginatedSuccess(c, apps, limit, offset, int64(total))
|
|
}
|
|
|
|
func (h *Handler) CancelApplication(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid application id")
|
|
return
|
|
}
|
|
|
|
userID := middleware.GetCurrentUserID(c)
|
|
if err := h.appSvc.Cancel(id, userID); err != nil {
|
|
if err.Error() == "cannot cancel others' applications" || err.Error() == "cannot cancel application" {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
response.SuccessWithMessage(c, "application cancelled successfully", nil)
|
|
}
|
|
|
|
func (h *Handler) ApproveApplication(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid application id")
|
|
return
|
|
}
|
|
|
|
// Check if admin
|
|
role := middleware.GetCurrentRole(c)
|
|
if role != "admin" {
|
|
response.Unauthorized(c, "admin only")
|
|
return
|
|
}
|
|
|
|
var req model.ApproveApplicationRequest
|
|
c.ShouldBindJSON(&req) // optional
|
|
|
|
approverID := middleware.GetCurrentUserID(c)
|
|
if err := h.appSvc.Approve(id, approverID, req.AutoOutbound); err != nil {
|
|
if err == service.ErrApplicationNotFound {
|
|
response.NotFound(c, "application not found")
|
|
return
|
|
}
|
|
if err == service.ErrApplicationNotPending {
|
|
response.BadRequest(c, "application is not pending")
|
|
return
|
|
}
|
|
if err == service.ErrInsufficientStock {
|
|
response.BadRequest(c, "insufficient stock")
|
|
return
|
|
}
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
response.SuccessWithMessage(c, "application approved successfully", nil)
|
|
}
|
|
|
|
func (h *Handler) RejectApplication(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "invalid application id")
|
|
return
|
|
}
|
|
|
|
// Check if admin
|
|
role := middleware.GetCurrentRole(c)
|
|
if role != "admin" {
|
|
response.Unauthorized(c, "admin only")
|
|
return
|
|
}
|
|
|
|
var req model.RejectApplicationRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "rejection reason is required")
|
|
return
|
|
}
|
|
|
|
if err := h.appSvc.Reject(id, req.Reason); err != nil {
|
|
if err == service.ErrApplicationNotFound {
|
|
response.NotFound(c, "application not found")
|
|
return
|
|
}
|
|
if err == service.ErrApplicationNotPending {
|
|
response.BadRequest(c, "application is not pending")
|
|
return
|
|
}
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
response.SuccessWithMessage(c, "application rejected successfully", nil)
|
|
}
|