added logout
This commit is contained in:
@@ -18,6 +18,7 @@ func GenerateToken(userID, email string, secret []byte, expiration time.Duration
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: userID,
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiration)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -38,6 +39,7 @@ func (h *Handler) Register(c *gin.Context) {
|
||||
c.JSON(http.StatusConflict, ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
log.Printf("register error: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"})
|
||||
return
|
||||
}
|
||||
@@ -64,7 +66,12 @@ func (h *Handler) Login(c *gin.Context) {
|
||||
|
||||
resp, err := h.service.Login(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, ErrorResponse{Error: err.Error()})
|
||||
if errors.Is(err, ErrInvalidCreds) {
|
||||
c.JSON(http.StatusUnauthorized, ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
log.Printf("login error: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -94,6 +101,7 @@ func (h *Handler) Refresh(c *gin.Context) {
|
||||
c.JSON(http.StatusUnauthorized, ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
log.Printf("refresh error: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"})
|
||||
return
|
||||
}
|
||||
@@ -101,6 +109,36 @@ func (h *Handler) Refresh(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// @Summary Logout epta
|
||||
// @Description Invalidate a refresh token (logout)
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body LogoutRequest true "Refresh token to invalidate"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 401 {object} ErrorResponse
|
||||
// @Router /api/auth/logout [post]
|
||||
func (h *Handler) Logout(c *gin.Context) {
|
||||
var req LogoutRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.Logout(c.Request.Context(), req.RefreshToken); err != nil {
|
||||
if errors.Is(err, ErrLogoutInvalid) {
|
||||
c.JSON(http.StatusUnauthorized, ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
log.Printf("logout error: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "logged out successfully"})
|
||||
}
|
||||
|
||||
// @Summary Epta get current user
|
||||
// @Description Get authenticated user's profile
|
||||
// @Tags auth
|
||||
@@ -129,6 +167,7 @@ func (h *Handler) Me(c *gin.Context) {
|
||||
c.JSON(http.StatusNotFound, ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
log.Printf("me error: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func AuthMiddleware(jwtSecret []byte) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrorResponse{Error: "invalid authorization header format"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ type RefreshRequest struct {
|
||||
RefreshToken string `json:"refresh_token" binding:"required" example:"dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4="`
|
||||
}
|
||||
|
||||
type LogoutRequest struct {
|
||||
RefreshToken string `json:"refresh_token" binding:"required" example:"dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4="`
|
||||
}
|
||||
|
||||
type RefreshTokenDoc struct {
|
||||
ID bson.ObjectID `json:"id" bson:"_id"`
|
||||
UserID bson.ObjectID `json:"user_id" bson:"user_id"`
|
||||
|
||||
@@ -90,3 +90,11 @@ func (r *Repository) DeleteRefreshToken(ctx context.Context, id bson.ObjectID) e
|
||||
_, err := r.refreshTokensCollection.DeleteOne(ctx, bson.M{"_id": id})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteRefreshTokenByHash(ctx context.Context, hash string) (bool, error) {
|
||||
res, err := r.refreshTokensCollection.DeleteOne(ctx, bson.M{"token_hash": hash})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return res.DeletedCount > 0, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
@@ -21,6 +23,7 @@ var (
|
||||
ErrInvalidUserID = errors.New("invalid user ID")
|
||||
ErrInvalidRefresh = errors.New("invalid refresh token")
|
||||
ErrRefreshExpired = errors.New("refresh token expired")
|
||||
ErrLogoutInvalid = errors.New("refresh token not found or already used")
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -81,6 +84,7 @@ func (s *Service) issueTokenPair(ctx context.Context, user *User) (*AuthResponse
|
||||
}
|
||||
|
||||
func (s *Service) Register(ctx context.Context, req RegisterRequest) (*UserPublic, error) {
|
||||
req.Email = strings.ToLower(req.Email)
|
||||
existing, err := s.repo.FindByEmail(ctx, req.Email)
|
||||
if err != nil && !errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return nil, fmt.Errorf("failed to check existing user: %w", err)
|
||||
@@ -109,6 +113,7 @@ func (s *Service) Register(ctx context.Context, req RegisterRequest) (*UserPubli
|
||||
}
|
||||
|
||||
func (s *Service) Login(ctx context.Context, req LoginRequest) (*AuthResponse, error) {
|
||||
req.Email = strings.ToLower(req.Email)
|
||||
user, err := s.repo.FindByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
@@ -136,7 +141,9 @@ func (s *Service) Refresh(ctx context.Context, rawRefresh string) (*AuthResponse
|
||||
}
|
||||
|
||||
if time.Now().UTC().After(doc.ExpiresAt) {
|
||||
s.repo.DeleteRefreshToken(ctx, doc.ID)
|
||||
if err := s.repo.DeleteRefreshToken(ctx, doc.ID); err != nil {
|
||||
log.Printf("failed to cleanup expired refresh token: %v", err)
|
||||
}
|
||||
return nil, ErrRefreshExpired
|
||||
}
|
||||
|
||||
@@ -152,6 +159,20 @@ func (s *Service) Refresh(ctx context.Context, rawRefresh string) (*AuthResponse
|
||||
return s.issueTokenPair(ctx, user)
|
||||
}
|
||||
|
||||
func (s *Service) Logout(ctx context.Context, rawRefresh string) error {
|
||||
hash := sha256Hex(rawRefresh)
|
||||
|
||||
found, err := s.repo.DeleteRefreshTokenByHash(ctx, hash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete refresh token: %w", err)
|
||||
}
|
||||
if !found {
|
||||
return ErrLogoutInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) GetUserByID(ctx context.Context, userID string) (*UserPublic, error) {
|
||||
id, err := bson.ObjectIDFromHex(userID)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user