diff --git a/app/controllers/logout.go b/app/controllers/logout.go new file mode 100644 index 0000000..0ca5d02 --- /dev/null +++ b/app/controllers/logout.go @@ -0,0 +1,42 @@ +package controllers + +import ( + "git.secnex.io/secnex/auth-api/services" + "git.secnex.io/secnex/auth-api/utils" + "git.secnex.io/secnex/masterlog" + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" +) + +type LogoutRequest struct { + SessionToken string `json:"token" validate:"required"` +} + +func LogoutController(c *fiber.Ctx) error { + var request LogoutRequest + if err := c.BodyParser(&request); err != nil { + return utils.NewErrorResponse(fiber.StatusBadRequest, &fiber.Map{ + "message": "Invalid request body", + }).Send(c) + } + + masterlog.Debug("Processing logout request", map[string]interface{}{"session_token": request.SessionToken}) + + validate := validator.New() + if err := validate.Struct(request); err != nil { + return utils.NewErrorResponse(fiber.StatusBadRequest, &fiber.Map{ + "message": "Invalid request body", + }).Send(c) + } + + err := services.Logout(request.SessionToken) + if err != nil { + return utils.NewErrorResponse(fiber.StatusInternalServerError, &fiber.Map{ + "message": "Error logging out", + }).Send(c) + } + + return utils.NewHTTPResponse(fiber.StatusOK, &fiber.Map{ + "message": "OK", + }, "", nil, nil).Send(c) +} diff --git a/app/main.go b/app/main.go index c5d4f75..5635bc5 100644 --- a/app/main.go +++ b/app/main.go @@ -65,6 +65,7 @@ func main() { // Controllers app.Post("/login", controllers.LoginController) app.Post("/register", controllers.RegisterController) + app.Post("/logout", controllers.LogoutController) app.Post("/session/info", controllers.SessionInfoController) if config.ENV == "development" { diff --git a/app/repositories/sessions.go b/app/repositories/sessions.go index b070725..082b30d 100644 --- a/app/repositories/sessions.go +++ b/app/repositories/sessions.go @@ -97,3 +97,31 @@ func GetSessionCache(sessionID string) *SessionDetails { masterlog.Debug("Session retrieved from cache", map[string]interface{}{"session_id": sessionID, "user_id": sessionDetails.UserID}) return &sessionDetails } + +func DeleteSession(sessionID string) error { + masterlog.Debug("Deleting session from database and cache", map[string]interface{}{"session_id": sessionID}) + if err := DeleteSessionCache(sessionID); err != nil { + masterlog.Debug("Failed to delete session from cache", map[string]interface{}{"error": err.Error(), "session_id": sessionID}) + return err + } + if err := database.DB.Where("id = ?", sessionID).Delete(&models.Session{}).Error; err != nil { + masterlog.Debug("Failed to delete session from database", map[string]interface{}{"error": err.Error(), "session_id": sessionID}) + return err + } + masterlog.Debug("Session deleted from database and cache", map[string]interface{}{"session_id": sessionID}) + return nil +} + +func DeleteSessionCache(sessionID string) error { + masterlog.Debug("Deleting session from cache", map[string]interface{}{"session_id": sessionID}) + if cache.Cache.Client == nil { + masterlog.Debug("Redis client not initialized", map[string]interface{}{"session_id": sessionID}) + return nil + } + if err := cache.Cache.Client.Do(cache.Cache.Context, cache.Cache.Client.B().Del().Key(sessionID).Build()).Error(); err != nil { + masterlog.Debug("Failed to delete session from cache", map[string]interface{}{"error": err.Error(), "session_id": sessionID}) + return err + } + masterlog.Debug("Session deleted from cache", map[string]interface{}{"session_id": sessionID}) + return nil +} diff --git a/app/services/logout.go b/app/services/logout.go new file mode 100644 index 0000000..19c5e20 --- /dev/null +++ b/app/services/logout.go @@ -0,0 +1,51 @@ +package services + +import ( + "errors" + "fmt" + + "git.secnex.io/secnex/auth-api/config" + "git.secnex.io/secnex/auth-api/repositories" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" +) + +func Logout(sessionToken string) error { + claims, err := jwt.ParseWithClaims(sessionToken, &jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(config.CONFIG.JwtSecret), nil + }) + if err != nil { + return err + } + if !claims.Valid { + return errors.New("invalid token") + } + + mapClaims := claims.Claims.(*jwt.MapClaims) + subValue, ok := (*mapClaims)["sub"] + if !ok { + return errors.New("sub claim not found") + } + + var sessionID string + switch v := subValue.(type) { + case string: + sessionID = v + case uuid.UUID: + sessionID = v.String() + default: + sessionID = fmt.Sprintf("%v", v) + } + + err = repositories.DeleteSessionCache(sessionID) + if err != nil { + return err + } + + err = repositories.DeleteSession(sessionID) + if err != nil { + return err + } + + return nil +}