Skip to content

Conversation

@arvindh123
Copy link
Contributor

@arvindh123 arvindh123 commented Aug 29, 2025

What type of PR is this?

What does this do?

Which issue(s) does this PR fix/relate to?

Have you included tests for your changes?

Did you document any new/modified feature?

Notes

@arvindh123 arvindh123 requested a review from a team as a code owner August 29, 2025 10:19
Copy link
Collaborator

@dborovcanin dborovcanin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is failing.

@arvindh123 arvindh123 force-pushed the user-verification branch 2 times, most recently from 1bdf4a4 to fbcb871 Compare September 2, 2025 09:21
@codecov
Copy link

codecov bot commented Sep 2, 2025

Codecov Report

❌ Patch coverage is 60.31519% with 277 lines in your changes missing coverage. Please review.
✅ Project coverage is 43.81%. Comparing base (2261691) to head (30bf733).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pkg/sdk/mocks/sdk.go 0.00% 72 Missing ⚠️
users/verification.go 45.90% 22 Missing and 11 partials ⚠️
cli/users.go 0.00% 26 Missing ⚠️
users/events/streams.go 0.00% 26 Missing ⚠️
users/emailer/emailer.go 0.00% 18 Missing ⚠️
users/postgres/users.go 68.51% 12 Missing and 5 partials ⚠️
users/service.go 81.17% 11 Missing and 5 partials ⚠️
users/events/events.go 0.00% 15 Missing ⚠️
pkg/sdk/users.go 0.00% 14 Missing ⚠️
users/postgres/verfications.go 86.51% 8 Missing and 4 partials ⚠️
... and 5 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3101      +/-   ##
==========================================
+ Coverage   35.15%   43.81%   +8.65%     
==========================================
  Files         323      212     -111     
  Lines       47509    35520   -11989     
==========================================
- Hits        16704    15562    -1142     
+ Misses      29977    19206   -10771     
+ Partials      828      752      -76     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

users/service.go Outdated

const (
verificationTokenExpiryDuration = 24 * time.Hour
verificationTokenSeparator = "."
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove if unused.

users/service.go Outdated
"github.com/absmach/supermq/pkg/policies"
)

