Skip to content

Commit 3da2179

Browse files
committed
feat: implement user logout
1 parent 69226d4 commit 3da2179

File tree

4 files changed

+62
-4
lines changed

4 files changed

+62
-4
lines changed

docs/api.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,11 @@ Uses the **refresh token cookie** to issue a new access token.
9797
**Errors**:
9898

9999
- `401 Unauthorized` → missing, invalid, or expired refresh token
100+
101+
### 4. Logout
102+
103+
`POST /auth/logout`
104+
105+
Revokes refresh token (cookie cleared).
106+
107+
**Response** `204 No Content`

internal/domain/token/service.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ func (s *Service) CreateRefreshToken(ctx context.Context, userId user.UserID) (*
6565
return token, nil
6666
}
6767

68+
func (s *Service) RevokeRefreshToken(ctx context.Context, refreshTok string) error {
69+
hash := HashPlaintext(refreshTok)
70+
71+
if err := s.repo.Revoke(ctx, "auth", hash); err != nil {
72+
return err
73+
}
74+
75+
return nil
76+
}
77+
6878
func (s *Service) RotateTokens(ctx context.Context, refreshTok string) (string, *Token, error) {
6979
hash := HashPlaintext(refreshTok)
7080

internal/transport/http/router.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,7 @@ func NewRouter(log *logger.Logger, userMw *UserMiddleware, userHandler *UserHand
3030
r.Group(func(r chi.Router) {
3131
r.Use(userMw.RequireAuthMiddleware)
3232

33-
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
34-
w.WriteHeader(http.StatusOK)
35-
_, _ = w.Write([]byte("OK"))
36-
})
33+
r.Post("/auth/logout", userHandler.LogoutUser)
3734
})
3835

3936
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {

internal/transport/http/user_handler.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"net/http"
8+
"time"
89

910
"github.com/raphico/go-device-telemetry-api/internal/config"
1011
"github.com/raphico/go-device-telemetry-api/internal/domain/token"
@@ -199,3 +200,45 @@ func (h *UserHandler) RefreshAccessToken(w http.ResponseWriter, r *http.Request)
199200

200201
WriteJSON(w, http.StatusOK, resp)
201202
}
203+
204+
type logoutResponse struct {
205+
Message string `json:"message"`
206+
}
207+
208+
func (h *UserHandler) LogoutUser(w http.ResponseWriter, r *http.Request) {
209+
cookie, err := r.Cookie("refresh_token")
210+
211+
if err != nil {
212+
w.WriteHeader(http.StatusNoContent)
213+
return
214+
}
215+
216+
refreshToken := cookie.Value
217+
err = h.tokenService.RevokeRefreshToken(r.Context(), refreshToken)
218+
if err != nil {
219+
switch {
220+
case errors.Is(err, token.ErrTokenNotFound):
221+
w.WriteHeader(http.StatusNoContent)
222+
default:
223+
h.log.Error(fmt.Sprintf("failed to revoke refresh token: %v", err))
224+
WriteJSONError(w, http.StatusInternalServerError, "internal_error", "Logout failed, please try again")
225+
}
226+
return
227+
}
228+
229+
http.SetCookie(w, &http.Cookie{
230+
Name: "refresh_token",
231+
Value: "",
232+
Path: "/",
233+
SameSite: http.SameSiteLaxMode,
234+
HttpOnly: true,
235+
Expires: time.Unix(0, 0),
236+
})
237+
if h.cfg.Env == "production" {
238+
cookie.Secure = true
239+
} else {
240+
cookie.Secure = false
241+
}
242+
243+
w.WriteHeader(http.StatusNoContent)
244+
}

0 commit comments

Comments
 (0)