all repos

onasty @ 218bb4e

a one-time notes service
13 files changed, 49 insertions(+), 100 deletions(-)
refactor: remove `username` (#112)

* chore: migrations

* refactor: remove all mentions of username

* e2e: refactor username out

* test(models): fix test

* fix(usersrv): validate user's password correctly
Author: Smirnov Oleksandr ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-05-21 00:35:22 +0300
Parent: 35d2b54
M e2e/apiv1_auth_test.go

@@ -8,13 +8,11 @@ "github.com/olexsmir/onasty/internal/models"

) type apiv1AuthSignUpRequest struct { - Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` } func (e *AppTestSuite) TestAuthV1_SignUP() { - username := "test" + e.uuid() email := e.uuid() + "test@test.com" password := "password"

@@ -22,13 +20,12 @@ httpResp := e.httpRequest(

http.MethodPost, "/api/v1/auth/signup", e.jsonify(apiv1AuthSignUpRequest{ - Username: username, Email: email, Password: password, }), ) - dbUser := e.getUserByUsername(username) + dbUser := e.getUserByEmail(email) hashedPasswd, err := e.hasher.Hash(password) e.require.NoError(err)

@@ -40,30 +37,18 @@

func (e *AppTestSuite) TestAuthV1_SignUP_badrequest() { tests := []struct { name string - username string email string password string }{ - {name: "all fields empty", email: "", password: "", username: ""}, - { - name: "non valid email", - username: "testing", - email: "email", - password: "password", - }, - { - name: "non valid password", - email: "test@test.com", - password: "12345", - username: "test", - }, + {name: "all fields empty", email: "", password: ""}, + {name: "non valid email", email: "email", password: "password"}, + {name: "non valid password", email: "test@test.com", password: "12345"}, } for _, t := range tests { httpResp := e.httpRequest( http.MethodPost, "/api/v1/auth/signup", e.jsonify(apiv1AuthSignUpRequest{ - Username: t.username, Email: t.email, Password: t.password, }),

@@ -92,7 +77,6 @@ httpResp := e.httpRequest(

http.MethodPost, "/api/v1/auth/signup", e.jsonify(apiv1AuthSignUpRequest{ - Username: e.uuid(), Email: email, Password: password, }),

@@ -119,7 +103,6 @@ signUpHTTPResp := e.httpRequest(

http.MethodPost, "/api/v1/auth/signup", e.jsonify(apiv1AuthSignUpRequest{ - Username: e.uuid(), Email: email, Password: password, }),

@@ -143,7 +126,7 @@ }

func (e *AppTestSuite) TestAuthV1_ResendVerificationEmail_wrong() { email, password := e.uuid()+"@"+e.uuid()+".com", "password" - e.insertUser(e.uuid(), email, password, true) + e.insertUser(email, password, true) tests := []struct { name string

@@ -182,8 +165,7 @@

func (e *AppTestSuite) TestAuthV1_SignIn() { email := e.uuid() + "email@email.com" password := "qwerty" - - uid := e.insertUser("test", email, password, true) + uid := e.insertUser(email, password, true) httpResp := e.httpRequest( http.MethodPost,

@@ -208,10 +190,10 @@

func (e *AppTestSuite) TestAuthV1_SignIn_wrong() { password := "password" email := e.uuid() + "@test.com" - e.insertUser(e.uuid(), email, "password", true) + e.insertUser(email, "password", true) unactivatedEmail := e.uuid() + "@test.com" - e.insertUser(e.uuid(), unactivatedEmail, password, false) + e.insertUser(unactivatedEmail, password, false) //exhaustruct:ignore tests := []struct {

@@ -271,7 +253,7 @@ RefreshToken string `json:"refresh_token"`

} func (e *AppTestSuite) TestAuthV1_RefreshTokens() { - uid, toks := e.createAndSingIn(e.uuid()+"@test.com", e.uuid(), "password") + uid, toks := e.createAndSingIn(e.uuid()+"@test.com", "password") httpResp := e.httpRequest( http.MethodPost, "/api/v1/auth/refresh-tokens",

@@ -306,7 +288,7 @@ e.Equal(httpResp.Code, http.StatusBadRequest)

} func (e *AppTestSuite) TestAuthV1_Logout() { - uid, toks := e.createAndSingIn(e.uuid()+"@test.com", e.uuid(), "password") + uid, toks := e.createAndSingIn(e.uuid()+"@test.com", "password") sessionDB := e.getLastSessionByUserID(uid) e.NotEmpty(sessionDB.RefreshToken)

@@ -326,8 +308,8 @@

func (e *AppTestSuite) TestAuthV1_ChangePassword() { password := e.uuid() newPassword := e.uuid() - username := e.uuid() - _, toks := e.createAndSingIn(e.uuid()+"@test.com", username, password) + email := e.uuid() + "@test.com" + _, toks := e.createAndSingIn(email, password) httpResp := e.httpRequest( http.MethodPost,

@@ -341,8 +323,7 @@ )

e.Equal(httpResp.Code, http.StatusOK) - userDB := e.getUserByUsername(username) - e.Equal(userDB.Username, username) + userDB := e.getUserByEmail(email) e.NoError(e.hasher.Compare(userDB.Password, newPassword)) }

@@ -357,8 +338,7 @@ )

func (e *AppTestSuite) TestAuthV1_ResetPassword() { email := e.uuid() + "@test.com" - uname := e.uuid() - uid, _ := e.createAndSingIn(email, uname, "password") + uid, _ := e.createAndSingIn(email, "password") httpResp := e.httpRequest( http.MethodPost,

@@ -384,7 +364,7 @@ Password: password,

}), ) - dbUser := e.getUserByUsername(uname) + dbUser := e.getUserByEmail(email) e.Equal(httpResp.Code, http.StatusOK) e.NoError(e.hasher.Compare(dbUser.Password, password))

@@ -393,7 +373,7 @@ e.NotEmpty(token.UsedAt)

} func (e *AppTestSuite) TestAuthV1_ResetPassword_nonExistentUser() { - _, _ = e.createAndSingIn(e.uuid()+"@test.comd", e.uuid(), "password") + _, _ = e.createAndSingIn(e.uuid()+"@test.com", "password") httpResp := e.httpRequest( http.MethodPost, "/api/v1/auth/reset-password",

@@ -405,12 +385,12 @@

e.Equal(httpResp.Code, http.StatusBadRequest) } -// createAndSingIn creates an activated username, logs them in, +// createAndSingIn creates an activated user, logs them in, // and returns their userID along with access and refresh tokens. func (e *AppTestSuite) createAndSingIn( - email, username, password string, + email, password string, ) (uuid.UUID, apiv1AuthSignInResponse) { - uid := e.insertUser(username, email, password, true) + uid := e.insertUser(email, password, true) httpResp := e.httpRequest( http.MethodPost, "/api/v1/auth/signin",
M e2e/apiv1_notes_authorized_test.go

@@ -3,7 +3,7 @@

import "net/http" func (e *AppTestSuite) TestNoteV1_Create_authorized() { - uid, toks := e.createAndSingIn(e.uuid()+"@test.com", e.uuid(), "password") + uid, toks := e.createAndSingIn(e.uuid()+"@test.com", "password") httpResp := e.httpRequest( http.MethodPost, "/api/v1/note",
M e2e/e2e_utils_db_test.go

@@ -10,25 +10,25 @@ "github.com/jackc/pgx/v5"

"github.com/olexsmir/onasty/internal/models" ) -// getUserByUsername queries user from db by it's username -func (e *AppTestSuite) getUserByUsername(username string) models.User { +// getUserByEmail queries user from db by it's email +func (e *AppTestSuite) getUserByEmail(email string) models.User { query, args, err := pgq. - Select("id", "username", "email", "password", "created_at", "last_login_at"). + Select("id", "email", "password", "created_at", "last_login_at"). From("users"). - Where(pgq.Eq{"username": username}). + Where(pgq.Eq{"email": email}). SQL() e.require.NoError(err) var user models.User err = e.postgresDB.QueryRow(e.ctx, query, args...). - Scan(&user.ID, &user.Username, &user.Email, &user.Password, &user.CreatedAt, &user.LastLoginAt) + Scan(&user.ID, &user.Email, &user.Password, &user.CreatedAt, &user.LastLoginAt) e.require.NoError(err) return user } // insertUser inserts user into db -func (e *AppTestSuite) insertUser(uname, email, passwd string, activated ...bool) uuid.UUID { +func (e *AppTestSuite) insertUser(email, passwd string, activated ...bool) uuid.UUID { p, err := e.hasher.Hash(passwd) e.require.NoError(err)

@@ -39,8 +39,8 @@ }

query, args, err := pgq. Insert("users"). - Columns("username", "email", "password", "activated", "created_at", "last_login_at"). - Values(uname, email, p, a, time.Now(), time.Now()). + Columns("email", "password", "activated", "created_at", "last_login_at"). + Values(email, p, a, time.Now(), time.Now()). Returning("id"). SQL() e.require.NoError(err)

@@ -77,7 +77,7 @@

// getLastUserByEmail gets last inserted [models.User] by user's email func (e *AppTestSuite) getLastUserByEmail(em string) models.User { query, args, err := pgq. - Select("id", "username", "activated", "email", "password", "created_at", "last_login_at"). + Select("id", "activated", "email", "password", "created_at", "last_login_at"). From("users"). Where(pgq.Eq{"email": em}). OrderBy("created_at DESC").

@@ -87,7 +87,7 @@ e.require.NoError(err)

var u models.User err = e.postgresDB.QueryRow(e.ctx, query, args...). - Scan(&u.ID, &u.Username, &u.Activated, &u.Email, &u.Password, &u.CreatedAt, &u.LastLoginAt) + Scan(&u.ID, &u.Activated, &u.Email, &u.Password, &u.CreatedAt, &u.LastLoginAt) if errors.Is(err, pgx.ErrNoRows) { return models.User{} //nolint:exhaustruct }
M internal/dtos/user.go

@@ -5,7 +5,6 @@ "time"

) type SignUp struct { - Username string Email string Password string CreatedAt time.Time
M internal/models/user.go

@@ -10,7 +10,6 @@ )

var ( ErrUserEmailIsAlreadyInUse = errors.New("user: email is already in use") - ErrUsernameIsAlreadyInUse = errors.New("user: username is already in use") ErrUserIsAlreadyVerified = errors.New("user: user is already verified") ErrResetPasswordTokenAlreadyUsed = errors.New("reset password token is already used")

@@ -22,12 +21,10 @@ ErrUserWrongCredentials = errors.New("user: wrong credentials")

ErrUserInvalidEmail = errors.New("user: invalid email") ErrUserInvalidPassword = errors.New("user: password too short, minimum 6 chars") - ErrUserInvalidUsername = errors.New("user: username is required") ) type User struct { ID uuid.UUID - Username string Email string Activated bool Password string

@@ -39,10 +36,6 @@ func (u User) Validate() error {

_, err := mail.ParseAddress(u.Email) if err != nil { return ErrUserInvalidEmail - } - - if len(u.Username) == 0 { - return ErrUserInvalidUsername } return u.ValidatePassword()
M internal/models/user_test.go

@@ -11,7 +11,6 @@ tests := []struct {

name string fail bool - username string email string password string }{

@@ -19,43 +18,31 @@ {

name: "valid", fail: false, email: "test@example.org", - username: "iuserarchbtw", password: "superhardasspassword", }, { name: "all fields empty", fail: true, email: "", - username: "", password: "", }, { name: "invalid email", fail: true, email: "test", - username: "iuserarchbtw", password: "superhardasspassword", }, { name: "invalid password", fail: true, email: "test@example.org", - username: "iuserarchbtw", password: "12345", }, - { - name: "invalid username", - fail: true, - email: "test@example.org", - username: "", - password: "superhardasspassword", - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := User{ //nolint:exhaustruct - Username: tt.username, Email: tt.email, Password: tt.password, }.Validate()
M internal/service/usersrv/oauth.go

@@ -4,7 +4,6 @@ import (

"context" "errors" "log/slog" - "strings" "time" "github.com/gofrs/uuid/v5"

@@ -74,11 +73,6 @@

return userInfo, err } -func getUsernameFromEmail(email string) string { - p := strings.Split(email, "@") - return p[0] -} - func (u *UserSrv) getUserByOAuthIDOrCreateOne( ctx context.Context, info oauth.UserInfo,

@@ -88,7 +82,6 @@ if err != nil {

if errors.Is(err, models.ErrUserNotFound) { uid, cerr := u.userstore.Create(ctx, models.User{ ID: uuid.Nil, - Username: getUsernameFromEmail(info.Email), Email: info.Email, Activated: true, Password: "",
M internal/service/usersrv/usersrv.go

@@ -93,23 +93,24 @@ }

} func (u *UserSrv) SignUp(ctx context.Context, inp dtos.SignUp) (uuid.UUID, error) { - hashedPassword, err := u.hasher.Hash(inp.Password) - if err != nil { - return uuid.UUID{}, err - } - user := models.User{ ID: uuid.Nil, // nil, because it does not get used here - Username: inp.Username, Email: inp.Email, Activated: false, - Password: hashedPassword, + Password: inp.Password, CreatedAt: inp.CreatedAt, LastLoginAt: inp.LastLoginAt, } - if err = user.Validate(); err != nil { + if err := user.Validate(); err != nil { return uuid.Nil, err } + + hashedPassword, err := u.hasher.Hash(inp.Password) + if err != nil { + return uuid.UUID{}, err + } + + user.Password = hashedPassword userID, err := u.userstore.Create(ctx, user) if err != nil {
M internal/store/psql/userepo/userepo.go

@@ -51,8 +51,8 @@

func (r *UserRepo) Create(ctx context.Context, inp models.User) (uuid.UUID, error) { query, args, err := pgq. Insert("users"). - Columns("username", "email", "password", "activated", "created_at", "last_login_at"). - Values(inp.Username, inp.Email, inp.Password, inp.Activated, inp.CreatedAt, inp.LastLoginAt). + Columns("email", "password", "activated", "created_at", "last_login_at"). + Values(inp.Email, inp.Password, inp.Activated, inp.CreatedAt, inp.LastLoginAt). Returning("id"). SQL() if err != nil {

@@ -63,10 +63,6 @@ var id uuid.UUID

err = r.db.QueryRow(ctx, query, args...).Scan(&id) // FIXME: somehow this does return errors but i can't errors.Is them in api layer - if psqlutil.IsDuplicateErr(err, "users_username_key") { - return uuid.UUID{}, models.ErrUsernameIsAlreadyInUse - } - if psqlutil.IsDuplicateErr(err, "users_email_key") { return uuid.UUID{}, models.ErrUserEmailIsAlreadyInUse }

@@ -79,7 +75,7 @@ ctx context.Context,

email string, ) (models.User, error) { query, args, err := pgq. - Select("id", "username", "email", "password", "activated", "created_at", "last_login_at"). + Select("id", "email", "password", "activated", "created_at", "last_login_at"). From("users"). Where(pgq.Eq{"email": email}). SQL()

@@ -89,7 +85,7 @@ }

var user models.User err = r.db.QueryRow(ctx, query, args...). - Scan(&user.ID, &user.Username, &user.Email, &user.Password, &user.Activated, &user.CreatedAt, &user.LastLoginAt) + Scan(&user.ID, &user.Email, &user.Password, &user.Activated, &user.CreatedAt, &user.LastLoginAt) if errors.Is(err, pgx.ErrNoRows) { return models.User{}, models.ErrUserNotFound }

@@ -121,7 +117,7 @@ ctx context.Context,

provider, providerID string, ) (models.User, error) { query := `--sql - select u.id, u.username, u.email, u.password, u.activated, u.created_at, u.last_login_at + select u.id, u.email, u.password, u.activated, u.created_at, u.last_login_at from users u join oauth_identities oi on u.id = oi.user_id where oi.provider = $1

@@ -130,7 +126,7 @@ limit 1`

var user models.User err := r.db.QueryRow(ctx, query, provider, providerID). - Scan(&user.ID, &user.Username, &user.Email, &user.Password, &user.Activated, &user.CreatedAt, &user.LastLoginAt) + Scan(&user.ID, &user.Email, &user.Password, &user.Activated, &user.CreatedAt, &user.LastLoginAt) if errors.Is(err, pgx.ErrNoRows) { return models.User{}, models.ErrUserNotFound }
M internal/transport/http/apiv1/auth.go

@@ -9,7 +9,6 @@ "github.com/olexsmir/onasty/internal/dtos"

) type signUpRequest struct { - Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` }

@@ -22,7 +21,6 @@ return

} if _, err := a.usersrv.SignUp(c.Request.Context(), dtos.SignUp{ - Username: req.Username, Email: req.Email, Password: req.Password, CreatedAt: time.Now(),
M internal/transport/http/apiv1/response.go

@@ -21,12 +21,10 @@ if errors.Is(err, usersrv.ErrProviderNotSupported) ||

errors.Is(err, models.ErrResetPasswordTokenAlreadyUsed) || errors.Is(err, models.ErrResetPasswordTokenExpired) || errors.Is(err, models.ErrUserEmailIsAlreadyInUse) || - errors.Is(err, models.ErrUsernameIsAlreadyInUse) || errors.Is(err, models.ErrUserIsAlreadyVerified) || errors.Is(err, models.ErrUserIsNotActivated) || errors.Is(err, models.ErrUserInvalidEmail) || errors.Is(err, models.ErrUserInvalidPassword) || - errors.Is(err, models.ErrUserInvalidUsername) || // notes errors.Is(err, models.ErrNoteContentIsEmpty) || errors.Is(err, models.ErrNoteSlugIsAlreadyInUse) {
A migrations/20250520211029_remove_username.down.sql

@@ -0,0 +1,2 @@

+ALTER TABLE users + add column username varchar(255) NOT NULL UNIQUE;
A migrations/20250520211029_remove_username.up.sql

@@ -0,0 +1,2 @@

+ALTER TABLE users + DROP COLUMN username;