const (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once you remove unused const, make this inline and rename const verificationTokenDuration.

Comment on lines 108 to 99
if err != nil {
if err == repoerr.ErrNotFound {
return nil
}
return err
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for nested if:

if err == repoerr.ErrNotFound {
    return nil
}
if err != nil {
    return err
}

Why do we return nil in case of notFound?

users/service.go Outdated
}

token, err := svc.token.Issue(ctx, &grpcTokenV1.IssueReq{UserId: dbUser.ID, UserRole: uint32(dbUser.Role + 1), Type: uint32(smqauth.AccessKey)})
token, err := svc.token.Issue(ctx, &grpcTokenV1.IssueReq{UserId: dbUser.ID, UserRole: uint32(dbUser.Role + 1), Type: uint32(smqauth.AccessKey), Verified: dbUser.Verified})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR, but Role+1 is too cryptic.

users/service.go Outdated
Comment on lines 669 to 708

type tokenPayload struct {
UserID string `json:"user_id"`
Email string `json:"email"`
Token string `json:"token"`
}

func generateVerificationToken(userID, email string) (string, error) {
randomBytes := make([]byte, 32)
if _, err := rand.Read(randomBytes); err != nil {
return "", errors.Wrap(errFailedToEncodeToken, err)
}

payload := tokenPayload{
UserID: userID,
Email: email,
Token: base64.URLEncoding.EncodeToString(randomBytes),
}

jsonBytes, err := json.Marshal(payload)
if err != nil {
return "", errors.Wrap(errFailedToEncodeToken, err)
}

return base64.URLEncoding.EncodeToString(jsonBytes), nil
}

func getUserIDFromToken(token string) (tokenPayload, error) {
decodedPayload, err := base64.URLEncoding.DecodeString(token)
if err != nil {
return tokenPayload{}, errors.Wrap(errFailedToDecodeToken, err)
}

var payload tokenPayload
if err := json.Unmarshal(decodedPayload, &payload); err != nil {
return tokenPayload{}, errInvalidTokenFormat
}

return payload, nil
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to a separate file.

users/users.go Outdated
Comment on lines 41 to 42
if u.VerificationTokenExpiresAt.IsZero() {
return true
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check if verificationToken is empty first?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will think about this, What we should return if token is empty ?

users/users.go Outdated
UpdateToUserVerificationDetails(ctx context.Context, user User) (User, error)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update instead of UpdateTo.


UpdateEmail(ctx context.Context, user User) (User, error)

UpdateRole(ctx context.Context, user User) (User, error)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add godoc comments.

Comment on lines 176 to 177

SendVerification(ctx context.Context, session authn.Session) error

VerifyEmail(ctx context.Context, verificationToken string) (User, error)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, add godoc.

policyCall := policies.On("AddPolicy", context.Background(), mock.Anything).Return(tc.addPolicyErr)
policyCall1 := policies.On("DeletePolicyFilter", context.Background(), mock.Anything).Return(tc.deletePolicyErr)
repoCall1 := cRepo.On("Update", context.Background(), mock.Anything, mock.Anything).Return(tc.updateRoleResponse, tc.updateRoleErr)
repoCall1 := cRepo.On("UpdateRole", context.Background(), mock.Anything).Return(tc.updateRoleResponse, tc.updateRoleErr)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are missing tests for the added methods. Either do it now or open a ticket for the follow-up PR, since this one is already large.

Id: "clients_07",
Up: []string{
`ALTER TABLE users
ADD COLUMN verified BOOLEAN DEFAULT FALSE,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this redundancy? Why don't we just use verified_at as nullable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. We use a verified (boolean) field in JWTs and for other condition checks. Relying only on verified_at would make the logic less clear.

For example, if we only had verified_at, we’d need to write something like:

verified: !verified_at.IsZero()

By keeping a verified field, the code is cleaner and easier to understand.

Comment on lines +75 to +87
r.Post("/tokens/refresh", otelhttp.NewHandler(kithttp.NewServer(
refreshTokenEndpoint(svc),
decodeRefreshToken,
api.EncodeResponse,
opts...,
), "refresh_token").ServeHTTP)
r.Patch("/{id}/email", otelhttp.NewHandler(kithttp.NewServer(
updateEmailEndpoint(svc),
decodeUpdateUserEmail,
api.EncodeResponse,
opts...,
), "update_user_email").ServeHTTP)
})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this allowed for unverified user?

Copy link
Contributor Author

@arvindh123 arvindh123 Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Change Email Endpoint:
    If a user enters the wrong email address during signup, they must have the ability to correct it. Allowing unverified users to change their email ensures they can complete the verification process successfully.

  2. Refresh Token Endpoint:
    The refresh token enables users to request new access tokens regardless of their verification status. Both access and refresh tokens include a verified field. Before verification, this field is false. Once the user completes verification and refreshes their token, the newly issued tokens will correctly reflect verified = true

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Change Email Endpoint:
    If a user enters the wrong email address during signup, they must have the ability to correct it. Allowing unverified users to change their email ensures they can complete the verification process successfully.

No need -> they can restart the procedure process with the correct email.

  1. Refresh Token Endpoint:
    The refresh token enables users to request new access tokens regardless of their verification status. Both access and refresh tokens include a verified field. Before verification, this field is false. Once the user completes verification and refreshes their token, the newly issued tokens will correctly reflect verified = true

Why? Isn't it simpler to disable issuing tokens to who don't have VerifiedAt field?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need -> they can restart the procedure process with the correct email.

  • A username cannot be reused until the associated account is deleted.
  • This ensures usernames can be properly recycled.
  • It helps prevent the buildup of unwanted or unverified user accounts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can provide some basic operation without verification of email
This provides flexibility.

During this basic operation time, If user want to reset password, then they need to verify the email.

Copy link
Contributor

@felixgateru felixgateru left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

@dborovcanin
Copy link
Collaborator

CI is failing.

Down: []string{
`ALTER TABLE users
DROP COLUMN verified_at,
DROP COLUMN verification_token,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the token needed at all? Can we put user ID in JWT we send for verification?

`ALTER TABLE users
DROP COLUMN verified_at,
DROP COLUMN verification_token,
DROP COLUMN verification_token_expires_at;`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed if we are going to use JWT that already contains the expiration?

felixgateru
felixgateru previously approved these changes Sep 4, 2025
email VARCHAR(254) NOT NULL,
otp VARCHAR(255),
created_at TIMESTAMPTZ,
expiry_at TIMESTAMPTZ,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use expires_at since we used created_at.

users/service.go Outdated
return User{}, errors.Wrap(svcerr.ErrMalformedEntity, err)
}

oguv, err := svc.users.RetrieveUserVerification(ctx, ruv.UserID, ruv.Email)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oguv?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oguv - Original (OG) User Verification

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both ruvs and oguv are not descriptive names. Please update. Rename ruvs to user; and user to updatedUser or just updated. Rename ruvs to token, and ruv := UserVerification to uv := userVerification. Especially since this does not have to be registration (r) related.

@@ -0,0 +1,131 @@
// Copyright (c) Abstract Machines
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename to verifications, no need for users prefix.

Copy link
Collaborator

@dborovcanin dborovcanin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is failing.

Copy link
Contributor

@smithjilks smithjilks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arvindh123
I get no token/invalid token when I call sdk verify email method with the token gotten from link query params.

Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: Arvindh <arvindh91@gmail.com>
felixgateru
felixgateru previously approved these changes Sep 5, 2025
Copy link
Contributor

@smithjilks smithjilks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image You can update the template so that it says welcome to 'host' instead of 'host/endpoint'

smithjilks
smithjilks previously approved these changes Sep 5, 2025
users/service.go Outdated
return User{}, errors.Wrap(svcerr.ErrMalformedEntity, err)
}

oguv, err := svc.users.RetrieveUserVerification(ctx, ruv.UserID, ruv.Email)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both ruvs and oguv are not descriptive names. Please update. Rename ruvs to user; and user to updatedUser or just updated. Rename ruvs to token, and ruv := UserVerification to uv := userVerification. Especially since this does not have to be registration (r) related.


// MakeHandler returns a HTTP handler for Groups API endpoints.
func MakeHandler(svc groups.Service, authn authn.Authentication, mux *chi.Mux, logger *slog.Logger, instanceID string, idp supermq.IDProvider) *chi.Mux {
func MakeHandler(svc groups.Service, authnMW authn.AuthNMiddleware, mux *chi.Mux, logger *slog.Logger, instanceID string, idp supermq.IDProvider) *chi.Mux {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just use authn for this variable.

Signed-off-by: Arvindh <arvindh91@gmail.com>
@arvindh123 arvindh123 dismissed stale reviews from smithjilks and felixgateru via 2de1dd1 September 5, 2025 12:36
Signed-off-by: Arvindh <arvindh91@gmail.com>
users/service.go Outdated
var ruv UserVerification
if err := ruv.Decode(ruvs); err != nil {
func (svc service) VerifyEmail(ctx context.Context, token string) (User, error) {
var uv UserVerification
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use received and stored instead, so we have stored.Match(received).

Signed-off-by: Arvindh <arvindh91@gmail.com>
@dborovcanin dborovcanin merged commit e57ad79 into absmach:main Sep 5, 2025
8 of 9 checks passed
@dborovcanin dborovcanin deleted the user-verification branch September 5, 2025 13:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants