feat(auth): support unbinding third-party identities
This commit is contained in:
@@ -2735,6 +2735,10 @@ func (r *oauthPendingFlowUserRepo) ListUserAuthIdentities(ctx context.Context, u
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (r *oauthPendingFlowUserRepo) UnbindUserAuthProvider(context.Context, int64, string) error {
|
||||
panic("unexpected UnbindUserAuthProvider call")
|
||||
}
|
||||
|
||||
func (r *oauthPendingFlowUserRepo) UpdateTotpSecret(ctx context.Context, userID int64, encryptedSecret *string) error {
|
||||
update := r.client.User.UpdateOneID(userID)
|
||||
if encryptedSecret == nil {
|
||||
|
||||
@@ -240,6 +240,34 @@ func (h *UserHandler) BindEmailIdentity(c *gin.Context) {
|
||||
response.Success(c, profileResp)
|
||||
}
|
||||
|
||||
// UnbindIdentity removes a third-party sign-in provider from the current user.
|
||||
// DELETE /api/v1/user/account-bindings/:provider
|
||||
func (h *UserHandler) UnbindIdentity(c *gin.Context) {
|
||||
subject, ok := middleware2.GetAuthSubjectFromContext(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "User not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
updatedUser, err := h.userService.UnbindUserAuthProvider(
|
||||
c.Request.Context(),
|
||||
subject.UserID,
|
||||
c.Param("provider"),
|
||||
)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
profileResp, err := h.buildUserProfileResponse(c.Request.Context(), subject.UserID, updatedUser)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, profileResp)
|
||||
}
|
||||
|
||||
// SendEmailBindingCode sends a verification code for the current user's email binding flow.
|
||||
// POST /api/v1/user/account-bindings/email/send-code
|
||||
func (h *UserHandler) SendEmailBindingCode(c *gin.Context) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
type userHandlerRepoStub struct {
|
||||
user *service.User
|
||||
identities []service.UserAuthIdentityRecord
|
||||
unbound []string
|
||||
}
|
||||
|
||||
func (s *userHandlerRepoStub) Create(context.Context, *service.User) error { return nil }
|
||||
@@ -116,6 +117,18 @@ func (s *userHandlerRepoStub) ListUserAuthIdentities(context.Context, int64) ([]
|
||||
copy(out, s.identities)
|
||||
return out, nil
|
||||
}
|
||||
func (s *userHandlerRepoStub) UnbindUserAuthProvider(_ context.Context, _ int64, provider string) error {
|
||||
s.unbound = append(s.unbound, provider)
|
||||
filtered := s.identities[:0]
|
||||
for _, identity := range s.identities {
|
||||
if identity.ProviderType == provider {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, identity)
|
||||
}
|
||||
s.identities = append([]service.UserAuthIdentityRecord(nil), filtered...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestUserHandlerUpdateProfileReturnsAvatarURL(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
@@ -428,6 +441,60 @@ func TestUserHandlerBindEmailIdentityReturnsProfileResponse(t *testing.T) {
|
||||
require.True(t, resp.Data.EmailBound)
|
||||
}
|
||||
|
||||
func TestUserHandlerUnbindIdentityReturnsUpdatedProfile(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
repo := &userHandlerRepoStub{
|
||||
user: &service.User{
|
||||
ID: 21,
|
||||
Email: "identity@example.com",
|
||||
Username: "identity-user",
|
||||
Role: service.RoleUser,
|
||||
Status: service.StatusActive,
|
||||
},
|
||||
identities: []service.UserAuthIdentityRecord{
|
||||
{
|
||||
ProviderType: "email",
|
||||
ProviderKey: "email",
|
||||
ProviderSubject: "identity@example.com",
|
||||
},
|
||||
{
|
||||
ProviderType: "linuxdo",
|
||||
ProviderKey: "linuxdo",
|
||||
ProviderSubject: "linuxdo-subject-21",
|
||||
Metadata: map[string]any{
|
||||
"username": "linuxdo-handle",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
handler := NewUserHandler(service.NewUserService(repo, nil, nil, nil), nil, nil, nil)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(recorder)
|
||||
c.Request = httptest.NewRequest(http.MethodDelete, "/api/v1/user/account-bindings/linuxdo", nil)
|
||||
c.Set(string(middleware2.ContextKeyUser), middleware2.AuthSubject{UserID: 21})
|
||||
c.Params = gin.Params{{Key: "provider", Value: "linuxdo"}}
|
||||
|
||||
handler.UnbindIdentity(c)
|
||||
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
require.Equal(t, []string{"linuxdo"}, repo.unbound)
|
||||
|
||||
var resp struct {
|
||||
Code int `json:"code"`
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp))
|
||||
require.Equal(t, 0, resp.Code)
|
||||
|
||||
authBindings, ok := resp.Data["auth_bindings"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
linuxdoBinding, ok := authBindings["linuxdo"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, false, linuxdoBinding["bound"])
|
||||
}
|
||||
|
||||
func TestUserHandlerBindEmailIdentityRejectsWrongCurrentPasswordForBoundEmail(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user