fix: error handling, defer cleanup, graceful shutdown, golangci-lint setup

This commit is contained in:
2026-04-18 17:32:34 +03:00
parent 44cb3c6576
commit ebb8cded41
16 changed files with 174 additions and 47 deletions

View File

@@ -13,6 +13,7 @@ import (
"github.com/google/uuid"
"github.com/gosimple/slug"
"github.com/huandu/go-sqlbuilder"
"github.com/jackc/pgx/v5"
"golang.org/x/crypto/bcrypt"
)
@@ -34,7 +35,12 @@ func (s *Server) ListDrivers(w http.ResponseWriter, r *http.Request, params api.
func (s *Server) CreateDriver(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims := ctx.Value("user").(*Claims)
claims, ok := ctx.Value(UserKey).(*Claims)
if !ok {
slog.ErrorContext(ctx, "Error while casting claims")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
if claims.Role != "admin" {
slog.WarnContext(ctx, "unusual try from not allowed role", slog.String("Role", claims.Role), slog.String("id", claims.ID.String()))
s.JSON(w, r, http.StatusForbidden, MsgForbidden, RespError)
@@ -72,7 +78,11 @@ func (s *Server) CreateDriver(w http.ResponseWriter, r *http.Request) {
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) {
slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error()))
}
}()
if err := storage.Create(ctx, "users", user, tx); err != nil {
slog.ErrorContext(ctx, "Unable to create user", slog.String("error", err.Error()), slog.Any("user", user))
@@ -111,9 +121,9 @@ func (s *Server) CreateDriver(w http.ResponseWriter, r *http.Request) {
func (s *Server) UpdateMyDriverStatus(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, ok := ctx.Value("user").(*Claims)
claims, ok := ctx.Value(UserKey).(*Claims)
if !ok || claims == nil {
slog.ErrorContext(ctx, "Unable to convert claims", slog.Any("claims", ctx.Value("user")))
slog.ErrorContext(ctx, "Unable to convert claims", slog.Any("claims", ctx.Value(UserKey)))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}

View File

@@ -2,6 +2,7 @@ package handler
import (
"context"
"log/slog"
"sync"
"github.com/google/uuid"
@@ -43,7 +44,9 @@ func (h *Hub) Broadcast(orderID uuid.UUID, msg any) {
h.mu.RLock()
defer h.mu.RUnlock()
for _, conn := range h.connections[orderID] {
conn.WriteJSON(msg)
if err := conn.WriteJSON(msg); err != nil {
slog.Error("error while writing json to clients", slog.String("error", err.Error()))
}
}
}

View File

@@ -12,6 +12,7 @@ import (
storage "github.com/anxi0uz/logiflow/pkg"
"github.com/google/uuid"
"github.com/huandu/go-sqlbuilder"
"github.com/jackc/pgx/v5"
"golang.org/x/crypto/bcrypt"
)
@@ -65,12 +66,18 @@ func (s *Server) CreateManager(w http.ResponseWriter, r *http.Request) {
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) {
slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error()))
}
}()
if err := storage.Create(ctx, "users", userModel, tx); err != nil {
slog.ErrorContext(ctx, "Error while creating user", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
tx.Rollback(ctx)
if err := tx.Rollback(ctx); err != nil {
slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error()))
}
return
}
@@ -84,7 +91,9 @@ func (s *Server) CreateManager(w http.ResponseWriter, r *http.Request) {
if err := storage.Create(ctx, "managers", managerModel, tx); err != nil {
slog.ErrorContext(ctx, "Error while creating manager", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
tx.Rollback(ctx)
if err := tx.Rollback(ctx); err != nil {
slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error()))
}
return
}

View File

@@ -13,7 +13,7 @@ import (
func (s *Server) ListNotifications(w http.ResponseWriter, r *http.Request, params api.ListNotificationsParams) {
ctx := r.Context()
claims, ok := ctx.Value("user").(*Claims)
claims, ok := ctx.Value(UserKey).(*Claims)
if !ok {
slog.ErrorContext(ctx, "Error while casting claims")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
@@ -36,7 +36,7 @@ func (s *Server) ListNotifications(w http.ResponseWriter, r *http.Request, param
func (s *Server) MarkNotificationRead(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {
ctx := r.Context()
claims, ok := ctx.Value("user").(*Claims)
claims, ok := ctx.Value(UserKey).(*Claims)
if !ok {
slog.ErrorContext(ctx, "Error while casting claims")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
@@ -51,6 +51,10 @@ func (s *Server) MarkNotificationRead(w http.ResponseWriter, r *http.Request, id
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
if notification == nil {
s.JSON(w, r, http.StatusNotFound, MsgNotFound, RespNotFound)
return
}
if notification.UserID != claims.ID {
s.JSON(w, r, http.StatusForbidden, MsgForbidden, RespError)
return

View File

@@ -15,7 +15,7 @@ import (
func (s *Server) ListOrders(w http.ResponseWriter, r *http.Request, params api.ListOrdersParams) {
ctx := r.Context()
claims, ok := ctx.Value("user").(*Claims)
claims, ok := ctx.Value(UserKey).(*Claims)
if !ok {
slog.ErrorContext(ctx, "Error while casting claims")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
@@ -32,7 +32,7 @@ func (s *Server) ListOrders(w http.ResponseWriter, r *http.Request, params api.L
func (s *Server) CreateOrder(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, ok := ctx.Value("user").(*Claims)
claims, ok := ctx.Value(UserKey).(*Claims)
if !ok {
slog.ErrorContext(ctx, "error while casting claims")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
@@ -60,7 +60,7 @@ func (s *Server) CreateOrder(w http.ResponseWriter, r *http.Request) {
func (s *Server) GetOrder(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {
ctx := r.Context()
claims, ok := ctx.Value("user").(*Claims)
claims, ok := ctx.Value(UserKey).(*Claims)
if !ok {
slog.ErrorContext(ctx, "Error while casting claims")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
@@ -86,7 +86,7 @@ func (s *Server) GetOrder(w http.ResponseWriter, r *http.Request, id openapi_typ
func (s *Server) CancelOrder(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {
ctx := r.Context()
claims, ok := ctx.Value("user").(*Claims)
claims, ok := ctx.Value(UserKey).(*Claims)
if !ok {
slog.ErrorContext(ctx, "Error while casting claims")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
@@ -111,7 +111,7 @@ func (s *Server) CancelOrder(w http.ResponseWriter, r *http.Request, id openapi_
func (s *Server) UpdateOrderStatus(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) {
ctx := r.Context()
claims, ok := ctx.Value("user").(*Claims)
claims, ok := ctx.Value(UserKey).(*Claims)
if !ok {
slog.ErrorContext(ctx, "Error while casting claims")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
@@ -123,6 +123,7 @@ func (s *Server) UpdateOrderStatus(w http.ResponseWriter, r *http.Request, id op
s.JSON(w, r, http.StatusBadRequest, MsgInvalidBody, RespError)
return
}
order, err := s.OrderSerice.UpdateOrderStatus(ctx, id, claims.ID, claims.Role, req)
if err != nil {
switch {
@@ -136,15 +137,17 @@ func (s *Server) UpdateOrderStatus(w http.ResponseWriter, r *http.Request, id op
}
return
}
if req.Status == api.OrderStatusUpdateStatusInTransit {
go s.startRouteTracker(id)
}
s.JSON(w, r, http.StatusOK, order, RespSuccess)
}
func (s *Server) GetOrdersReport(w http.ResponseWriter, r *http.Request, params api.GetOrdersReportParams) {
ctx := r.Context()
claims, ok := ctx.Value("user").(*Claims)
claims, ok := ctx.Value(UserKey).(*Claims)
if !ok {
slog.ErrorContext(ctx, "Error while casting claims")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
@@ -164,12 +167,12 @@ func (s *Server) GetOrdersReport(w http.ResponseWriter, r *http.Request, params
f := excelize.NewFile()
sheet := "Orders"
f.SetSheetName("Sheet1", sheet)
f.SetSheetName("Sheet1", sheet) //nolint:errcheck
headers := []string{"ID", "Status", "Origin", "Destination", "Weight", "Volume", "Price", "Created At"}
for i, h := range headers {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
f.SetCellValue(sheet, cell, h)
f.SetCellValue(sheet, cell, h) //nolint:errcheck
}
for row, o := range orders {
@@ -185,7 +188,7 @@ func (s *Server) GetOrdersReport(w http.ResponseWriter, r *http.Request, params
}
for col, v := range values {
cell, _ := excelize.CoordinatesToCellName(col+1, row+2)
f.SetCellValue(sheet, cell, v)
f.SetCellValue(sheet, cell, v) //nolint:errcheck
}
}
buf, err := f.WriteToBuffer()
@@ -198,12 +201,12 @@ func (s *Server) GetOrdersReport(w http.ResponseWriter, r *http.Request, params
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
w.Header().Set("Content-Disposition", "attachment; filename=orders_report.xlsx")
w.WriteHeader(http.StatusOK)
w.Write(buf.Bytes())
w.Write(buf.Bytes()) //nolint:errcheck
}
func (s *Server) GetDashboard(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, ok := ctx.Value("user").(*Claims)
claims, ok := ctx.Value(UserKey).(*Claims)
if !ok {
slog.ErrorContext(ctx, "error while casting claims")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)

View File

@@ -39,7 +39,11 @@ func (s *Server) RouteWebSocket(w http.ResponseWriter, r *http.Request, id opena
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
defer conn.Close()
defer func() {
if err := conn.Close(); err != nil {
slog.Warn("ws conn close error", slog.String("error", err.Error()))
}
}()
orderID := id
s.Hub.Register(id, conn)
@@ -58,10 +62,12 @@ func (s *Server) RouteWebSocket(w http.ResponseWriter, r *http.Request, id opena
if err == nil {
coords, err := route.ParseCoordinates()
if err == nil && route.CurrentIndex < len(coords) {
conn.WriteJSON(map[string]any{
if err := conn.WriteJSON(map[string]any{
"current_index": route.CurrentIndex,
"coordinate": coords[route.CurrentIndex],
})
}); err != nil {
slog.ErrorContext(ctx, "error while writing date to clients", slog.String("error", err.Error()))
}
}
}
@@ -102,9 +108,11 @@ func (s *Server) startRouteTracker(orderID uuid.UUID) {
"coordinate": coords[route.CurrentIndex],
})
route.CurrentIndex++
storage.Update(ctx, "routes", *route, s.DB, func(sb *sqlbuilder.UpdateBuilder) {
if err := storage.Update(ctx, "routes", *route, s.DB, func(sb *sqlbuilder.UpdateBuilder) {
sb.Where(sb.EQ("order_id", orderID))
})
}); err != nil {
slog.ErrorContext(ctx, "error while updating route", slog.String("id", route.ID.String()), slog.String("error", err.Error()))
}
}
}
})

View File

@@ -28,6 +28,7 @@ type ctxKey string
const (
requestIDKey ctxKey = "X-Request-ID"
tokenKey ctxKey = "Authorization"
UserKey ctxKey = "user"
// response messages
MsgInternalError = "Internal server error"
@@ -145,7 +146,7 @@ func (s *Server) AuthMiddleware(next http.Handler) http.Handler {
return
}
ctx := context.WithValue(r.Context(), "user", claims)
ctx := context.WithValue(r.Context(), UserKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/google/uuid"
"github.com/gosimple/slug"
"github.com/huandu/go-sqlbuilder"
"github.com/jackc/pgx/v5"
"github.com/redis/go-redis/v9"
"golang.org/x/crypto/bcrypt"
)
@@ -54,9 +55,13 @@ func (s *Server) AuthLogin(w http.ResponseWriter, r *http.Request) {
now := time.Now()
user.LastLoginAt = &now
user.UpdatedAt = now
err = storage.Update(ctx, "users", user, s.DB, func(sb *sqlbuilder.UpdateBuilder) {
if err := storage.Update(ctx, "users", user, s.DB, func(sb *sqlbuilder.UpdateBuilder) {
sb.Where(sb.Equal("id", user.ID))
})
}); err != nil {
slog.ErrorContext(ctx, "Failed to update user", slog.String("error", err.Error()))
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
s.issueTokens(w, r, user)
}
@@ -93,6 +98,8 @@ func (s *Server) AuthLogout(w http.ResponseWriter, r *http.Request) {
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
s.deleteRefreshCookie(w)
s.JSON(w, r, http.StatusOK, "Logged out", RespSuccess)
}
func (s *Server) AuthRefresh(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -113,7 +120,7 @@ func (s *Server) AuthRefresh(w http.ResponseWriter, r *http.Request) {
return
}
claimsValue := ctx.Value("user")
claimsValue := ctx.Value(UserKey)
claims, ok := claimsValue.(*Claims)
@@ -181,7 +188,7 @@ func (s *Server) AuthRegister(w http.ResponseWriter, r *http.Request) {
func (s *Server) DeleteMe(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claimsValue := ctx.Value("user")
claimsValue := ctx.Value(UserKey)
claims, ok := claimsValue.(*Claims)
if !ok {
slog.ErrorContext(ctx, "Error while converting claims", slog.Any("claims", claimsValue))
@@ -195,7 +202,11 @@ func (s *Server) DeleteMe(w http.ResponseWriter, r *http.Request) {
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
return
}
defer tx.Rollback(ctx)
defer func() {
if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) {
slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error()))
}
}()
err = storage.Delete[models.User](ctx, "users", tx, func(sb *sqlbuilder.DeleteBuilder) {
sb.Where(sb.Equal("id", userID))
@@ -242,7 +253,7 @@ func (s *Server) DeleteMe(w http.ResponseWriter, r *http.Request) {
func (s *Server) GetMe(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claimsValue := ctx.Value("user")
claimsValue := ctx.Value(UserKey)
claims, ok := claimsValue.(*Claims)
if !ok {
slog.ErrorContext(ctx, "Error while converting claims", slog.Any("claims", claimsValue))
@@ -265,7 +276,7 @@ func (s *Server) GetMe(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) UpdateMe(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, ok := ctx.Value("user").(*Claims)
claims, ok := ctx.Value(UserKey).(*Claims)
if !ok {
slog.ErrorContext(ctx, "Error while casting claims")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)
@@ -325,7 +336,7 @@ func (s *Server) UpdateMe(w http.ResponseWriter, r *http.Request) {
func (s *Server) GetMyTrips(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, ok := ctx.Value("user").(*Claims)
claims, ok := ctx.Value(UserKey).(*Claims)
if !ok {
slog.ErrorContext(ctx, "Error while casting claims")
s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError)