support unlinking external authentication
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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() != ""
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user