support unlinking external authentication

This commit is contained in:
MaysWind
2025-10-25 02:45:40 +08:00
parent 7b49a9f142
commit ce752c992c
26 changed files with 707 additions and 39 deletions
+104
View File
@@ -0,0 +1,104 @@
package api
import (
"sort"
"github.com/mayswind/ezbookkeeping/pkg/auth/oauth2"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/services"
)
// UserExternalAuthsApi represents user external auth api
type UserExternalAuthsApi struct {
users *services.UserService
userExternalAuths *services.UserExternalAuthService
}
// Initialize a user external auth api singleton instance
var (
UserExternalAuths = &UserExternalAuthsApi{
users: services.Users,
userExternalAuths: services.UserExternalAuths,
}
)
// ExternalAuthListHanlder returns external authentications list of current user
func (a *UserExternalAuthsApi) ExternalAuthListHanlder(c *core.WebContext) (any, *errs.Error) {
uid := c.GetCurrentUid()
userExternalAuths, err := a.userExternalAuths.GetUserAllExternalAuthsByUid(c, uid)
if err != nil {
log.Errorf(c, "[user_external_auths.ExternalAuthListHanlder] failed to get all external authentications for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
userExternalAuthResps := make(models.UserExternalAuthInfoResponsesSlice, 0, len(userExternalAuths)+1)
currentExternalAuthType := oauth2.GetExternalUserAuthType()
hasCurrentExternalAuth := false
for i := 0; i < len(userExternalAuths); i++ {
userExternalAuth := userExternalAuths[i]
if userExternalAuth.ExternalAuthType == currentExternalAuthType {
hasCurrentExternalAuth = true
}
userExternalAuthResps = append(userExternalAuthResps, userExternalAuth.ToUserExternalAuthInfoResponse())
}
if !hasCurrentExternalAuth {
userExternalAuthResps = append(userExternalAuthResps, &models.UserExternalAuthInfoResponse{
ExternalAuthCategory: currentExternalAuthType.GetCategory(),
ExternalAuthType: currentExternalAuthType,
Linked: false,
})
}
sort.Sort(userExternalAuthResps)
return userExternalAuthResps, nil
}
// UnlinkExternalAuthHandler unlinks external authentication for current user
func (a *UserExternalAuthsApi) UnlinkExternalAuthHandler(c *core.WebContext) (any, *errs.Error) {
var externalAuthLinkReq models.UserExternalAuthUnlinkRequest
err := c.ShouldBindJSON(&externalAuthLinkReq)
if err != nil {
log.Warnf(c, "[user_external_auths.UnlinkExternalAuthHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
if !errs.IsCustomError(err) {
log.Warnf(c, "[user_external_auths.UnlinkExternalAuthHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
}
return nil, errs.ErrUserNotFound
}
if !a.users.IsPasswordEqualsUserPassword(externalAuthLinkReq.Password, user) {
return nil, errs.ErrUserPasswordWrong
}
externalAuthType := core.UserExternalAuthType(externalAuthLinkReq.ExternalAuthType)
if !externalAuthType.IsValid() {
return nil, errs.ErrUserExternalAuthNotFound
}
err = a.userExternalAuths.DeleteUserExternalAuth(c, uid, externalAuthType)
if err != nil {
log.Errorf(c, "[user_external_auths.UnlinkExternalAuthHandler] failed to unlink external authentication \"%s\" for user \"uid:%d\", because %s", externalAuthLinkReq.ExternalAuthType, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
return true, nil
}
+11 -4
View File
@@ -1,5 +1,7 @@
package core
const USER_EXTERNAL_AUTH_TYPE_CATEOGRY_OAUTH2 = "oauth2"
// UserExternalAuthType represents the type of user external authentication
type UserExternalAuthType string
@@ -11,14 +13,19 @@ const (
USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITHUB UserExternalAuthType = "github"
)
// IsValid checks if the UserExternalAuthType is valid
func (t UserExternalAuthType) IsValid() bool {
// GetCategory returns the category of the UserExternalAuthType
func (t UserExternalAuthType) GetCategory() string {
switch t {
case USER_EXTERNAL_AUTH_TYPE_OAUTH2_OIDC,
USER_EXTERNAL_AUTH_TYPE_OAUTH2_NEXTCLOUD,
USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITEA,
USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITHUB:
return true
return USER_EXTERNAL_AUTH_TYPE_CATEOGRY_OAUTH2
}
return false
return ""
}
// IsValid checks if the UserExternalAuthType is valid
func (t UserExternalAuthType) IsValid() bool {
return t.GetCategory() != ""
}
+1
View File
@@ -8,4 +8,5 @@ import (
var (
ErrUserExternalAuthNotFound = NewNormalError(NormalSubcategoryUserExternalAuth, 0, http.StatusBadRequest, "user external auth is not found")
ErrUserExternalAuthAlreadyExists = NewNormalError(NormalSubcategoryUserExternalAuth, 1, http.StatusBadRequest, "user external auth already exists")
ErrUserExternalAuthTypeInvalid = NewNormalError(NormalSubcategoryUserExternalAuth, 2, http.StatusBadRequest, "user external auth type invalid")
)
+50 -3
View File
@@ -11,7 +11,54 @@ type UserExternalAuth struct {
CreatedUnixTime int64
}
// UserExternalAuthRevokeRequest represents all parameters of user external auth revoke request
type UserExternalAuthRevokeRequest struct {
ExternalAuthType core.UserExternalAuthType `json:"externalAuthType" binding:"required,notBlank"`
// UserExternalAuthUnlinkRequest represents all parameters of user external auth unlink request
type UserExternalAuthUnlinkRequest struct {
ExternalAuthType string `json:"externalAuthType" binding:"required,notBlank"`
Password string `json:"password" binding:"required,min=6,max=128"`
}
// UserExternalAuthInfoResponse represents a view-object of user external auth
type UserExternalAuthInfoResponse struct {
ExternalAuthCategory string `json:"externalAuthCategory"`
ExternalAuthType core.UserExternalAuthType `json:"externalAuthType"`
Linked bool `json:"linked"`
ExternalUsername string `json:"externalUsername,omitempty"`
CreatedAt int64 `json:"createdAt,omitempty"`
}
// ToUserExternalAuthInfoResponse returns a view-object according to database model
func (a *UserExternalAuth) ToUserExternalAuthInfoResponse() *UserExternalAuthInfoResponse {
return &UserExternalAuthInfoResponse{
ExternalAuthCategory: a.ExternalAuthType.GetCategory(),
ExternalAuthType: a.ExternalAuthType,
Linked: true,
ExternalUsername: a.ExternalUsername,
CreatedAt: a.CreatedUnixTime,
}
}
// UserExternalAuthInfoResponsesSlice represents the slice data structure of UserExternalAuthInfoResponse
type UserExternalAuthInfoResponsesSlice []*UserExternalAuthInfoResponse
// Len returns the count of items
func (a UserExternalAuthInfoResponsesSlice) Len() int {
return len(a)
}
// Swap swaps two items
func (a UserExternalAuthInfoResponsesSlice) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
// Less reports whether the first item is less than the second one
func (a UserExternalAuthInfoResponsesSlice) Less(i, j int) bool {
if a[i].Linked && !a[j].Linked {
return true
} else if !a[i].Linked && a[j].Linked {
return false
} else if !a[i].Linked && !a[j].Linked {
return a[i].ExternalAuthType < a[j].ExternalAuthType
}
return a[i].CreatedAt > a[j].CreatedAt
}
+41
View File
@@ -0,0 +1,41 @@
package models
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/core"
)
func TestUserExternalAuthInfoResponsesSliceLess(t *testing.T) {
var userExternalAuthInfoResponsesSlice UserExternalAuthInfoResponsesSlice
userExternalAuthInfoResponsesSlice = append(userExternalAuthInfoResponsesSlice, &UserExternalAuthInfoResponse{
ExternalAuthType: core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_OIDC,
Linked: true,
ExternalUsername: "test",
CreatedAt: int64(1),
})
userExternalAuthInfoResponsesSlice = append(userExternalAuthInfoResponsesSlice, &UserExternalAuthInfoResponse{
ExternalAuthType: core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_NEXTCLOUD,
Linked: false,
})
userExternalAuthInfoResponsesSlice = append(userExternalAuthInfoResponsesSlice, &UserExternalAuthInfoResponse{
ExternalAuthType: core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITEA,
Linked: false,
})
userExternalAuthInfoResponsesSlice = append(userExternalAuthInfoResponsesSlice, &UserExternalAuthInfoResponse{
ExternalAuthType: core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITHUB,
Linked: true,
ExternalUsername: "test4",
CreatedAt: int64(2),
})
sort.Sort(userExternalAuthInfoResponsesSlice)
assert.Equal(t, core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITHUB, userExternalAuthInfoResponsesSlice[0].ExternalAuthType)
assert.Equal(t, core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_OIDC, userExternalAuthInfoResponsesSlice[1].ExternalAuthType)
assert.Equal(t, core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_GITEA, userExternalAuthInfoResponsesSlice[2].ExternalAuthType)
assert.Equal(t, core.USER_EXTERNAL_AUTH_TYPE_OAUTH2_NEXTCLOUD, userExternalAuthInfoResponsesSlice[3].ExternalAuthType)
}