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,6 +95,10 @@ sessionrepo := sessionrepo.New(psqlDB)

vertokrepo := vertokrepo.New(psqlDB) pwdtokrepo := passwordtokrepo.NewPasswordResetTokenRepo(psqlDB) + notecache := notecache.New(redisDB, cfg.CacheNoteTTL) + noterepo := noterepo.New(psqlDB) + notesrv := notesrv.New(noterepo, notePasswordHasher, notecache) + userepo := userepo.New(psqlDB) usercache := usercache.New(redisDB, cfg.CacheUsersTTL) usersrv := usersrv.New(

@@ -102,6 +106,7 @@ userepo,

sessionrepo, vertokrepo, pwdtokrepo, + noterepo, userPasswordHasher, jwtTokenizer, mailermq,

@@ -112,10 +117,6 @@ cfg.JwtRefreshTokenTTL,

cfg.VerificationTokenTTL, cfg.ResetPasswordTokenTTL, ) - - notecache := notecache.New(redisDB, cfg.CacheNoteTTL) - noterepo := noterepo.New(psqlDB) - notesrv := notesrv.New(noterepo, notePasswordHasher, notecache) rateLimiterConfig := ratelimit.Config{ RPS: cfg.RateLimiterRPS,
M e2e/apiv1_auth_test.go

@@ -449,13 +449,15 @@ e.Equal(httpResp.Code, http.StatusBadRequest)

} type getMeResponse struct { - Email string `json:"email"` - CreatedAt time.Time `json:"created_at"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` + LastLoginAt time.Time `json:"last_login_at"` + NotesCreated int `json:"notes_created"` } func (e *AppTestSuite) TestApiV1_getMe() { email := e.uuid() + "@test.com" - _, toks := e.createAndSingIn(email, "password") + uid, toks := e.createAndSingIn(email, "password") httpResp := e.httpRequest(http.MethodGet, "/api/v1/me", nil, toks.AccessToken)

@@ -466,6 +468,15 @@ e.readBodyAndUnjsonify(httpResp.Body, &body)

e.Equal(email, body.Email) e.NotZero(body.CreatedAt) + e.NotZero(body.LastLoginAt) + + var notesCount int + err := e.postgresDB. + QueryRow(e.ctx, "select count(*) from notes_authors where user_id = $1", uid). + Scan(&notesCount) + e.require.NoError(err) + + e.Equal(body.NotesCreated, notesCount) } // createAndSingIn creates an activated user, logs them in,
M e2e/e2e_test.go

@@ -105,6 +105,10 @@ pwdtokrepo := passwordtokrepo.NewPasswordResetTokenRepo(e.postgresDB)

stubOAuthProvider := newOauthProviderMock() + notecache := notecache.New(e.redisDB, cfg.CacheUsersTTL) + noterepo := noterepo.New(e.postgresDB) + notesrv := notesrv.New(noterepo, e.hasher, notecache) + userepo := userepo.New(e.postgresDB) usercache := usercache.New(e.redisDB, cfg.CacheUsersTTL) usersrv := usersrv.New(

@@ -112,6 +116,7 @@ userepo,

sessionrepo, vertokrepo, pwdtokrepo, + noterepo, e.hasher, e.jwtTokenizer, newMailerMockService(),

@@ -122,10 +127,6 @@ cfg.JwtRefreshTokenTTL,

cfg.VerificationTokenTTL, cfg.ResetPasswordTokenTTL, ) - - notecache := notecache.New(e.redisDB, cfg.CacheUsersTTL) - noterepo := noterepo.New(e.postgresDB) - notesrv := notesrv.New(noterepo, e.hasher, notecache) // for testing purposes, it's ok to have high values ig ratelimitCfg := ratelimit.Config{
M internal/dtos/user.go

@@ -41,6 +41,8 @@ Refresh string

} type UserInfo struct { - Email string - CreatedAt time.Time + Email string + CreatedAt time.Time + LastLoginAt time.Time + NotesCreated int }
M internal/service/usersrv/usersrv.go

