NOISSUE - Add Auth provider in profile view response (#3187)

Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
Co-authored-by: nyagamunene <stevenyaga2014@gmail.com>
This commit is contained in:
Arvindh
2025-10-15 17:47:37 +05:30
committed by GitHub
parent 5833bafbee
commit 27995cb093
9 changed files with 251 additions and 43 deletions
+4 -1
View File
@@ -196,7 +196,10 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
errors.Contains(err, errors.ErrUsernameNotAvailable),
errors.Contains(err, errors.ErrRouteNotAvailable),
errors.Contains(err, errors.ErrChannelRouteNotAvailable),
errors.Contains(err, errors.ErrDomainRouteNotAvailable):
errors.Contains(err, errors.ErrDomainRouteNotAvailable),
errors.Contains(err, svcerr.ErrExternalAuthProviderCouldNotChangePassword),
errors.Contains(err, svcerr.ErrExternalAuthProviderCouldNotResetPassword),
errors.Contains(err, svcerr.ErrExternalAuthProviderCouldNotUpdate):
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(err, svcerr.ErrAuthorization),
errors.Contains(err, svcerr.ErrDomainAuthorization),
+9
View File
@@ -102,4 +102,13 @@ var (
// ErrUserVerificationExpired indicates user verification is expired.
ErrUserVerificationExpired = errors.New("verification expired, please generate new verification")
// ErrExternalAuthProviderCouldNotUpdate indicates that users authenticated via external provider cannot update their account details directly.
ErrExternalAuthProviderCouldNotUpdate = errors.New("account details can only be updated through your authentication provider's settings")
// ErrExternalAuthProviderCouldNotResetPassword indicates that password cannot be reset for users authenticated via external provider.
ErrExternalAuthProviderCouldNotResetPassword = errors.New("password cannot be reset for users authenticated via external provider")
// ErrExternalAuthProviderCouldNotChangePassword indicates that password cannot be reset for users authenticated via external provider.
ErrExternalAuthProviderCouldNotChangePassword = errors.New("password cannot be reset for users authenticated via external provider")
)
+4
View File
@@ -575,6 +575,10 @@ func oauth2CallbackHandler(oauth oauth2.Provider, svc users.Service, tokenClient
return
}
user.AuthProvider = oauth.Name()
if user.AuthProvider == "" {
user.AuthProvider = "oauth"
}
user, err = svc.OAuthCallback(r.Context(), user)
if err != nil {
http.Redirect(w, r, oauth.ErrorURL()+"?error="+err.Error(), http.StatusSeeOther)
+9
View File
@@ -127,6 +127,15 @@ func Migration() *migrate.MemoryMigrationSource {
`ALTER TABLE users RENAME CONSTRAINT clients_email_key TO clients_identity_key;`,
},
},
{
Id: "clients_09",
Up: []string{
`ALTER TABLE users ADD COLUMN auth_provider VARCHAR(254);`,
},
Down: []string{
`ALTER TABLE users DROP COLUMN auth_provider`,
},
},
},
}
}
+19 -6
View File
@@ -34,9 +34,9 @@ func NewRepository(db postgres.Database) users.Repository {
}
func (repo *userRepo) Save(ctx context.Context, c users.User) (users.User, error) {
q := `INSERT INTO users (id, tags, email, secret, metadata, created_at, status, role, first_name, last_name, username, profile_picture)
VALUES (:id, :tags, :email, :secret, :metadata, :created_at, :status, :role, :first_name, :last_name, :username, :profile_picture)
RETURNING id, tags, email, metadata, created_at, status, role, first_name, last_name, username, profile_picture, verified_at`
q := `INSERT INTO users (id, tags, email, secret, metadata, created_at, status, role, first_name, last_name, username, profile_picture, auth_provider)
VALUES (:id, :tags, :email, :secret, :metadata, :created_at, :status, :role, :first_name, :last_name, :username, :profile_picture, :auth_provider)
RETURNING id, tags, email, metadata, created_at, status, role, first_name, last_name, username, profile_picture, verified_at, auth_provider`
dbu, err := toDBUser(c)
if err != nil {
@@ -96,7 +96,7 @@ func (repo *userRepo) CheckSuperAdmin(ctx context.Context, adminID string) error
}
func (repo *userRepo) RetrieveByID(ctx context.Context, id string) (users.User, error) {
q := `SELECT id, tags, email, secret, metadata, created_at, updated_at, updated_by, status, role, first_name, last_name, username, profile_picture, verified_at
q := `SELECT id, tags, email, secret, metadata, created_at, updated_at, updated_by, status, role, first_name, last_name, username, profile_picture, verified_at, auth_provider
FROM users WHERE id = :id`
dbu := DBUser{
@@ -431,7 +431,7 @@ func (repo *userRepo) RetrieveAllByIDs(ctx context.Context, pm users.Page) (user
}
func (repo *userRepo) RetrieveByEmail(ctx context.Context, email string) (users.User, error) {
q := `SELECT id, tags, email, secret, metadata, created_at, updated_at, updated_by, status, role, first_name, last_name, username, verified_at
q := `SELECT id, tags, email, secret, metadata, created_at, updated_at, updated_by, status, role, first_name, last_name, username, verified_at, auth_provider
FROM users WHERE email = :email AND status = :status`
dbu := DBUser{
@@ -458,7 +458,7 @@ func (repo *userRepo) RetrieveByEmail(ctx context.Context, email string) (users.
}
func (repo *userRepo) RetrieveByUsername(ctx context.Context, username string) (users.User, error) {
q := `SELECT id, tags, email, secret, metadata, created_at, updated_at, updated_by, status, role, first_name, last_name, username, verified_at
q := `SELECT id, tags, email, secret, metadata, created_at, updated_at, updated_by, status, role, first_name, last_name, username, verified_at, auth_provider
FROM users WHERE username = :username AND status = :status`
dbu := DBUser{
@@ -502,6 +502,7 @@ type DBUser struct {
ProfilePicture sql.NullString `db:"profile_picture, omitempty"`
Email string `db:"email,omitempty"`
VerifiedAt sql.NullTime `db:"verified_at,omitempty"`
AuthProvider sql.NullString `db:"auth_provider,omitempty"`
}
func toDBUser(u users.User) (DBUser, error) {
@@ -530,6 +531,11 @@ func toDBUser(u users.User) (DBUser, error) {
verifiedAt = sql.NullTime{Time: u.VerifiedAt, Valid: true}
}
var authProvider sql.NullString
if u.AuthProvider != "" {
authProvider = sql.NullString{String: u.AuthProvider, Valid: true}
}
return DBUser{
ID: u.ID,
Tags: tags,
@@ -546,6 +552,7 @@ func toDBUser(u users.User) (DBUser, error) {
ProfilePicture: stringToNullString(u.ProfilePicture),
Email: u.Email,
VerifiedAt: verifiedAt,
AuthProvider: authProvider,
}, nil
}
@@ -573,6 +580,11 @@ func ToUser(dbu DBUser) (users.User, error) {
verifiedAt = dbu.VerifiedAt.Time.UTC()
}
var authProvider string
if dbu.AuthProvider.Valid {
authProvider = dbu.AuthProvider.String
}
user := users.User{
ID: dbu.ID,
FirstName: nullStringString(dbu.FirstName),
@@ -590,6 +602,7 @@ func ToUser(dbu DBUser) (users.User, error) {
Tags: tags,
ProfilePicture: nullStringString(dbu.ProfilePicture),
VerifiedAt: verifiedAt,
AuthProvider: authProvider,
}
if dbu.Role != nil {
user.Role = *dbu.Role
+45 -1
View File
@@ -45,6 +45,17 @@ func TestUsersSave(t *testing.T) {
email := first_name + "@example.com"
externalUser := users.User{
ID: testsutil.GenerateUUID(t),
FirstName: namesgen.Generate(),
LastName: namesgen.Generate(),
Metadata: users.Metadata{},
Credentials: users.Credentials{
Username: namesgen.Generate(),
},
Email: namesgen.Generate() + "@example.com",
AuthProvider: "external",
}
cases := []struct {
desc string
user users.User
@@ -66,6 +77,12 @@ func TestUsersSave(t *testing.T) {
},
err: nil,
},
{
desc: "add new external user successfully",
user: externalUser,
err: nil,
},
{
desc: "add user with duplicate user email",
user: users.User{
@@ -272,14 +289,38 @@ func TestRetrieveByID(t *testing.T) {
_, err := repo.Save(context.Background(), user)
require.Nil(t, err, fmt.Sprintf("failed to save users %s", user.ID))
externalUser := users.User{
ID: testsutil.GenerateUUID(t),
FirstName: namesgen.Generate(),
LastName: namesgen.Generate(),
Metadata: users.Metadata{},
Credentials: users.Credentials{
Username: namesgen.Generate(),
},
Email: namesgen.Generate() + "@example.com",
AuthProvider: "external",
}
_, err = repo.Save(context.Background(), externalUser)
require.Nil(t, err, fmt.Sprintf("failed to save users %s", user.ID))
cases := []struct {
desc string
userID string
user users.User
err error
}{
{
desc: "retrieve existing user",
userID: user.ID,
user: user,
err: nil,
},
{
desc: "retrieve existing oauth user",
userID: externalUser.ID,
user: externalUser,
err: nil,
},
{
@@ -295,8 +336,11 @@ func TestRetrieveByID(t *testing.T) {
}
for _, tc := range cases {
_, err := repo.RetrieveByID(context.Background(), tc.userID)
rUser, err := repo.RetrieveByID(context.Background(), tc.userID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.err, err))
if err == nil {
assert.Equal(t, tc.user, rUser, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.user, rUser))
}
}
}
+29
View File
@@ -294,6 +294,15 @@ func (svc service) Update(ctx context.Context, session authn.Session, id string,
return User{}, err
}
}
u, err := svc.users.RetrieveByID(ctx, id)
if err != nil {
return User{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
}
if u.AuthProvider != "" {
if usr.FirstName != nil || usr.LastName != nil || usr.ProfilePicture != nil {
return User{}, svcerr.ErrExternalAuthProviderCouldNotUpdate
}
}
updatedAt := time.Now().UTC()
usr.UpdatedAt = &updatedAt
usr.UpdatedBy = &session.UserID
@@ -331,6 +340,14 @@ func (svc service) UpdateProfilePicture(ctx context.Context, session authn.Sessi
}
}
u, err := svc.users.RetrieveByID(ctx, id)
if err != nil {
return User{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
}
if u.AuthProvider != "" {
return User{}, svcerr.ErrExternalAuthProviderCouldNotUpdate
}
updatedAt := time.Now().UTC()
usr.UpdatedAt = &updatedAt
usr.UpdatedBy = &session.UserID
@@ -353,6 +370,9 @@ func (svc service) UpdateEmail(ctx context.Context, session authn.Session, userI
if err != nil {
return User{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
}
if oldUsr.AuthProvider != "" {
return User{}, svcerr.ErrExternalAuthProviderCouldNotUpdate
}
if oldUsr.Email == email {
return User{}, fmt.Errorf("current email is same as update requested email")
}
@@ -377,6 +397,9 @@ func (svc service) SendPasswordReset(ctx context.Context, email string) error {
if err != nil {
return errors.Wrap(svcerr.ErrViewEntity, err)
}
if user.AuthProvider != "" {
return svcerr.ErrExternalAuthProviderCouldNotResetPassword
}
issueReq := &grpcTokenV1.IssueReq{
UserId: user.ID,
UserRole: uint32(user.Role + 1),
@@ -395,6 +418,9 @@ func (svc service) ResetSecret(ctx context.Context, session authn.Session, secre
if err != nil {
return errors.Wrap(svcerr.ErrViewEntity, err)
}
if u.AuthProvider != "" {
return svcerr.ErrExternalAuthProviderCouldNotResetPassword
}
secret, err = svc.hasher.Hash(secret)
if err != nil {
@@ -420,6 +446,9 @@ func (svc service) UpdateSecret(ctx context.Context, session authn.Session, oldS
if err != nil {
return User{}, errors.Wrap(svcerr.ErrViewEntity, err)
}
if dbUser.AuthProvider != "" {
return User{}, svcerr.ErrExternalAuthProviderCouldNotChangePassword
}
if _, err := svc.IssueToken(ctx, dbUser.Credentials.Username, oldSecret); err != nil {
return User{}, err
}
+131 -35
View File
@@ -519,6 +519,8 @@ func TestUpdateUser(t *testing.T) {
userReq users.UserReq
session authn.Session
updateResponse users.User
retrieveByIDResp users.User
retrieveByIDErr error
token string
updateErr error
checkSuperAdminErr error
@@ -530,10 +532,11 @@ func TestUpdateUser(t *testing.T) {
userReq: users.UserReq{
FirstName: &updateFirstName,
},
session: authn.Session{UserID: user1.ID},
updateResponse: user1,
token: validToken,
err: nil,
session: authn.Session{UserID: user1.ID},
updateResponse: user1,
retrieveByIDResp: user1,
token: validToken,
err: nil,
},
{
desc: "update metadata successfully as normal user",
@@ -541,10 +544,11 @@ func TestUpdateUser(t *testing.T) {
userReq: users.UserReq{
Metadata: &updatedMetadata,
},
session: authn.Session{UserID: user2.ID},
updateResponse: user2,
token: validToken,
err: nil,
session: authn.Session{UserID: user2.ID},
updateResponse: user2,
retrieveByIDResp: user2,
token: validToken,
err: nil,
},
{
desc: "update user name as normal user with repo error on update",
@@ -552,11 +556,12 @@ func TestUpdateUser(t *testing.T) {
userReq: users.UserReq{
FirstName: &updateFirstName,
},
session: authn.Session{UserID: user1.ID},
updateResponse: users.User{},
token: validToken,
updateErr: errors.ErrMalformedEntity,
err: svcerr.ErrUpdateEntity,
session: authn.Session{UserID: user1.ID},
updateResponse: users.User{},
retrieveByIDResp: user1,
token: validToken,
updateErr: errors.ErrMalformedEntity,
err: svcerr.ErrUpdateEntity,
},
{
desc: "update user name as admin successfully",
@@ -564,10 +569,11 @@ func TestUpdateUser(t *testing.T) {
userReq: users.UserReq{
FirstName: &updateFirstName,
},
session: authn.Session{UserID: adminID, SuperAdmin: true},
updateResponse: user1,
token: validToken,
err: nil,
session: authn.Session{UserID: adminID, SuperAdmin: true},
updateResponse: user1,
retrieveByIDResp: user1,
token: validToken,
err: nil,
},
{
desc: "update user metadata as admin successfully",
@@ -575,10 +581,11 @@ func TestUpdateUser(t *testing.T) {
userReq: users.UserReq{
Metadata: &updatedMetadata,
},
session: authn.Session{UserID: adminID, SuperAdmin: true},
updateResponse: user2,
token: validToken,
err: nil,
session: authn.Session{UserID: adminID, SuperAdmin: true},
updateResponse: user2,
retrieveByIDResp: user2,
token: validToken,
err: nil,
},
{
desc: "update user with failed check on super admin",
@@ -597,26 +604,88 @@ func TestUpdateUser(t *testing.T) {
userReq: users.UserReq{
FirstName: &updateFirstName,
},
session: authn.Session{UserID: adminID, SuperAdmin: true},
updateResponse: users.User{},
token: validToken,
updateErr: errors.ErrMalformedEntity,
err: svcerr.ErrUpdateEntity,
session: authn.Session{UserID: adminID, SuperAdmin: true},
updateResponse: users.User{},
retrieveByIDResp: user1,
token: validToken,
updateErr: errors.ErrMalformedEntity,
err: svcerr.ErrUpdateEntity,
},
{
desc: "update user first name with external auth provider should fail",
userID: user1.ID,
userReq: users.UserReq{
FirstName: &updateFirstName,
},
session: authn.Session{UserID: user1.ID},
retrieveByIDResp: users.User{
ID: user1.ID,
AuthProvider: "google",
},
token: validToken,
err: svcerr.ErrExternalAuthProviderCouldNotUpdate,
},
{
desc: "update user last name with external auth provider should fail",
userID: user1.ID,
userReq: users.UserReq{
LastName: &updateFirstName,
},
session: authn.Session{UserID: user1.ID},
retrieveByIDResp: users.User{
ID: user1.ID,
AuthProvider: "google",
},
token: validToken,
err: svcerr.ErrExternalAuthProviderCouldNotUpdate,
},
{
desc: "update user metadata with external auth provider should succeed",
userID: user2.ID,
userReq: users.UserReq{
Metadata: &updatedMetadata,
},
session: authn.Session{UserID: user2.ID},
retrieveByIDResp: users.User{
ID: user2.ID,
AuthProvider: "google",
Metadata: updatedMetadata,
},
updateResponse: users.User{
ID: user2.ID,
AuthProvider: "google",
Metadata: updatedMetadata,
},
token: validToken,
err: nil,
},
{
desc: "update user with retrieve by id error",
userID: user1.ID,
userReq: users.UserReq{
FirstName: &updateFirstName,
},
session: authn.Session{UserID: user1.ID},
retrieveByIDErr: repoerr.ErrNotFound,
token: validToken,
err: svcerr.ErrUpdateEntity,
},
}
for _, tc := range cases {
repoCall := cRepo.On("CheckSuperAdmin", context.Background(), mock.Anything).Return(tc.checkSuperAdminErr)
repoCall1 := cRepo.On("Update", context.Background(), tc.userID, mock.Anything).Return(tc.updateResponse, tc.err)
repoCall1 := cRepo.On("RetrieveByID", context.Background(), tc.userID).Return(tc.retrieveByIDResp, tc.retrieveByIDErr)
repoCall2 := cRepo.On("Update", context.Background(), tc.userID, mock.Anything).Return(tc.updateResponse, tc.updateErr)
updatedUser, err := svc.Update(context.Background(), tc.session, tc.userID, tc.userReq)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
assert.Equal(t, tc.updateResponse, updatedUser, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.updateResponse, updatedUser))
if tc.err == nil {
ok := repoCall1.Parent.AssertCalled(t, "Update", context.Background(), tc.userID, mock.Anything)
ok := repoCall2.Parent.AssertCalled(t, "Update", context.Background(), tc.userID, mock.Anything)
assert.True(t, ok, fmt.Sprintf("Update was not called on %s", tc.desc))
}
repoCall.Unset()
repoCall1.Unset()
repoCall2.Unset()
}
}
@@ -1010,6 +1079,8 @@ func TestUpdateProfilePicture(t *testing.T) {
userReq users.UserReq
session authn.Session
updateProfilePicResponse users.User
retrieveByIDResp users.User
retrieveByIDErr error
updateProfilePicErr error
checkSuperAdminErr error
err error
@@ -1020,6 +1091,7 @@ func TestUpdateProfilePicture(t *testing.T) {
userReq: users.UserReq{ProfilePicture: &updatedPicture},
session: authn.Session{UserID: user.ID},
updateProfilePicResponse: user,
retrieveByIDResp: user,
err: nil,
},
{
@@ -1028,15 +1100,17 @@ func TestUpdateProfilePicture(t *testing.T) {
userReq: users.UserReq{ProfilePicture: &updatedPicture},
session: authn.Session{UserID: user.ID},
updateProfilePicResponse: users.User{},
retrieveByIDResp: user,
updateProfilePicErr: errors.ErrMalformedEntity,
err: svcerr.ErrUpdateEntity,
},
{
desc: "update profile picture as admin successfully",
userID: user.ID,
userReq: users.UserReq{ProfilePicture: &updatedPicture},
session: authn.Session{UserID: adminID, SuperAdmin: true},
err: nil,
desc: "update profile picture as admin successfully",
userID: user.ID,
userReq: users.UserReq{ProfilePicture: &updatedPicture},
session: authn.Session{UserID: adminID, SuperAdmin: true},
retrieveByIDResp: user,
err: nil,
},
{
desc: "update profile picture as admin with failed check on super admin",
@@ -1052,23 +1126,45 @@ func TestUpdateProfilePicture(t *testing.T) {
userReq: users.UserReq{ProfilePicture: &updatedPicture},
session: authn.Session{UserID: adminID, SuperAdmin: true},
updateProfilePicResponse: users.User{},
retrieveByIDResp: user,
updateProfilePicErr: errors.ErrMalformedEntity,
err: svcerr.ErrUpdateEntity,
},
{
desc: "update profile picture with external auth provider",
userID: user.ID,
userReq: users.UserReq{ProfilePicture: &updatedPicture},
session: authn.Session{UserID: user.ID},
retrieveByIDResp: users.User{
ID: user.ID,
AuthProvider: "google",
},
err: svcerr.ErrExternalAuthProviderCouldNotUpdate,
},
{
desc: "update profile picture with retrieve by id error",
userID: user.ID,
userReq: users.UserReq{ProfilePicture: &updatedPicture},
session: authn.Session{UserID: user.ID},
retrieveByIDErr: repoerr.ErrNotFound,
err: svcerr.ErrUpdateEntity,
},
}
for _, tc := range cases {
repoCall := cRepo.On("CheckSuperAdmin", context.Background(), mock.Anything).Return(tc.checkSuperAdminErr)
repoCall1 := cRepo.On("Update", context.Background(), tc.userID, mock.Anything).Return(tc.updateProfilePicResponse, tc.updateProfilePicErr)
repoCall1 := cRepo.On("RetrieveByID", context.Background(), tc.userID).Return(tc.retrieveByIDResp, tc.retrieveByIDErr)
repoCall2 := cRepo.On("Update", context.Background(), tc.userID, mock.Anything).Return(tc.updateProfilePicResponse, tc.updateProfilePicErr)
updatedUser, err := svc.UpdateProfilePicture(context.Background(), tc.session, tc.userID, tc.userReq)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
assert.Equal(t, tc.updateProfilePicResponse, updatedUser, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.updateProfilePicResponse, updatedUser))
if tc.err == nil {
ok := repoCall1.Parent.AssertCalled(t, "Update", context.Background(), tc.userID, mock.Anything)
ok := repoCall2.Parent.AssertCalled(t, "Update", context.Background(), tc.userID, mock.Anything)
assert.True(t, ok, fmt.Sprintf("Update was not called on %s", tc.desc))
}
repoCall.Unset()
repoCall1.Unset()
repoCall2.Unset()
}
}
+1
View File
@@ -30,6 +30,7 @@ type User struct {
UpdatedAt time.Time `json:"updated_at,omitempty"`
UpdatedBy string `json:"updated_by,omitempty"`
VerifiedAt time.Time `json:"verified_at,omitempty"`
AuthProvider string `json:"auth_provider,omitempty"`
}
type Credentials struct {