all repos

onasty @ 39d6b8e

a one-time notes service
7 files changed, 66 insertions(+), 19 deletions(-)
feat(api): return more info about user on /me route (#162)

* feat(api): return more info about user on /me route

* test(e2e): test updated /me

* docs(noterepo): document new method
Author: Olexandr Smirnov ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-07-08 16:12:38 +0300
Parent: f01c95c
M cmd/api/main.go
···
        95
        95
         	vertokrepo := vertokrepo.New(psqlDB)

      
        96
        96
         	pwdtokrepo := passwordtokrepo.NewPasswordResetTokenRepo(psqlDB)

      
        97
        97
         

      
        
        98
        +	notecache := notecache.New(redisDB, cfg.CacheNoteTTL)

      
        
        99
        +	noterepo := noterepo.New(psqlDB)

      
        
        100
        +	notesrv := notesrv.New(noterepo, notePasswordHasher, notecache)

      
        
        101
        +

      
        98
        102
         	userepo := userepo.New(psqlDB)

      
        99
        103
         	usercache := usercache.New(redisDB, cfg.CacheUsersTTL)

      
        100
        104
         	usersrv := usersrv.New(

      ···
        102
        106
         		sessionrepo,

      
        103
        107
         		vertokrepo,

      
        104
        108
         		pwdtokrepo,

      
        
        109
        +		noterepo,

      
        105
        110
         		userPasswordHasher,

      
        106
        111
         		jwtTokenizer,

      
        107
        112
         		mailermq,

      ···
        112
        117
         		cfg.VerificationTokenTTL,

      
        113
        118
         		cfg.ResetPasswordTokenTTL,

      
        114
        119
         	)

      
        115
        
        -

      
        116
        
        -	notecache := notecache.New(redisDB, cfg.CacheNoteTTL)

      
        117
        
        -	noterepo := noterepo.New(psqlDB)

      
        118
        
        -	notesrv := notesrv.New(noterepo, notePasswordHasher, notecache)

      
        119
        120
         

      
        120
        121
         	rateLimiterConfig := ratelimit.Config{

      
        121
        122
         		RPS:   cfg.RateLimiterRPS,

      
M e2e/apiv1_auth_test.go
···
        449
        449
         }

      
        450
        450
         

      
        451
        451
         type getMeResponse struct {

      
        452
        
        -	Email     string    `json:"email"`

      
        453
        
        -	CreatedAt time.Time `json:"created_at"`

      
        
        452
        +	Email        string    `json:"email"`

      
        
        453
        +	CreatedAt    time.Time `json:"created_at"`

      
        
        454
        +	LastLoginAt  time.Time `json:"last_login_at"`

      
        
        455
        +	NotesCreated int       `json:"notes_created"`

      
        454
        456
         }

      
        455
        457
         

      
        456
        458
         func (e *AppTestSuite) TestApiV1_getMe() {

      
        457
        459
         	email := e.uuid() + "@test.com"

      
        458
        
        -	_, toks := e.createAndSingIn(email, "password")

      
        
        460
        +	uid, toks := e.createAndSingIn(email, "password")

      
        459
        461
         

      
        460
        462
         	httpResp := e.httpRequest(http.MethodGet, "/api/v1/me", nil, toks.AccessToken)

      
        461
        463
         

      ···
        466
        468
         

      
        467
        469
         	e.Equal(email, body.Email)

      
        468
        470
         	e.NotZero(body.CreatedAt)

      
        
        471
        +	e.NotZero(body.LastLoginAt)

      
        
        472
        +

      
        
        473
        +	var notesCount int

      
        
        474
        +	err := e.postgresDB.

      
        
        475
        +		QueryRow(e.ctx, "select count(*) from notes_authors where user_id = $1", uid).

      
        
        476
        +		Scan(&notesCount)

      
        
        477
        +	e.require.NoError(err)

      
        
        478
        +

      
        
        479
        +	e.Equal(body.NotesCreated, notesCount)

      
        469
        480
         }

      
        470
        481
         

      
        471
        482
         // createAndSingIn creates an activated user, logs them in,

      
M e2e/e2e_test.go
···
        105
        105
         

      
        106
        106
         	stubOAuthProvider := newOauthProviderMock()

      
        107
        107
         

      
        
        108
        +	notecache := notecache.New(e.redisDB, cfg.CacheUsersTTL)

      
        
        109
        +	noterepo := noterepo.New(e.postgresDB)

      
        
        110
        +	notesrv := notesrv.New(noterepo, e.hasher, notecache)

      
        
        111
        +

      
        108
        112
         	userepo := userepo.New(e.postgresDB)

      
        109
        113
         	usercache := usercache.New(e.redisDB, cfg.CacheUsersTTL)

      
        110
        114
         	usersrv := usersrv.New(

      ···
        112
        116
         		sessionrepo,

      
        113
        117
         		vertokrepo,

      
        114
        118
         		pwdtokrepo,

      
        
        119
        +		noterepo,

      
        115
        120
         		e.hasher,

      
        116
        121
         		e.jwtTokenizer,

      
        117
        122
         		newMailerMockService(),

      ···
        122
        127
         		cfg.VerificationTokenTTL,

      
        123
        128
         		cfg.ResetPasswordTokenTTL,

      
        124
        129
         	)

      
        125
        
        -

      
        126
        
        -	notecache := notecache.New(e.redisDB, cfg.CacheUsersTTL)

      
        127
        
        -	noterepo := noterepo.New(e.postgresDB)

      
        128
        
        -	notesrv := notesrv.New(noterepo, e.hasher, notecache)

      
        129
        130
         

      
        130
        131
         	// for testing purposes, it's ok to have high values ig

      
        131
        132
         	ratelimitCfg := ratelimit.Config{

      
M internal/dtos/user.go
···
        41
        41
         }

      
        42
        42
         

      
        43
        43
         type UserInfo struct {

      
        44
        
        -	Email     string

      
        45
        
        -	CreatedAt time.Time

      
        
        44
        +	Email        string

      
        
        45
        +	CreatedAt    time.Time

      
        
        46
        +	LastLoginAt  time.Time

      
        
        47
        +	NotesCreated int

      
        46
        48
         }

      
M internal/service/usersrv/usersrv.go
···
        13
        13
         	"github.com/olexsmir/onasty/internal/jwtutil"

      
        14
        14
         	"github.com/olexsmir/onasty/internal/models"

      
        15
        15
         	"github.com/olexsmir/onasty/internal/oauth"

      
        
        16
        +	"github.com/olexsmir/onasty/internal/store/psql/noterepo"

      
        16
        17
         	"github.com/olexsmir/onasty/internal/store/psql/passwordtokrepo"

      
        17
        18
         	"github.com/olexsmir/onasty/internal/store/psql/sessionrepo"

      
        18
        19
         	"github.com/olexsmir/onasty/internal/store/psql/userepo"

      ···
        51
        52
         	sessionstore sessionrepo.SessionStorer

      
        52
        53
         	vertokrepo   vertokrepo.VerificationTokenStorer

      
        53
        54
         	pwdtokrepo   passwordtokrepo.PasswordResetTokenStorer

      
        
        55
        +	notestore    noterepo.NoteStorer

      
        54
        56
         	cache        usercache.UserCacheer

      
        55
        57
         

      
        56
        58
         	hasher       hasher.Hasher

      ···
        70
        72
         	sessionstore sessionrepo.SessionStorer,

      
        71
        73
         	vertokrepo vertokrepo.VerificationTokenStorer,

      
        72
        74
         	pwdtokrepo passwordtokrepo.PasswordResetTokenStorer,

      
        
        75
        +	notestore noterepo.NoteStorer,

      
        73
        76
         	hasher hasher.Hasher,

      
        74
        77
         	jwtTokenizer jwtutil.JWTTokenizer,

      
        75
        78
         	mailermq mailermq.Mailer,

      ···
        82
        85
         		sessionstore:          sessionstore,

      
        83
        86
         		vertokrepo:            vertokrepo,

      
        84
        87
         		pwdtokrepo:            pwdtokrepo,

      
        
        88
        +		notestore:             notestore,

      
        85
        89
         		cache:                 cache,

      
        86
        90
         		hasher:                hasher,

      
        87
        91
         		jwtTokenizer:          jwtTokenizer,

      ···
        174
        178
         		return dtos.UserInfo{}, err

      
        175
        179
         	}

      
        176
        180
         

      
        
        181
        +	count, err := u.notestore.GetCountOfNotesByAuthorID(ctx, userID)

      
        
        182
        +	if err != nil {

      
        
        183
        +		return dtos.UserInfo{}, err

      
        
        184
        +	}

      
        
        185
        +

      
        177
        186
         	return dtos.UserInfo{

      
        178
        
        -		Email:     user.Email,

      
        179
        
        -		CreatedAt: user.CreatedAt,

      
        
        187
        +		Email:        user.Email,

      
        
        188
        +		CreatedAt:    user.CreatedAt,

      
        
        189
        +		LastLoginAt:  user.LastLoginAt,

      
        
        190
        +		NotesCreated: int(count),

      
        180
        191
         	}, nil

      
        181
        192
         }

      
        182
        193
         

      
M internal/store/psql/noterepo/noterepo.go
···
        28
        28
         	// GetAllByAuthorID returns all notes with specified author.

      
        29
        29
         	GetAllByAuthorID(ctx context.Context, authorID uuid.UUID) ([]models.Note, error)

      
        30
        30
         

      
        
        31
        +	// GetCountOfNotesByAuthorID returns count of notes created by specified author.

      
        
        32
        +	GetCountOfNotesByAuthorID(ctx context.Context, authorID uuid.UUID) (int64, error)

      
        
        33
        +

      
        31
        34
         	// GetBySlugAndPassword gets a note by slug and password.

      
        32
        35
         	// the "password" should be hashed.

      
        33
        36
         	//

      ···
        170
        173
         	}

      
        171
        174
         

      
        172
        175
         	return notes, rows.Err()

      
        
        176
        +}

      
        
        177
        +

      
        
        178
        +func (s *NoteRepo) GetCountOfNotesByAuthorID(

      
        
        179
        +	ctx context.Context,

      
        
        180
        +	authorID uuid.UUID,

      
        
        181
        +) (int64, error) {

      
        
        182
        +	var count int64

      
        
        183
        +	err := s.db.QueryRow(

      
        
        184
        +		ctx,

      
        
        185
        +		`select count(*) from notes_authors where user_id = $1`,

      
        
        186
        +		authorID.String(),

      
        
        187
        +	).Scan(&count)

      
        
        188
        +

      
        
        189
        +	return count, err

      
        173
        190
         }

      
        174
        191
         

      
        175
        192
         func (s *NoteRepo) GetBySlugAndPassword(

      
M internal/transport/http/apiv1/auth.go
···
        264
        264
         }

      
        265
        265
         

      
        266
        266
         type getMeResponse struct {

      
        267
        
        -	Email     string    `json:"email"`

      
        268
        
        -	CreatedAt time.Time `json:"created_at"`

      
        
        267
        +	Email        string    `json:"email"`

      
        
        268
        +	CreatedAt    time.Time `json:"created_at"`

      
        
        269
        +	LastLoginAt  time.Time `json:"last_login_at"`

      
        
        270
        +	NotesCreated int       `json:"notes_created"`

      
        269
        271
         }

      
        270
        272
         

      
        271
        273
         func (a *APIV1) getMeHandler(c *gin.Context) {

      ···
        276
        278
         	}

      
        277
        279
         

      
        278
        280
         	c.JSON(http.StatusOK, getMeResponse{

      
        279
        
        -		Email:     uinfo.Email,

      
        280
        
        -		CreatedAt: uinfo.CreatedAt,

      
        
        281
        +		Email:        uinfo.Email,

      
        
        282
        +		CreatedAt:    uinfo.CreatedAt,

      
        
        283
        +		LastLoginAt:  uinfo.LastLoginAt,

      
        
        284
        +		NotesCreated: uinfo.NotesCreated,

      
        281
        285
         	})

      
        282
        286
         }