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
        286
         	e.Equal(httpResp.Code, http.StatusBadRequest)

      
        287
        287
         }

      
        288
        288
         

      
        
        289
        +type apiV1AuthLogoutRequest struct {

      
        
        290
        +	RefreshToken string `json:"refresh_token"`

      
        
        291
        +}

      
        
        292
        +

      
        289
        293
         func (e *AppTestSuite) TestAuthV1_Logout() {

      
        290
        294
         	uid, toks := e.createAndSingIn(e.uuid()+"@test.com", "password")

      
        291
        295
         

      
        292
        296
         	sessionDB := e.getLastSessionByUserID(uid)

      
        293
        297
         	e.NotEmpty(sessionDB.RefreshToken)

      
        294
        298
         

      
        295
        
        -	httpResp := e.httpRequest(http.MethodPost, "/api/v1/auth/logout", nil, toks.AccessToken)

      
        
        299
        +	httpResp := e.httpRequest(

      
        
        300
        +		http.MethodPost,

      
        
        301
        +		"/api/v1/auth/logout",

      
        
        302
        +		e.jsonify(apiV1AuthLogoutRequest{

      
        
        303
        +			RefreshToken: toks.RefreshToken,

      
        
        304
        +		}),

      
        
        305
        +		toks.AccessToken,

      
        
        306
        +	)

      
        296
        307
         	e.Equal(httpResp.Code, http.StatusNoContent)

      
        297
        308
         

      
        298
        309
         	sessionDB = e.getLastSessionByUserID(uid)

      
        299
        310
         	e.Empty(sessionDB.RefreshToken)

      
        
        311
        +}

      
        
        312
        +

      
        
        313
        +func (e *AppTestSuite) TestAuthV1_LogoutAll() {

      
        
        314
        +	uid, toks := e.createAndSingIn(e.uuid()+"@test.com", "password")

      
        
        315
        +

      
        
        316
        +	var res int

      
        
        317
        +	query := "select count(*) from sessions where user_id = $1"

      
        
        318
        +

      
        
        319
        +	err := e.postgresDB.QueryRow(e.ctx, query, uid).Scan(&res)

      
        
        320
        +	e.require.NoError(err)

      
        
        321
        +	e.NotZero(res)

      
        
        322
        +

      
        
        323
        +	httpResp := e.httpRequest(http.MethodPost, "/api/v1/auth/logout/all", nil, toks.AccessToken)

      
        
        324
        +	e.Equal(httpResp.Code, http.StatusNoContent)

      
        
        325
        +

      
        
        326
        +	err = e.postgresDB.QueryRow(e.ctx, query, uid).Scan(&res)

      
        
        327
        +	e.require.NoError(err)

      
        
        328
        +	e.Zero(res)

      
        300
        329
         }

      
        301
        330
         

      
        302
        331
         type apiv1AuthChangePasswordRequest struct {

      
M internal/service/usersrv/usersrv.go
···
        24
        24
         	SignUp(ctx context.Context, inp dtos.SignUp) (uuid.UUID, error)

      
        25
        25
         	SignIn(ctx context.Context, inp dtos.SignIn) (dtos.Tokens, error)

      
        26
        26
         	RefreshTokens(ctx context.Context, refreshToken string) (dtos.Tokens, error)

      
        27
        
        -	Logout(ctx context.Context, userID uuid.UUID) error

      
        
        27
        +	Logout(ctx context.Context, userID uuid.UUID, refreshToken string) error

      
        
        28
        +	LogoutAll(ctx context.Context, userID uuid.UUID) error

      
        28
        29
         

      
        29
        30
         	ChangePassword(ctx context.Context, userID uuid.UUID, inp dtos.ChangeUserPassword) error

      
        30
        31
         	RequestPasswordReset(ctx context.Context, inp dtos.RequestResetPassword) error

      ···
        158
        159
         	return tokens, err

      
        159
        160
         }

      
        160
        161
         

      
        161
        
        -func (u *UserSrv) Logout(ctx context.Context, userID uuid.UUID) error {

      
        162
        
        -	return u.sessionstore.Delete(ctx, userID)

      
        
        162
        +func (u *UserSrv) Logout(ctx context.Context, userID uuid.UUID, refreshToken string) error {

      
        
        163
        +	return u.sessionstore.Delete(ctx, userID, refreshToken)

      
        
        164
        +}

      
        
        165
        +

      
        
        166
        +func (u *UserSrv) LogoutAll(ctx context.Context, userID uuid.UUID) error {

      
        
        167
        +	return u.sessionstore.DeleteAllByUserID(ctx, userID)

      
        163
        168
         }

      
        164
        169
         

      
        165
        170
         func (u *UserSrv) RefreshTokens(ctx context.Context, rtoken string) (dtos.Tokens, error) {

      
M internal/store/psql/sessionrepo/sessionrepo.go
···
        16
        16
         	Set(ctx context.Context, usedID uuid.UUID, refreshToken string, expiresAt time.Time) error

      
        17
        17
         	GetUserIDByRefreshToken(ctx context.Context, refreshToken string) (uuid.UUID, error)

      
        18
        18
         	Update(ctx context.Context, userID uuid.UUID, refreshToken string, newRefreshToken string) error

      
        19
        
        -	Delete(ctx context.Context, userID uuid.UUID) error

      
        
        19
        +	Delete(ctx context.Context, userID uuid.UUID, refreshToken string) error

      
        
        20
        +	DeleteAllByUserID(ctx context.Context, userID uuid.UUID) error

      
        20
        21
         }

      
        21
        22
         

      
        22
        23
         var _ SessionStorer = (*SessionRepo)(nil)

      ···
        95
        96
         	return userID, err

      
        96
        97
         }

      
        97
        98
         

      
        98
        
        -func (s *SessionRepo) Delete(ctx context.Context, userID uuid.UUID) error {

      
        
        99
        +func (s *SessionRepo) Delete(ctx context.Context, userID uuid.UUID, refreshToken string) error {

      
        99
        100
         	query := `--sql

      
        100
        101
         DELETE FROM sessions

      
        101
        102
         WHERE user_id = $1

      
        102
        
        -`

      
        
        103
        +  AND refresh_token = $2`

      
        
        104
        +

      
        
        105
        +	_, err := s.db.Exec(ctx, query, userID, refreshToken)

      
        
        106
        +	return err

      
        
        107
        +}

      
        
        108
        +

      
        
        109
        +func (s *SessionRepo) DeleteAllByUserID(ctx context.Context, userID uuid.UUID) error {

      
        
        110
        +	query := `--sql

      
        
        111
        +delete from sessions

      
        
        112
        +where user_id = $1`

      
        103
        113
         

      
        104
        114
         	_, err := s.db.Exec(ctx, query, userID)

      
        105
        115
         	return err

      
M internal/transport/http/apiv1/apiv1.go
···
        49
        49
         		authorized := auth.Group("/", a.authorizedMiddleware)

      
        50
        50
         		{

      
        51
        51
         			authorized.POST("/logout", a.logOutHandler)

      
        
        52
        +			authorized.POST("/logout/all", a.logOutAllHandler)

      
        52
        53
         			authorized.POST("/change-password", a.changePasswordHandler)

      
        53
        54
         		}

      
        54
        55
         	}

      
M internal/transport/http/apiv1/auth.go
···
        163
        163
         	c.Status(http.StatusOK)

      
        164
        164
         }

      
        165
        165
         

      
        
        166
        +type logoutRequest struct {

      
        
        167
        +	RefreshToken string `json:"refresh_token"`

      
        
        168
        +}

      
        
        169
        +

      
        166
        170
         func (a *APIV1) logOutHandler(c *gin.Context) {

      
        167
        
        -	if err := a.usersrv.Logout(c.Request.Context(), a.getUserID(c)); err != nil {

      
        
        171
        +	var req logoutRequest

      
        
        172
        +	if err := c.ShouldBindJSON(&req); err != nil {

      
        
        173
        +		newError(c, http.StatusBadRequest, "invalid request")

      
        
        174
        +		return

      
        
        175
        +	}

      
        
        176
        +

      
        
        177
        +	if err := a.usersrv.Logout(c.Request.Context(), a.getUserID(c), req.RefreshToken); err != nil {

      
        
        178
        +		errorResponse(c, err)

      
        
        179
        +		return

      
        
        180
        +	}

      
        
        181
        +

      
        
        182
        +	c.Status(http.StatusNoContent)

      
        
        183
        +}

      
        
        184
        +

      
        
        185
        +func (a *APIV1) logOutAllHandler(c *gin.Context) {

      
        
        186
        +	if err := a.usersrv.LogoutAll(c.Request.Context(), a.getUserID(c)); err != nil {

      
        168
        187
         		errorResponse(c, err)

      
        169
        188
         		return

      
        170
        189
         	}