all repos

onasty @ 060f2e6

a one-time notes service
5 files changed, 72 insertions(+), 8 deletions(-)
feat: logout from all sessions (#130)

* fix: logout from one specific session

* feat: add logout from all sessions

* test(e2e): fix log out test

* test(e2e): logout all

* fixup! test(e2e): logout all
Author: Smirnov Oleksandr ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-06-07 17:47:56 +0300
Parent: 51d3b53
M e2e/apiv1_auth_test.go

@@ -286,17 +286,46 @@

e.Equal(httpResp.Code, http.StatusBadRequest) } +type apiV1AuthLogoutRequest struct { + RefreshToken string `json:"refresh_token"` +} + func (e *AppTestSuite) TestAuthV1_Logout() { uid, toks := e.createAndSingIn(e.uuid()+"@test.com", "password") sessionDB := e.getLastSessionByUserID(uid) e.NotEmpty(sessionDB.RefreshToken) - httpResp := e.httpRequest(http.MethodPost, "/api/v1/auth/logout", nil, toks.AccessToken) + httpResp := e.httpRequest( + http.MethodPost, + "/api/v1/auth/logout", + e.jsonify(apiV1AuthLogoutRequest{ + RefreshToken: toks.RefreshToken, + }), + toks.AccessToken, + ) e.Equal(httpResp.Code, http.StatusNoContent) sessionDB = e.getLastSessionByUserID(uid) e.Empty(sessionDB.RefreshToken) +} + +func (e *AppTestSuite) TestAuthV1_LogoutAll() { + uid, toks := e.createAndSingIn(e.uuid()+"@test.com", "password") + + var res int + query := "select count(*) from sessions where user_id = $1" + + err := e.postgresDB.QueryRow(e.ctx, query, uid).Scan(&res) + e.require.NoError(err) + e.NotZero(res) + + httpResp := e.httpRequest(http.MethodPost, "/api/v1/auth/logout/all", nil, toks.AccessToken) + e.Equal(httpResp.Code, http.StatusNoContent) + + err = e.postgresDB.QueryRow(e.ctx, query, uid).Scan(&res) + e.require.NoError(err) + e.Zero(res) } type apiv1AuthChangePasswordRequest struct {
M internal/service/usersrv/usersrv.go

@@ -24,7 +24,8 @@ type UserServicer interface {

SignUp(ctx context.Context, inp dtos.SignUp) (uuid.UUID, error) SignIn(ctx context.Context, inp dtos.SignIn) (dtos.Tokens, error) RefreshTokens(ctx context.Context, refreshToken string) (dtos.Tokens, error) - Logout(ctx context.Context, userID uuid.UUID) error + Logout(ctx context.Context, userID uuid.UUID, refreshToken string) error + LogoutAll(ctx context.Context, userID uuid.UUID) error ChangePassword(ctx context.Context, userID uuid.UUID, inp dtos.ChangeUserPassword) error RequestPasswordReset(ctx context.Context, inp dtos.RequestResetPassword) error

@@ -158,8 +159,12 @@ tokens, err := u.issueTokens(ctx, user.ID)

return tokens, err } -func (u *UserSrv) Logout(ctx context.Context, userID uuid.UUID) error { - return u.sessionstore.Delete(ctx, userID) +func (u *UserSrv) Logout(ctx context.Context, userID uuid.UUID, refreshToken string) error { + return u.sessionstore.Delete(ctx, userID, refreshToken) +} + +func (u *UserSrv) LogoutAll(ctx context.Context, userID uuid.UUID) error { + return u.sessionstore.DeleteAllByUserID(ctx, userID) } func (u *UserSrv) RefreshTokens(ctx context.Context, rtoken string) (dtos.Tokens, error) {
M internal/store/psql/sessionrepo/sessionrepo.go

@@ -16,7 +16,8 @@ type SessionStorer interface {

Set(ctx context.Context, usedID uuid.UUID, refreshToken string, expiresAt time.Time) error GetUserIDByRefreshToken(ctx context.Context, refreshToken string) (uuid.UUID, error) Update(ctx context.Context, userID uuid.UUID, refreshToken string, newRefreshToken string) error - Delete(ctx context.Context, userID uuid.UUID) error + Delete(ctx context.Context, userID uuid.UUID, refreshToken string) error + DeleteAllByUserID(ctx context.Context, userID uuid.UUID) error } var _ SessionStorer = (*SessionRepo)(nil)

@@ -95,11 +96,20 @@

return userID, err } -func (s *SessionRepo) Delete(ctx context.Context, userID uuid.UUID) error { +func (s *SessionRepo) Delete(ctx context.Context, userID uuid.UUID, refreshToken string) error { query := `--sql DELETE FROM sessions WHERE user_id = $1 -` + AND refresh_token = $2` + + _, err := s.db.Exec(ctx, query, userID, refreshToken) + return err +} + +func (s *SessionRepo) DeleteAllByUserID(ctx context.Context, userID uuid.UUID) error { + query := `--sql +delete from sessions +where user_id = $1` _, err := s.db.Exec(ctx, query, userID) return err
M internal/transport/http/apiv1/apiv1.go

@@ -49,6 +49,7 @@

authorized := auth.Group("/", a.authorizedMiddleware) { authorized.POST("/logout", a.logOutHandler) + authorized.POST("/logout/all", a.logOutAllHandler) authorized.POST("/change-password", a.changePasswordHandler) } }
M internal/transport/http/apiv1/auth.go

@@ -163,8 +163,27 @@

c.Status(http.StatusOK) } +type logoutRequest struct { + RefreshToken string `json:"refresh_token"` +} + func (a *APIV1) logOutHandler(c *gin.Context) { - if err := a.usersrv.Logout(c.Request.Context(), a.getUserID(c)); err != nil { + var req logoutRequest + if err := c.ShouldBindJSON(&req); err != nil { + newError(c, http.StatusBadRequest, "invalid request") + return + } + + if err := a.usersrv.Logout(c.Request.Context(), a.getUserID(c), req.RefreshToken); err != nil { + errorResponse(c, err) + return + } + + c.Status(http.StatusNoContent) +} + +func (a *APIV1) logOutAllHandler(c *gin.Context) { + if err := a.usersrv.LogoutAll(c.Request.Context(), a.getUserID(c)); err != nil { errorResponse(c, err) return }