@@ -13,6 +13,7 @@ "github.com/olexsmir/onasty/internal/hasher"

"github.com/olexsmir/onasty/internal/jwtutil" "github.com/olexsmir/onasty/internal/models" "github.com/olexsmir/onasty/internal/oauth" + "github.com/olexsmir/onasty/internal/store/psql/noterepo" "github.com/olexsmir/onasty/internal/store/psql/passwordtokrepo" "github.com/olexsmir/onasty/internal/store/psql/sessionrepo" "github.com/olexsmir/onasty/internal/store/psql/userepo"

@@ -51,6 +52,7 @@ userstore userepo.UserStorer

sessionstore sessionrepo.SessionStorer vertokrepo vertokrepo.VerificationTokenStorer pwdtokrepo passwordtokrepo.PasswordResetTokenStorer + notestore noterepo.NoteStorer cache usercache.UserCacheer hasher hasher.Hasher

@@ -70,6 +72,7 @@ userstore userepo.UserStorer,

sessionstore sessionrepo.SessionStorer, vertokrepo vertokrepo.VerificationTokenStorer, pwdtokrepo passwordtokrepo.PasswordResetTokenStorer, + notestore noterepo.NoteStorer, hasher hasher.Hasher, jwtTokenizer jwtutil.JWTTokenizer, mailermq mailermq.Mailer,

@@ -82,6 +85,7 @@ userstore: userstore,

sessionstore: sessionstore, vertokrepo: vertokrepo, pwdtokrepo: pwdtokrepo, + notestore: notestore, cache: cache, hasher: hasher, jwtTokenizer: jwtTokenizer,

@@ -174,9 +178,16 @@ if err != nil {

return dtos.UserInfo{}, err } + count, err := u.notestore.GetCountOfNotesByAuthorID(ctx, userID) + if err != nil { + return dtos.UserInfo{}, err + } + return dtos.UserInfo{ - Email: user.Email, - CreatedAt: user.CreatedAt, + Email: user.Email, + CreatedAt: user.CreatedAt, + LastLoginAt: user.LastLoginAt, + NotesCreated: int(count), }, nil }
M internal/store/psql/noterepo/noterepo.go

@@ -28,6 +28,9 @@

// GetAllByAuthorID returns all notes with specified author. GetAllByAuthorID(ctx context.Context, authorID uuid.UUID) ([]models.Note, error) + // GetCountOfNotesByAuthorID returns count of notes created by specified author. + GetCountOfNotesByAuthorID(ctx context.Context, authorID uuid.UUID) (int64, error) + // GetBySlugAndPassword gets a note by slug and password. // the "password" should be hashed. //

@@ -170,6 +173,20 @@ notes = append(notes, note)

} return notes, rows.Err() +} + +func (s *NoteRepo) GetCountOfNotesByAuthorID( + ctx context.Context, + authorID uuid.UUID, +) (int64, error) { + var count int64 + err := s.db.QueryRow( + ctx, + `select count(*) from notes_authors where user_id = $1`, + authorID.String(), + ).Scan(&count) + + return count, err } func (s *NoteRepo) GetBySlugAndPassword(
M internal/transport/http/apiv1/auth.go

@@ -264,8 +264,10 @@ })

} type getMeResponse struct { - Email string `json:"email"` - CreatedAt time.Time `json:"created_at"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` + LastLoginAt time.Time `json:"last_login_at"` + NotesCreated int `json:"notes_created"` } func (a *APIV1) getMeHandler(c *gin.Context) {

@@ -276,7 +278,9 @@ return

} c.JSON(http.StatusOK, getMeResponse{ - Email: uinfo.Email, - CreatedAt: uinfo.CreatedAt, + Email: uinfo.Email, + CreatedAt: uinfo.CreatedAt, + LastLoginAt: uinfo.LastLoginAt, + NotesCreated: uinfo.NotesCreated, }) }