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:
+30
-27
@@ -3,7 +3,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -59,12 +59,12 @@ func NewAuthService(
|
||||
// Register creates a new user account.
|
||||
func (s *AuthService) Register(ctx context.Context, username, email, password string) (*model.User, error) {
|
||||
if username == "" || email == "" || password == "" {
|
||||
return nil, fmt.Errorf("username, email, and password are required")
|
||||
return nil, &model.AppError{Status: http.StatusBadRequest, Message: "username, email, and password are required"}
|
||||
}
|
||||
|
||||
passwordHash, err := auth.HashPassword(password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hash password: %w", err)
|
||||
return nil, model.NewInternalError("hash password", err)
|
||||
}
|
||||
|
||||
user := &model.User{
|
||||
@@ -76,9 +76,9 @@ func (s *AuthService) Register(ctx context.Context, username, email, password st
|
||||
|
||||
if err := s.userRepo.Create(ctx, user); err != nil {
|
||||
if errors.Is(err, model.ErrDuplicate) {
|
||||
return nil, fmt.Errorf("username or email already exists")
|
||||
return nil, &model.AppError{Status: http.StatusConflict, Message: "username or email already exists"}
|
||||
}
|
||||
return nil, fmt.Errorf("create user: %w", err)
|
||||
return nil, model.NewInternalError("create user", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
@@ -89,13 +89,13 @@ func (s *AuthService) Login(ctx context.Context, email, password string) (*Token
|
||||
user, err := s.userRepo.FindByEmail(ctx, email)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil, fmt.Errorf("invalid email or password")
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid email or password"}
|
||||
}
|
||||
return nil, fmt.Errorf("find user: %w", err)
|
||||
return nil, model.NewInternalError("find user", err)
|
||||
}
|
||||
|
||||
if err := auth.VerifyPassword(user.PasswordHash, password); err != nil {
|
||||
return nil, fmt.Errorf("invalid email or password")
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid email or password"}
|
||||
}
|
||||
|
||||
return s.issueTokens(ctx, user.ID)
|
||||
@@ -106,28 +106,28 @@ func (s *AuthService) Login(ctx context.Context, email, password string) (*Token
|
||||
func (s *AuthService) Refresh(ctx context.Context, refreshTokenStr string) (*TokenPair, error) {
|
||||
claims, err := auth.ParseToken(refreshTokenStr, s.jwtSecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid token"}
|
||||
}
|
||||
|
||||
if claims.Type != auth.TokenRefresh {
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid token"}
|
||||
}
|
||||
|
||||
tokenHash := auth.HashToken(refreshTokenStr)
|
||||
session, err := s.sessionRepo.FindByTokenHash(ctx, tokenHash)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid token"}
|
||||
}
|
||||
return nil, fmt.Errorf("find session: %w", err)
|
||||
return nil, model.NewInternalError("find session", err)
|
||||
}
|
||||
|
||||
if session.UserID != claims.UserID {
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid token"}
|
||||
}
|
||||
|
||||
if err := s.sessionRepo.Delete(ctx, session.ID); err != nil {
|
||||
return nil, fmt.Errorf("delete old session: %w", err)
|
||||
return nil, model.NewInternalError("delete old session", err)
|
||||
}
|
||||
|
||||
return s.issueTokens(ctx, claims.UserID)
|
||||
@@ -141,7 +141,7 @@ func (s *AuthService) Logout(ctx context.Context, refreshTokenStr string) error
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("find session: %w", err)
|
||||
return model.NewInternalError("find session", err)
|
||||
}
|
||||
|
||||
return s.sessionRepo.Delete(ctx, session.ID)
|
||||
@@ -151,7 +151,7 @@ func (s *AuthService) Logout(ctx context.Context, refreshTokenStr string) error
|
||||
func (s *AuthService) CreatePasskey(ctx context.Context, userID, label string) (*CreatedPasskey, error) {
|
||||
raw, hash, err := auth.GenerateToken()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate token: %w", err)
|
||||
return nil, model.NewInternalError("generate token", err)
|
||||
}
|
||||
|
||||
cred := &model.Credential{
|
||||
@@ -163,7 +163,7 @@ func (s *AuthService) CreatePasskey(ctx context.Context, userID, label string) (
|
||||
}
|
||||
|
||||
if err := s.credentialRepo.Create(ctx, cred); err != nil {
|
||||
return nil, fmt.Errorf("create credential: %w", err)
|
||||
return nil, model.NewInternalError("create credential", err)
|
||||
}
|
||||
|
||||
return &CreatedPasskey{
|
||||
@@ -176,24 +176,24 @@ func (s *AuthService) CreatePasskey(ctx context.Context, userID, label string) (
|
||||
// LoginWithPasskey authenticates a user using an app passkey token.
|
||||
func (s *AuthService) LoginWithPasskey(ctx context.Context, tokenStr string) (*TokenPair, error) {
|
||||
if !strings.HasPrefix(tokenStr, "mygo_") {
|
||||
return nil, fmt.Errorf("invalid passkey format")
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid passkey format"}
|
||||
}
|
||||
|
||||
tokenHash := auth.HashToken(tokenStr)
|
||||
cred, err := s.credentialRepo.FindByHash(ctx, tokenHash)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil, fmt.Errorf("invalid passkey")
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid passkey"}
|
||||
}
|
||||
return nil, fmt.Errorf("find credential: %w", err)
|
||||
return nil, model.NewInternalError("find credential", err)
|
||||
}
|
||||
|
||||
if cred.Type != "app_passkey" {
|
||||
return nil, fmt.Errorf("invalid credential type")
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid credential type"}
|
||||
}
|
||||
|
||||
if err := s.credentialRepo.UpdateLastUsed(ctx, cred.ID); err != nil {
|
||||
return nil, fmt.Errorf("update last used: %w", err)
|
||||
return nil, model.NewInternalError("update last used", err)
|
||||
}
|
||||
|
||||
return s.issueTokens(ctx, cred.UserID)
|
||||
@@ -208,11 +208,14 @@ func (s *AuthService) ListPasskeys(ctx context.Context, userID string) ([]model.
|
||||
func (s *AuthService) RevokePasskey(ctx context.Context, userID, credID string) error {
|
||||
cred, err := s.credentialRepo.FindByID(ctx, credID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("find credential: %w", err)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return &model.AppError{Status: http.StatusNotFound, Message: "passkey not found", Err: model.ErrNotFound}
|
||||
}
|
||||
return model.NewInternalError("find credential", err)
|
||||
}
|
||||
|
||||
if cred.UserID != userID {
|
||||
return model.ErrForbidden
|
||||
return &model.AppError{Status: http.StatusForbidden, Message: "access denied", Err: model.ErrForbidden}
|
||||
}
|
||||
|
||||
return s.credentialRepo.Delete(ctx, credID)
|
||||
@@ -221,12 +224,12 @@ func (s *AuthService) RevokePasskey(ctx context.Context, userID, credID string)
|
||||
func (s *AuthService) issueTokens(ctx context.Context, userID string) (*TokenPair, error) {
|
||||
accessToken, err := auth.GenerateAccessToken(userID, s.jwtSecret, s.accessTTL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate access token: %w", err)
|
||||
return nil, model.NewInternalError("generate access token", err)
|
||||
}
|
||||
|
||||
refreshToken, err := auth.GenerateRefreshToken(userID, s.jwtSecret, s.refreshTTL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate refresh token: %w", err)
|
||||
return nil, model.NewInternalError("generate refresh token", err)
|
||||
}
|
||||
|
||||
session := &model.Session{
|
||||
@@ -237,7 +240,7 @@ func (s *AuthService) issueTokens(ctx context.Context, userID string) (*TokenPai
|
||||
}
|
||||
|
||||
if err := s.sessionRepo.Create(ctx, session); err != nil {
|
||||
return nil, fmt.Errorf("create session: %w", err)
|
||||
return nil, model.NewInternalError("create session", err)
|
||||
}
|
||||
|
||||
return &TokenPair{
|
||||
|
||||
Reference in New Issue
Block a user