Add structured logging and centralized error handling
- Initialize slog in the serve command with terminal/file support - Introduce `AppError` with HTTP status for unified service-layer errors - Replace ad-hoc `api.Error` calls with `api.RespondError` - Wrap internal errors with `model.NewInternalError` and add reference IDs - Add `RequestID` middleware and switch router from `gin.Default` to `gin.New`
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -37,7 +38,7 @@ func (h *AccountHandler) ListPasskeys(c *gin.Context) {
|
||||
|
||||
creds, err := h.authService.ListPasskeys(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, err.Error())
|
||||
api.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -54,13 +55,14 @@ func (h *AccountHandler) CreatePasskey(c *gin.Context) {
|
||||
|
||||
var req createPasskeyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "invalid request: "+err.Error())
|
||||
slog.DebugContext(c.Request.Context(), "create passkey bind failed", "error", err)
|
||||
api.Error(c, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
pk, err := h.authService.CreatePasskey(c.Request.Context(), userID, req.Label)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, err.Error())
|
||||
api.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -78,11 +80,7 @@ func (h *AccountHandler) RevokePasskey(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.authService.RevokePasskey(c.Request.Context(), userID, passkeyID); err != nil {
|
||||
if err == model.ErrForbidden {
|
||||
api.Error(c, http.StatusForbidden, err.Error())
|
||||
return
|
||||
}
|
||||
api.Error(c, http.StatusInternalServerError, err.Error())
|
||||
api.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -38,13 +39,14 @@ type tokenRequest struct {
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
var req registerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "invalid request: "+err.Error())
|
||||
slog.DebugContext(c.Request.Context(), "register bind failed", "error", err)
|
||||
api.Error(c, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.authService.Register(c.Request.Context(), req.Username, req.Email, req.Password)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusConflict, err.Error())
|
||||
api.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -55,13 +57,14 @@ func (h *AuthHandler) Register(c *gin.Context) {
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req loginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "invalid request: "+err.Error())
|
||||
slog.DebugContext(c.Request.Context(), "login bind failed", "error", err)
|
||||
api.Error(c, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
pair, err := h.authService.Login(c.Request.Context(), req.Email, req.Password)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusUnauthorized, err.Error())
|
||||
api.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -72,13 +75,14 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
func (h *AuthHandler) Refresh(c *gin.Context) {
|
||||
var req tokenRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "invalid request: "+err.Error())
|
||||
slog.DebugContext(c.Request.Context(), "refresh bind failed", "error", err)
|
||||
api.Error(c, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
pair, err := h.authService.Refresh(c.Request.Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusUnauthorized, err.Error())
|
||||
api.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,12 +93,13 @@ func (h *AuthHandler) Refresh(c *gin.Context) {
|
||||
func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
var req tokenRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "invalid request: "+err.Error())
|
||||
slog.DebugContext(c.Request.Context(), "logout bind failed", "error", err)
|
||||
api.Error(c, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.authService.Logout(c.Request.Context(), req.RefreshToken); err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, err.Error())
|
||||
api.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+17
-27
@@ -1,9 +1,9 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -49,13 +49,14 @@ func (h *FileHandler) Upload(c *gin.Context) {
|
||||
if mediaType == "application/json" {
|
||||
var req createDirRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "invalid request: "+err.Error())
|
||||
slog.DebugContext(c.Request.Context(), "create dir bind failed", "error", err)
|
||||
api.Error(c, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
dir, err := h.fileService.CreateDir(c.Request.Context(), userID, req.ParentID, req.Name)
|
||||
if err != nil {
|
||||
api.Error(c, mapError(err), err.Error())
|
||||
api.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -65,7 +66,8 @@ func (h *FileHandler) Upload(c *gin.Context) {
|
||||
|
||||
// File upload via multipart.
|
||||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "invalid multipart form: "+err.Error())
|
||||
slog.DebugContext(c.Request.Context(), "parse multipart form failed", "error", err)
|
||||
api.Error(c, http.StatusBadRequest, "invalid multipart form")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -77,20 +79,21 @@ func (h *FileHandler) Upload(c *gin.Context) {
|
||||
|
||||
header, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "missing file field: "+err.Error())
|
||||
slog.DebugContext(c.Request.Context(), "form file missing", "error", err)
|
||||
api.Error(c, http.StatusBadRequest, "missing file field")
|
||||
return
|
||||
}
|
||||
|
||||
file, err := header.Open()
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "open uploaded file: "+err.Error())
|
||||
api.RespondError(c, model.NewInternalError("open uploaded file", err))
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
info, err := h.fileService.Upload(c.Request.Context(), userID, parentID, header.Filename, file)
|
||||
if err != nil {
|
||||
api.Error(c, mapError(err), err.Error())
|
||||
api.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -124,7 +127,7 @@ func (h *FileHandler) List(c *gin.Context) {
|
||||
|
||||
list, err := h.fileService.List(c.Request.Context(), userID, parentID, offset, limit)
|
||||
if err != nil {
|
||||
api.Error(c, mapError(err), err.Error())
|
||||
api.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -138,7 +141,7 @@ func (h *FileHandler) Get(c *gin.Context) {
|
||||
|
||||
info, err := h.fileService.Get(c.Request.Context(), userID, fileID)
|
||||
if err != nil {
|
||||
api.Error(c, mapError(err), err.Error())
|
||||
api.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -152,7 +155,7 @@ func (h *FileHandler) Download(c *gin.Context) {
|
||||
|
||||
reader, info, err := h.fileService.Download(c.Request.Context(), userID, fileID)
|
||||
if err != nil {
|
||||
api.Error(c, mapError(err), err.Error())
|
||||
api.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
@@ -177,7 +180,8 @@ func (h *FileHandler) Update(c *gin.Context) {
|
||||
|
||||
var req updateFileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "invalid request: "+err.Error())
|
||||
slog.DebugContext(c.Request.Context(), "update file bind failed", "error", err)
|
||||
api.Error(c, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -189,7 +193,7 @@ func (h *FileHandler) Update(c *gin.Context) {
|
||||
|
||||
info, err := h.fileService.Update(c.Request.Context(), userID, fileID, req.Name, req.ParentID)
|
||||
if err != nil {
|
||||
api.Error(c, mapError(err), err.Error())
|
||||
api.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -202,23 +206,9 @@ func (h *FileHandler) Delete(c *gin.Context) {
|
||||
fileID := c.Param("id")
|
||||
|
||||
if err := h.fileService.Delete(c.Request.Context(), userID, fileID); err != nil {
|
||||
api.Error(c, mapError(err), err.Error())
|
||||
api.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// mapError maps sentinel errors to HTTP status codes.
|
||||
func mapError(err error) int {
|
||||
switch {
|
||||
case errors.Is(err, model.ErrNotFound):
|
||||
return http.StatusNotFound
|
||||
case errors.Is(err, model.ErrForbidden):
|
||||
return http.StatusForbidden
|
||||
case errors.Is(err, model.ErrDuplicate):
|
||||
return http.StatusConflict
|
||||
default:
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func setupFileHandler(t *testing.T) *FileHandler {
|
||||
|
||||
fileRepo := repository.NewFileRepository(db)
|
||||
store := newInMemStore()
|
||||
fileService := service.NewFileService(fileRepo, store, 0)
|
||||
fileService := service.NewFileService(fileRepo, store, 0, nil)
|
||||
|
||||
return NewFileHandler(fileService)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user