You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by wa...@apache.org on 2022/06/06 15:49:49 UTC

[incubator-devlake] 01/04: feat(api): change jira basicauthencoded to username/password and extract common utils

This is an automated email from the ASF dual-hosted git repository.

warren pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git

commit f88e5c22fb6e6699930967ab9c145cab162ec752
Author: Yingchu Chen <yi...@merico.dev>
AuthorDate: Wed May 25 17:24:19 2022 +0800

    feat(api): change jira basicauthencoded to username/password and extract common utils
    
    closes #2003
---
 api/shared/api_output.go                           |   2 +-
 config-ui/src/hooks/useConnectionManager.jsx       |   2 +-
 config-ui/src/pages/configure/settings/jira.jsx    |  26 +--
 plugins/feishu/tasks/api_client.go                 |   2 +-
 plugins/github/tasks/api_client.go                 |   2 +-
 plugins/gitlab/tasks/api_client.go                 |   2 +-
 plugins/helper/connection.go                       | 217 +++++++++++++++++++++
 plugins/jira/api/connection.go                     | 191 +++---------------
 plugins/jira/api/issue_status_mapping.go           | 216 --------------------
 plugins/jira/api/issue_type_mapping.go             | 214 --------------------
 plugins/jira/api/proxy.go                          |  16 +-
 plugins/jira/jira.go                               |  17 +-
 plugins/jira/models/connection.go                  |  16 +-
 .../migrationscripts/updateSchemas20220505.go      |  20 +-
 .../migrationscripts/updateSchemas20220524.go      |  72 +++++++
 plugins/jira/tasks/api_client.go                   |   8 +-
 plugins/tapd/tasks/api_client.go                   |   4 +-
 17 files changed, 353 insertions(+), 674 deletions(-)

diff --git a/api/shared/api_output.go b/api/shared/api_output.go
index 8e006acd..28e2e630 100644
--- a/api/shared/api_output.go
+++ b/api/shared/api_output.go
@@ -35,7 +35,7 @@ func ApiOutputError(c *gin.Context, err error, status int) {
 			Message: err.Error(),
 		})
 	} else {
-		logger.Global.Error("Server Internal Error: %w", err)
+		logger.Global.Error("Server Internal Error: %s", err.Error())
 		c.JSON(status, &ApiBody{
 			Success: false,
 			Message: err.Error(),
diff --git a/config-ui/src/hooks/useConnectionManager.jsx b/config-ui/src/hooks/useConnectionManager.jsx
index a3558df0..f9116919 100644
--- a/config-ui/src/hooks/useConnectionManager.jsx
+++ b/config-ui/src/hooks/useConnectionManager.jsx
@@ -83,7 +83,7 @@ function useConnectionManager ({
       let connectionPayload
       switch (activeProvider.id) {
         case Providers.JIRA:
-          connectionPayload = { endpoint: endpointUrl, auth: token, proxy: proxy }
+          connectionPayload = { endpoint: endpointUrl, username: username, password: password, proxy: proxy }
           break
         case Providers.GITHUB:
           connectionPayload = { endpoint: endpointUrl, auth: token, proxy: proxy }
diff --git a/config-ui/src/pages/configure/settings/jira.jsx b/config-ui/src/pages/configure/settings/jira.jsx
index 8305eaa0..cda3defc 100644
--- a/config-ui/src/pages/configure/settings/jira.jsx
+++ b/config-ui/src/pages/configure/settings/jira.jsx
@@ -88,25 +88,6 @@ export default function JiraSettings (props) {
       : null
   }
 
-  const parseTypeMappings = useCallback((mappings = []) => {
-    const GroupedMappings = {
-      [MAPPING_TYPES.Requirement]: [],
-      [MAPPING_TYPES.Incident]: [],
-      [MAPPING_TYPES.Bug]: [],
-    }
-    Object.entries(mappings).forEach(([tag, typeObj]) => {
-      GroupedMappings[typeObj.standardType].push(tag)
-    })
-    console.log('>>>> PARSED TYPE MAPPINGS ....', GroupedMappings)
-    setTypeMappingRequirement(GroupedMappings[MAPPING_TYPES.Requirement])
-    setTypeMappingBug(GroupedMappings[MAPPING_TYPES.Bug])
-    setTypeMappingIncident(GroupedMappings[MAPPING_TYPES.Incident])
-    setRequirementTags(requirementTagsList?.filter(t => GroupedMappings[MAPPING_TYPES.Requirement].includes(t.value)))
-    setBugTags(bugTagsList?.filter(t => GroupedMappings[MAPPING_TYPES.Bug].includes(t.value)))
-    setIncidentTags(incidentTagsList?.filter(t => GroupedMappings[MAPPING_TYPES.Incident].includes(t.value)))
-    return GroupedMappings
-  }, [requirementTagsList, bugTagsList, incidentTagsList])
-
   useEffect(() => {
     const settings = {
       epicKeyField: jiraIssueEpicKeyField?.value || '',
@@ -164,13 +145,12 @@ export default function JiraSettings (props) {
     console.log('>> CONN SETTINGS OBJECT ', connection)
     if (connection && connection.ID) {
       // Parse Type Mappings (V2)
-      parseTypeMappings(connection.typeMappings)
       setStatusMappings([])
       setRemoteLinkCommitSha(connection.remotelinkCommitShaPattern)
       // setJiraIssueEpicKeyField(fieldsList.find(f => f.value === connection.epicKeyField))
       // setJiraIssueStoryPointField(fieldsList.find(f => f.value === connection.storyPointField))
     }
-  }, [connection, parseTypeMappings])
+  }, [connection])
 
   useEffect(() => {
     setTypeMappingRequirement(requirementTags)
@@ -208,10 +188,6 @@ export default function JiraSettings (props) {
     setJiraIssueStoryPointField(fieldsList.find(f => f.value === connection.storyPointField))
   }, [fieldsList, connection.epicKeyField, connection.storyPointField])
 
-  useEffect(() => {
-    parseTypeMappings(connection.typeMappings)
-  }, [requirementTagsList, bugTagsList, incidentTagsList, connection.typeMappings, parseTypeMappings])
-
   return (
     <>
       <div className='headlineContainer'>
diff --git a/plugins/feishu/tasks/api_client.go b/plugins/feishu/tasks/api_client.go
index d6b7b7d0..da54b5f9 100644
--- a/plugins/feishu/tasks/api_client.go
+++ b/plugins/feishu/tasks/api_client.go
@@ -92,7 +92,7 @@ func NewFeishuApiClient(taskCtx core.TaskContext) (*helper.ApiAsyncClient, error
 
 	apiClient.SetAfterFunction(func(res *http.Response) error {
 		if res.StatusCode == http.StatusUnauthorized {
-			return fmt.Errorf("feishu authentication failed, please check your Bearer Auth Token")
+			return fmt.Errorf("feishu authentication failed, please check your AccessToken")
 		}
 		return nil
 	})
diff --git a/plugins/github/tasks/api_client.go b/plugins/github/tasks/api_client.go
index 94a60e05..aa0e9aa0 100644
--- a/plugins/github/tasks/api_client.go
+++ b/plugins/github/tasks/api_client.go
@@ -60,7 +60,7 @@ func CreateApiClient(taskCtx core.TaskContext) (*helper.ApiAsyncClient, error) {
 	})
 	apiClient.SetAfterFunction(func(res *http.Response) error {
 		if res.StatusCode == http.StatusUnauthorized {
-			return fmt.Errorf("authentication failed, please check your Token configuration")
+			return fmt.Errorf("authentication failed, please check your AccessToken configuration")
 		}
 		return nil
 	})
diff --git a/plugins/gitlab/tasks/api_client.go b/plugins/gitlab/tasks/api_client.go
index 0dc8bf64..f4c2b653 100644
--- a/plugins/gitlab/tasks/api_client.go
+++ b/plugins/gitlab/tasks/api_client.go
@@ -54,7 +54,7 @@ func NewGitlabApiClient(taskCtx core.TaskContext) (*helper.ApiAsyncClient, error
 	}
 	apiClient.SetAfterFunction(func(res *http.Response) error {
 		if res.StatusCode == http.StatusUnauthorized {
-			return fmt.Errorf("authentication failed, please check your Basic Auth Token")
+			return fmt.Errorf("authentication failed, please check your AccessToken")
 		}
 		return nil
 	})
diff --git a/plugins/helper/connection.go b/plugins/helper/connection.go
new file mode 100644
index 00000000..93974f73
--- /dev/null
+++ b/plugins/helper/connection.go
@@ -0,0 +1,217 @@
+package helper
+
+import (
+	"encoding/base64"
+	"fmt"
+	"github.com/apache/incubator-devlake/config"
+	"github.com/apache/incubator-devlake/models/common"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/go-playground/validator/v10"
+	"github.com/mitchellh/mapstructure"
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"reflect"
+	"strconv"
+)
+
+type BaseConnection struct {
+	Name string `gorm:"type:varchar(100);uniqueIndex" json:"name" validate:"required"`
+	common.Model
+}
+
+type BasicAuth struct {
+	Username string `mapstructure:"username" validate:"required" json:"username"`
+	Password string `mapstructure:"password" validate:"required" json:"password" encrypt:"yes"`
+}
+
+func (ba BasicAuth) GetEncodedToken() string {
+	return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", ba.Username, ba.Password)))
+}
+
+type AccessToken struct {
+	Token string `mapstructure:"token" validate:"required" json:"token" encrypt:"yes"`
+}
+
+type RestConnection struct {
+	BaseConnection `mapstructure:",squash"`
+	Endpoint       string `mapstructure:"endpoint" validate:"required" json:"endpoint"`
+	Proxy          string `mapstructure:"proxy" json:"proxy"`
+	RateLimit      int    `comment:"api request rate limt per hour" json:"rateLimit"`
+}
+
+// RefreshAndSaveConnection populate from request input into connection which come from REST functions to connection struct and save to DB
+// and only change value which `data` has
+// mergeFieldsToConnection merges fields from data
+// `connection` is the pointer of a plugin connection
+// `data` is http request input param
+func RefreshAndSaveConnection(connection interface{}, data map[string]interface{}, db *gorm.DB) error {
+	var err error
+	// update fields from request body
+	err = mergeFieldsToConnection(connection, data)
+	if err != nil {
+		return err
+	}
+
+	err = saveToDb(connection, db)
+
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func saveToDb(connection interface{}, db *gorm.DB) error {
+	dataVal := reflect.ValueOf(connection)
+	if dataVal.Kind() != reflect.Ptr {
+		panic("entityPtr is not a pointer")
+	}
+
+	dataType := reflect.Indirect(dataVal).Type()
+	fieldName := getEncryptField(dataType, "encrypt")
+	plainPwd := ""
+	err := doEncrypt(dataVal, fieldName)
+	if err != nil {
+		return err
+	}
+	err = db.Clauses(clause.OnConflict{UpdateAll: true}).Save(connection).Error
+	if err != nil {
+		return err
+	}
+
+	err = doDecrypt(dataVal, fieldName)
+	if err != nil {
+		return err
+	}
+	dataVal.Elem().FieldByName(fieldName).Set(reflect.ValueOf(plainPwd))
+
+	return err
+}
+
+// mergeFieldsToConnection will populate all value in map to connection struct and validate the struct
+func mergeFieldsToConnection(specificConnection interface{}, connections ...map[string]interface{}) error {
+	// decode
+	for _, connection := range connections {
+		err := mapstructure.Decode(connection, specificConnection)
+		if err != nil {
+			return err
+		}
+	}
+	// validate
+	vld := validator.New()
+	err := vld.Struct(specificConnection)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func getEncKey() (string, error) {
+	// encrypt
+	v := config.GetConfig()
+	encKey := v.GetString(core.EncodeKeyEnvStr)
+	if encKey == "" {
+		// Randomly generate a bunch of encryption keys and set them to config
+		encKey = core.RandomEncKey()
+		v.Set(core.EncodeKeyEnvStr, encKey)
+		err := config.WriteConfig(v)
+		if err != nil {
+			return encKey, err
+		}
+	}
+	return encKey, nil
+}
+
+// FindConnectionByInput finds connection from db  by parsing request input and decrypt it
+func FindConnectionByInput(input *core.ApiResourceInput, connection interface{}, db *gorm.DB) error {
+	dataVal := reflect.ValueOf(connection)
+	if dataVal.Kind() != reflect.Ptr {
+		return fmt.Errorf("connection is not a pointer")
+	}
+
+	id, err := GetConnectionIdByInputParam(input)
+	if err != nil {
+		return fmt.Errorf("invalid connectionId")
+	}
+
+	err = db.First(connection, id).Error
+	if err != nil {
+		fmt.Printf("--- %s", err.Error())
+		return err
+	}
+
+	dataType := reflect.Indirect(dataVal).Type()
+
+	fieldName := getEncryptField(dataType, "encrypt")
+	return doDecrypt(dataVal, fieldName)
+
+}
+
+// GetConnectionIdByInputParam gets connectionId by parsing request input
+func GetConnectionIdByInputParam(input *core.ApiResourceInput) (uint64, error) {
+	connectionId := input.Params["connectionId"]
+	if connectionId == "" {
+		return 0, fmt.Errorf("missing connectionId")
+	}
+	return strconv.ParseUint(connectionId, 10, 64)
+}
+
+func getEncryptField(t reflect.Type, tag string) string {
+	fieldName := ""
+	for i := 0; i < t.NumField(); i++ {
+		field := t.Field(i)
+		if field.Type.Kind() == reflect.Struct {
+			fieldName = getEncryptField(field.Type, tag)
+		} else {
+			if field.Tag.Get(tag) == "yes" {
+				fieldName = field.Name
+			}
+		}
+	}
+	return fieldName
+}
+
+// DecryptConnection decrypts password/token field for connection
+func DecryptConnection(connection interface{}, fieldName string) error {
+	dataVal := reflect.ValueOf(connection)
+	if dataVal.Kind() != reflect.Ptr {
+		panic("connection is not a pointer")
+	}
+	if len(fieldName) == 0 {
+		dataType := reflect.Indirect(dataVal).Type()
+		fieldName = getEncryptField(dataType, "encrypt")
+	}
+	return doDecrypt(dataVal, fieldName)
+}
+
+func doDecrypt(dataVal reflect.Value, fieldName string) error {
+	encryptCode, err := getEncKey()
+	if err != nil {
+		return err
+	}
+	if len(fieldName) > 0 {
+		decryptStr, err := core.Decrypt(encryptCode, dataVal.Elem().FieldByName(fieldName).String())
+		if err != nil {
+			return err
+		}
+		dataVal.Elem().FieldByName(fieldName).Set(reflect.ValueOf(decryptStr))
+	}
+	return nil
+}
+
+func doEncrypt(dataVal reflect.Value, fieldName string) error {
+	encryptCode, err := getEncKey()
+	if err != nil {
+		return err
+	}
+	if len(fieldName) > 0 {
+		plainPwd := dataVal.Elem().FieldByName(fieldName).String()
+		encyptedStr, err := core.Encrypt(encryptCode, plainPwd)
+
+		if err != nil {
+			return err
+		}
+		dataVal.Elem().FieldByName(fieldName).Set(reflect.ValueOf(encyptedStr))
+	}
+	return nil
+}
diff --git a/plugins/jira/api/connection.go b/plugins/jira/api/connection.go
index 96970d77..aa94537a 100644
--- a/plugins/jira/api/connection.go
+++ b/plugins/jira/api/connection.go
@@ -25,9 +25,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/apache/incubator-devlake/config"
-	"github.com/apache/incubator-devlake/models/common"
-
 	"github.com/apache/incubator-devlake/errors"
 	"github.com/apache/incubator-devlake/plugins/core"
 	"github.com/apache/incubator-devlake/plugins/helper"
@@ -52,11 +49,12 @@ func TestConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, erro
 	if err != nil {
 		return nil, err
 	}
+
 	// test connection
 	apiClient, err := helper.NewApiClient(
 		connection.Endpoint,
 		map[string]string{
-			"Authorization": fmt.Sprintf("Basic %v", connection.Auth),
+			"Authorization": fmt.Sprintf("Basic %v", connection.GetEncodedToken()),
 		},
 		3*time.Second,
 		connection.Proxy,
@@ -100,111 +98,6 @@ func TestConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, erro
 	return nil, nil
 }
 
-func findConnectionByInputParam(input *core.ApiResourceInput) (*models.JiraConnection, error) {
-	jiraConnectionId, err := getJiraConnectionIdByInputParam(input)
-	if err != nil {
-		return nil, fmt.Errorf("invalid connectionId")
-	}
-	return getJiraConnectionById(jiraConnectionId)
-}
-
-func getJiraConnectionIdByInputParam(input *core.ApiResourceInput) (uint64, error) {
-	connectionId := input.Params["connectionId"]
-	if connectionId == "" {
-		return 0, fmt.Errorf("missing connectionId")
-	}
-	return strconv.ParseUint(connectionId, 10, 64)
-}
-
-func getJiraConnectionById(id uint64) (*models.JiraConnection, error) {
-	jiraConnection := &models.JiraConnection{}
-	err := db.First(jiraConnection, id).Error
-	if err != nil {
-		return nil, err
-	}
-
-	// decrypt
-	v := config.GetConfig()
-	encKey := v.GetString(core.EncodeKeyEnvStr)
-	jiraConnection.BasicAuthEncoded, err = core.Decrypt(encKey, jiraConnection.BasicAuthEncoded)
-	if err != nil {
-		log.Error("failed to decrypt basic auth: %s", err)
-	}
-
-	return jiraConnection, nil
-}
-func mergeFieldsToJiraConnection(jiraConnection *models.JiraConnection, connections ...map[string]interface{}) error {
-	// decode
-	for _, connection := range connections {
-		err := mapstructure.Decode(connection, jiraConnection)
-		if err != nil {
-			return err
-		}
-	}
-
-	// validate
-	vld := validator.New()
-	err := vld.Struct(jiraConnection)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func refreshAndSaveJiraConnection(jiraConnection *models.JiraConnection, data map[string]interface{}) error {
-	var err error
-	// update fields from request body
-	err = mergeFieldsToJiraConnection(jiraConnection, data)
-	if err != nil {
-		return err
-	}
-
-	encKey, err := getEncKey()
-	if err != nil {
-		return err
-	}
-	jiraConnection.BasicAuthEncoded, err = core.Encrypt(encKey, jiraConnection.BasicAuthEncoded)
-	if err != nil {
-		return err
-	}
-
-	// transaction for nested operations
-	tx := db.Begin()
-	defer func() {
-		if err != nil {
-			tx.Rollback()
-		} else {
-			tx.Commit()
-		}
-	}()
-	if jiraConnection.ID > 0 {
-		err = tx.Save(jiraConnection).Error
-	} else {
-		err = tx.Create(jiraConnection).Error
-	}
-	if err != nil {
-		if common.IsDuplicateError(err) {
-			return fmt.Errorf("jira connection with name %s already exists", jiraConnection.Name)
-		}
-		return err
-	}
-	// perform optional operation
-	typeMappings := data["typeMappings"]
-	if typeMappings != nil {
-		err = saveTypeMappings(tx, jiraConnection.ID, typeMappings)
-		if err != nil {
-			return err
-		}
-	}
-
-	jiraConnection.BasicAuthEncoded, err = core.Decrypt(encKey, jiraConnection.BasicAuthEncoded)
-	if err != nil {
-		log.Error("failed to decrypt basic auth: %s", err)
-	}
-	return nil
-}
-
 /*
 POST /plugins/jira/connections
 {
@@ -230,7 +123,7 @@ func PostConnections(input *core.ApiResourceInput) (*core.ApiResourceOutput, err
 	jiraConnection := &models.JiraConnection{}
 
 	// update from request and save to database
-	err := refreshAndSaveJiraConnection(jiraConnection, input.Body)
+	err := helper.RefreshAndSaveConnection(jiraConnection, input.Body, db)
 	if err != nil {
 		return nil, err
 	}
@@ -259,14 +152,15 @@ PATCH /plugins/jira/connections/:connectionId
 }
 */
 func PatchConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
+	jiraConnection := &models.JiraConnection{}
 	// load from db
-	jiraConnection, err := findConnectionByInputParam(input)
+	err := helper.FindConnectionByInput(input, jiraConnection, db)
 	if err != nil {
 		return nil, err
 	}
 
 	// update from request and save to database
-	err = refreshAndSaveJiraConnection(jiraConnection, input.Body)
+	err = helper.RefreshAndSaveConnection(jiraConnection, input.Body, db)
 	if err != nil {
 		return nil, err
 	}
@@ -279,7 +173,7 @@ DELETE /plugins/jira/connections/:connectionId
 */
 func DeleteConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
 	// load from db
-	jiraConnectionID, err := getJiraConnectionIdByInputParam(input)
+	jiraConnectionID, err := helper.GetConnectionIdByInputParam(input)
 	if err != nil {
 		return nil, err
 	}
@@ -304,22 +198,19 @@ func DeleteConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, er
 GET /plugins/jira/connections
 */
 func ListConnections(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	jiraConnections := make([]models.JiraConnection, 0)
+	jiraConnections := make([]*models.JiraConnection, 0)
 	err := db.Find(&jiraConnections).Error
 	if err != nil {
 		return nil, err
 	}
-	encKey, err := getEncKey()
-	if err != nil {
-		return nil, err
-	}
-	for i := range jiraConnections {
-		jiraConnections[i].BasicAuthEncoded, err = core.Decrypt(encKey, jiraConnections[i].BasicAuthEncoded)
+	for i, _ := range jiraConnections {
+		err = helper.DecryptConnection(jiraConnections[i], "Password")
 		if err != nil {
-			log.Error("failed to decrypt basic auth: %s", err)
+			return nil, err
 		}
 	}
-	return &core.ApiResourceOutput{Body: jiraConnections}, nil
+
+	return &core.ApiResourceOutput{Body: jiraConnections, Status: http.StatusOK}, nil
 }
 
 /*
@@ -345,53 +236,27 @@ GET /plugins/jira/connections/:connectionId
 }
 */
 func GetConnection(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	jiraConnection, err := findConnectionByInputParam(input)
+	jiraConnection := &models.JiraConnection{}
+	err := helper.FindConnectionByInput(input, jiraConnection, db)
 	if err != nil {
 		return nil, err
 	}
 
 	detail := &models.JiraConnectionDetail{
 		JiraConnection: *jiraConnection,
-		TypeMappings:   make(map[string]map[string]interface{}),
 	}
 
-	typeMappings, err := findIssueTypeMappingByConnectionId(jiraConnection.ID)
 	if err != nil {
 		return nil, err
 	}
-	for _, jiraTypeMapping := range typeMappings {
-		// type mapping
-		typeMappingDict := map[string]interface{}{
-			"standardType": jiraTypeMapping.StandardType,
-		}
-		detail.TypeMappings[jiraTypeMapping.UserType] = typeMappingDict
-
-		// status mapping
-		statusMappings, err := findIssueStatusMappingByConnectionIdAndUserType(
-			jiraConnection.ID,
-			jiraTypeMapping.UserType,
-		)
-		if err != nil {
-			return nil, err
-		}
-		if len(statusMappings) == 0 {
-			continue
-		}
-		statusMappingsDict := make(map[string]interface{})
-		for _, jiraStatusMapping := range statusMappings {
-			statusMappingsDict[jiraStatusMapping.UserStatus] = map[string]interface{}{
-				"standardStatus": jiraStatusMapping.StandardStatus,
-			}
-		}
-		typeMappingDict["statusMappings"] = statusMappingsDict
-	}
 
 	return &core.ApiResourceOutput{Body: detail}, nil
 }
 
 // GET /plugins/jira/connections/:connectionId/epics
 func GetEpicsByConnectionId(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	jiraConnection, err := findConnectionByInputParam(input)
+	jiraConnection := &models.JiraConnection{}
+	err := helper.FindConnectionByInput(input, jiraConnection, db)
 	if err != nil {
 		return nil, err
 	}
@@ -410,7 +275,11 @@ type GranularitiesResponse struct {
 }
 
 func GetGranularitiesByConnectionId(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	jiraConnection, err := findConnectionByInputParam(input)
+	jiraConnection := &models.JiraConnection{}
+	err := helper.FindConnectionByInput(input, jiraConnection, db)
+	if err != nil {
+		return nil, err
+	}
 	if err != nil {
 		return nil, err
 	}
@@ -448,19 +317,3 @@ func GetBoardsByConnectionId(input *core.ApiResourceInput) (*core.ApiResourceOut
 	}
 	return &core.ApiResourceOutput{Body: boardResponses}, nil
 }
-
-func getEncKey() (string, error) {
-	// encrypt
-	v := config.GetConfig()
-	encKey := v.GetString(core.EncodeKeyEnvStr)
-	if encKey == "" {
-		// Randomly generate a bunch of encryption keys and set them to config
-		encKey = core.RandomEncKey()
-		v.Set(core.EncodeKeyEnvStr, encKey)
-		err := config.WriteConfig(v)
-		if err != nil {
-			return encKey, err
-		}
-	}
-	return encKey, nil
-}
diff --git a/plugins/jira/api/issue_status_mapping.go b/plugins/jira/api/issue_status_mapping.go
deleted file mode 100644
index 9caa90b3..00000000
--- a/plugins/jira/api/issue_status_mapping.go
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
-Licensed to the Apache Software Foundation (ASF) under one or more
-contributor license agreements.  See the NOTICE file distributed with
-this work for additional information regarding copyright ownership.
-The ASF licenses this file to You under the Apache License, Version 2.0
-(the "License"); you may not use this file except in compliance with
-the License.  You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package api
-
-import (
-	"fmt"
-	"net/http"
-
-	"github.com/apache/incubator-devlake/models/common"
-
-	"github.com/apache/incubator-devlake/plugins/core"
-	"github.com/apache/incubator-devlake/plugins/jira/models"
-	"github.com/go-playground/validator/v10"
-	"github.com/mitchellh/mapstructure"
-	"gorm.io/gorm"
-)
-
-func findIssueStatusMappingFromInput(input *core.ApiResourceInput) (*models.JiraIssueStatusMapping, error) {
-	// load type mapping
-	jiraIssueTypeMapping, err := findIssueTypeMappingByInputParam(input)
-	if err != nil {
-		return nil, err
-	}
-	// load status mapping from db
-	userStatus := input.Params["userStatus"]
-	if userStatus == "" {
-		return nil, fmt.Errorf("missing userStatus")
-	}
-	jiraIssueStatusMapping := &models.JiraIssueStatusMapping{}
-	err = db.First(
-		jiraIssueStatusMapping,
-		jiraIssueTypeMapping.ConnectionID,
-		jiraIssueTypeMapping.UserType,
-		userStatus,
-	).Error
-	if err != nil {
-		return nil, err
-	}
-
-	return jiraIssueStatusMapping, nil
-}
-
-func mergeFieldsToJiraStatusMapping(
-	jiraIssueStatusMapping *models.JiraIssueStatusMapping,
-	connections ...map[string]interface{},
-) error {
-	// merge fields from connections to jiraIssueStatusMapping
-	for _, connection := range connections {
-		err := mapstructure.Decode(connection, jiraIssueStatusMapping)
-		if err != nil {
-			return err
-		}
-	}
-	// validate
-	vld := validator.New()
-	err := vld.Struct(jiraIssueStatusMapping)
-	if err != nil {
-		return err
-	}
-	return nil
-}
-
-func wrapIssueStatusDuplicateErr(err error) error {
-	if common.IsDuplicateError(err) {
-		return fmt.Errorf("jira issue status mapping already exists")
-	}
-	return err
-}
-
-func saveStatusMappings(tx *gorm.DB, jiraConnectionId uint64, userType string, statusMappings interface{}) error {
-	statusMappingsMap, ok := statusMappings.(map[string]interface{})
-	if !ok {
-		return fmt.Errorf("statusMappings is not a JSON object: %v", statusMappings)
-	}
-	err := tx.Where(
-		"connection_id = ? AND user_type = ?",
-		jiraConnectionId,
-		userType).Delete(&models.JiraIssueStatusMapping{}).Error
-	if err != nil {
-		return err
-	}
-	for userStatus, statusMapping := range statusMappingsMap {
-		statusMappingMap, ok := statusMapping.(map[string]interface{})
-		if !ok {
-			return fmt.Errorf("statusMapping is not a JSON object: %v", statusMappings)
-		}
-		jiraIssueStatusMapping := &models.JiraIssueStatusMapping{}
-		err = mergeFieldsToJiraStatusMapping(jiraIssueStatusMapping, statusMappingMap, map[string]interface{}{
-			"ConnectionID": jiraConnectionId,
-			"UserType":     userType,
-			"UserStatus":   userStatus,
-		})
-		if err != nil {
-			return err
-		}
-		err = tx.Create(jiraIssueStatusMapping).Error
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func findIssueStatusMappingByConnectionIdAndUserType(
-	jiraConnectionId uint64,
-	userType string,
-) ([]*models.JiraIssueStatusMapping, error) {
-	jiraIssueStatusMappings := make([]*models.JiraIssueStatusMapping, 0)
-	err := db.Where(
-		"connection_id = ? AND user_type = ?",
-		jiraConnectionId,
-		userType,
-	).Find(&jiraIssueStatusMappings).Error
-	return jiraIssueStatusMappings, err
-}
-
-/*
-POST /plugins/jira/connections/:connectionId/type-mappings/:userType/status-mappings
-{
-	"userStatus": "user custom status",
-	"standardStatus": "devlake standard status"
-}
-*/
-func PostIssueStatusMappings(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	jiraIssueTypeMapping, err := findIssueTypeMappingByInputParam(input)
-	if err != nil {
-		return nil, err
-	}
-	jiraIssueStatusMapping := &models.JiraIssueStatusMapping{}
-	err = mergeFieldsToJiraStatusMapping(jiraIssueStatusMapping, input.Body, map[string]interface{}{
-		"ConnectionID": jiraIssueTypeMapping.ConnectionID,
-		"UserType":     jiraIssueTypeMapping.UserType,
-	})
-	if err != nil {
-		return nil, err
-	}
-	// save
-	err = wrapIssueStatusDuplicateErr(db.Create(jiraIssueStatusMapping).Error)
-	if err != nil {
-		return nil, err
-	}
-	return &core.ApiResourceOutput{Body: jiraIssueStatusMapping, Status: http.StatusOK}, nil
-}
-
-/*
-PUT /plugins/jira/connections/:connectionId/type-mappings/:userType/status-mappings/:userStatus
-{
-	"standardStatus": "devlake standard status"
-}
-*/
-func PutIssueStatusMapping(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	// load from db
-	jiraIssueStatusMapping, err := findIssueStatusMappingFromInput(input)
-	if err != nil {
-		return nil, err
-	}
-	// update with request body
-	err = mergeFieldsToJiraStatusMapping(jiraIssueStatusMapping, input.Body)
-	if err != nil {
-		return nil, err
-	}
-	// save
-	err = wrapIssueStatusDuplicateErr(db.Save(jiraIssueStatusMapping).Error)
-	if err != nil {
-		return nil, err
-	}
-	return &core.ApiResourceOutput{Body: jiraIssueStatusMapping}, nil
-}
-
-/*
-DELETE /plugins/jira/connections/:connectionId/type-mappings/:userType/status-mappings/:userStatus
-*/
-func DeleteIssueStatusMapping(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	jiraIssueStatusMapping, err := findIssueStatusMappingFromInput(input)
-	if err != nil {
-		return nil, err
-	}
-	err = db.Delete(jiraIssueStatusMapping).Error
-	if err != nil {
-		return nil, err
-	}
-	return &core.ApiResourceOutput{Body: jiraIssueStatusMapping}, nil
-}
-
-/*
-GET /plugins/jira/connections/:connectionId/type-mappings/:userType/status-mappings
-*/
-func ListIssueStatusMappings(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	jiraIssueTypeMapping, err := findIssueTypeMappingByInputParam(input)
-	if err != nil {
-		return nil, err
-	}
-	jiraIssueStatusMappings, err := findIssueStatusMappingByConnectionIdAndUserType(
-		jiraIssueTypeMapping.ConnectionID,
-		jiraIssueTypeMapping.UserType,
-	)
-	if err != nil {
-		return nil, err
-	}
-	return &core.ApiResourceOutput{Body: jiraIssueStatusMappings}, nil
-}
diff --git a/plugins/jira/api/issue_type_mapping.go b/plugins/jira/api/issue_type_mapping.go
deleted file mode 100644
index 7f135646..00000000
--- a/plugins/jira/api/issue_type_mapping.go
+++ /dev/null
@@ -1,214 +0,0 @@
-/*
-Licensed to the Apache Software Foundation (ASF) under one or more
-contributor license agreements.  See the NOTICE file distributed with
-this work for additional information regarding copyright ownership.
-The ASF licenses this file to You under the Apache License, Version 2.0
-(the "License"); you may not use this file except in compliance with
-the License.  You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package api
-
-import (
-	"fmt"
-	"net/http"
-
-	"github.com/apache/incubator-devlake/models/common"
-
-	"github.com/apache/incubator-devlake/plugins/core"
-	"github.com/apache/incubator-devlake/plugins/jira/models"
-	"github.com/go-playground/validator/v10"
-	"github.com/mitchellh/mapstructure"
-	"gorm.io/gorm"
-)
-
-func findIssueTypeMappingByInputParam(input *core.ApiResourceInput) (*models.JiraIssueTypeMapping, error) {
-	// load jira connection
-	jiraConnection, err := findConnectionByInputParam(input)
-	if err != nil {
-		return nil, err
-	}
-	// load jira type mapping from db
-	userType := input.Params["userType"]
-	if userType == "" {
-		return nil, fmt.Errorf("missing userType")
-	}
-	jiraIssueTypeMapping := &models.JiraIssueTypeMapping{}
-	err = db.First(jiraIssueTypeMapping, jiraConnection.ID, userType).Error
-	if err != nil {
-		return nil, err
-	}
-
-	return jiraIssueTypeMapping, nil
-}
-
-func mergeFieldsToJiraTypeMapping(
-	jiraIssueTypeMapping *models.JiraIssueTypeMapping,
-	connections ...map[string]interface{},
-) error {
-	// merge fields from connections to jiraIssueTypeMapping
-	for _, connection := range connections {
-		err := mapstructure.Decode(connection, jiraIssueTypeMapping)
-		if err != nil {
-			return err
-		}
-	}
-	// validate
-	vld := validator.New()
-	err := vld.Struct(jiraIssueTypeMapping)
-	if err != nil {
-		return err
-	}
-	return nil
-}
-
-func wrapIssueTypeDuplicateErr(err error) error {
-	if common.IsDuplicateError(err) {
-		return fmt.Errorf("jira issue type mapping already exists")
-	}
-	return err
-}
-
-func saveTypeMappings(tx *gorm.DB, jiraConnectionId uint64, typeMappings interface{}) error {
-	typeMappingsMap, ok := typeMappings.(map[string]interface{})
-	if !ok {
-		return fmt.Errorf("typeMappings is not a JSON object: %v", typeMappings)
-	}
-	err := tx.Where("connection_id = ?", jiraConnectionId).Delete(&models.JiraIssueTypeMapping{}).Error
-	if err != nil {
-		return err
-	}
-	for userType, typeMapping := range typeMappingsMap {
-		typeMappingMap, ok := typeMapping.(map[string]interface{})
-		if !ok {
-			return fmt.Errorf("typeMapping is not a JSON object: %v", typeMapping)
-		}
-		jiraIssueTypeMapping := &models.JiraIssueTypeMapping{}
-		err = mergeFieldsToJiraTypeMapping(jiraIssueTypeMapping, typeMappingMap, map[string]interface{}{
-			"ConnectionID": jiraConnectionId,
-			"UserType":     userType,
-		})
-		if err != nil {
-			return err
-		}
-		err = wrapIssueTypeDuplicateErr(tx.Create(jiraIssueTypeMapping).Error)
-		if err != nil {
-			return err
-		}
-
-		statusMappings := typeMappingMap["statusMappings"]
-		if statusMappings != nil {
-			err = saveStatusMappings(tx, jiraConnectionId, userType, statusMappings)
-			if err != nil {
-				return err
-			}
-		}
-	}
-	return nil
-}
-
-func findIssueTypeMappingByConnectionId(jiraConnectionId uint64) ([]*models.JiraIssueTypeMapping, error) {
-	jiraIssueTypeMappings := make([]*models.JiraIssueTypeMapping, 0)
-	err := db.Where("connection_id = ?", jiraConnectionId).Find(&jiraIssueTypeMappings).Error
-	if err != nil {
-		return nil, err
-	}
-	return jiraIssueTypeMappings, nil
-}
-
-/*
-POST /plugins/jira/connections/:connectionId/type-mappings
-{
-	"userType": "user custom type",
-	"standardType": "devlake standard type"
-}
-*/
-func PostIssueTypeMappings(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	// create new
-	jiraConnection, err := findConnectionByInputParam(input)
-	if err != nil {
-		return nil, err
-	}
-	jiraIssueTypeMapping := &models.JiraIssueTypeMapping{}
-	err = mergeFieldsToJiraTypeMapping(jiraIssueTypeMapping, input.Body, map[string]interface{}{
-		"ConnectionID": jiraConnection.ID,
-	})
-	if err != nil {
-		return nil, err
-	}
-	// save
-	err = wrapIssueTypeDuplicateErr(db.Create(jiraIssueTypeMapping).Error)
-	if err != nil {
-		return nil, err
-	}
-	return &core.ApiResourceOutput{Body: jiraIssueTypeMapping, Status: http.StatusOK}, nil
-}
-
-/*
-PUT /plugins/jira/connections/:connectionId/type-mappings/:userType
-{
-	"standardType": "devlake standard type"
-}
-*/
-func PutIssueTypeMapping(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	// load from db
-	jiraIssueTypeMapping, err := findIssueTypeMappingByInputParam(input)
-	if err != nil {
-		return nil, err
-	}
-	// update with request body
-	err = mergeFieldsToJiraTypeMapping(jiraIssueTypeMapping, input.Body)
-	if err != nil {
-		return nil, err
-	}
-	// save
-	err = wrapIssueTypeDuplicateErr(db.Save(jiraIssueTypeMapping).Error)
-	if err != nil {
-		return nil, err
-	}
-	return &core.ApiResourceOutput{Body: jiraIssueTypeMapping}, nil
-}
-
-/*
-DELETE /plugins/jira/connections/:connectionId/type-mappings/:userType
-*/
-func DeleteIssueTypeMapping(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	// load from db
-	jiraIssueTypeMapping, err := findIssueTypeMappingByInputParam(input)
-	if err != nil {
-		return nil, err
-	}
-	err = db.Delete(jiraIssueTypeMapping).Error
-	if err != nil {
-		return nil, err
-	}
-	err = db.Where(
-		"connection_id = ? AND user_type = ?",
-		jiraIssueTypeMapping.ConnectionID,
-		jiraIssueTypeMapping.UserType,
-	).Delete(&models.JiraIssueStatusMapping{}).Error
-	if err != nil {
-		return nil, err
-	}
-	return &core.ApiResourceOutput{Body: jiraIssueTypeMapping}, nil
-}
-
-/*
-GET /plugins/jira/connections/:connectionId/type-mappings
-*/
-func ListIssueTypeMappings(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	jiraConnection, err := findConnectionByInputParam(input)
-	if err != nil {
-		return nil, err
-	}
-	jiraIssueTypeMappings, err := findIssueTypeMappingByConnectionId(jiraConnection.ID)
-	return &core.ApiResourceOutput{Body: jiraIssueTypeMappings}, err
-}
diff --git a/plugins/jira/api/proxy.go b/plugins/jira/api/proxy.go
index 55331e01..e4a3eb90 100644
--- a/plugins/jira/api/proxy.go
+++ b/plugins/jira/api/proxy.go
@@ -20,8 +20,8 @@ package api
 import (
 	"encoding/json"
 	"fmt"
+	"github.com/apache/incubator-devlake/utils"
 	"io/ioutil"
-	"strconv"
 	"time"
 
 	"github.com/apache/incubator-devlake/plugins/core"
@@ -34,21 +34,13 @@ const (
 )
 
 func Proxy(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
-	connectionId := input.Params["connectionId"]
-	if connectionId == "" {
-		return nil, fmt.Errorf("missing connectionid")
-	}
-	jiraConnectionId, err := strconv.ParseUint(connectionId, 10, 64)
-	if err != nil {
-		return nil, err
-	}
 	jiraConnection := &models.JiraConnection{}
-	err = db.First(jiraConnection, jiraConnectionId).Error
+	err := helper.FindConnectionByInput(input, jiraConnection, db)
 	if err != nil {
 		return nil, err
 	}
-	encKey := cfg.GetString(core.EncodeKeyEnvStr)
-	basicAuth, err := core.Decrypt(encKey, jiraConnection.BasicAuthEncoded)
+	basicAuth := utils.GetEncodedToken(jiraConnection.Username, jiraConnection.Password)
+
 	if err != nil {
 		return nil, err
 	}
diff --git a/plugins/jira/jira.go b/plugins/jira/jira.go
index 2b865ea6..2a40b901 100644
--- a/plugins/jira/jira.go
+++ b/plugins/jira/jira.go
@@ -150,6 +150,7 @@ func (plugin Jira) MigrationScripts() []migration.Script {
 		new(migrationscripts.UpdateSchemas20220505),
 		new(migrationscripts.UpdateSchemas20220507),
 		new(migrationscripts.UpdateSchemas20220518),
+		new(migrationscripts.UpdateSchemas20220524),
 		new(migrationscripts.UpdateSchemas20220525),
 		new(migrationscripts.UpdateSchemas20220526),
 		new(migrationscripts.UpdateSchemas20220527),
@@ -184,22 +185,6 @@ func (plugin Jira) ApiResources() map[string]map[string]core.ApiResourceHandler
 		"connections/:connectionId/boards": {
 			"GET": api.GetBoardsByConnectionId,
 		},
-		"connections/:connectionId/type-mappings": {
-			"POST": api.PostIssueTypeMappings,
-			"GET":  api.ListIssueTypeMappings,
-		},
-		"connections/:connectionId/type-mappings/:userType": {
-			"PUT":    api.PutIssueTypeMapping,
-			"DELETE": api.DeleteIssueTypeMapping,
-		},
-		"connections/:connectionId/type-mappings/:userType/status-mappings": {
-			"POST": api.PostIssueStatusMappings,
-			"GET":  api.ListIssueStatusMappings,
-		},
-		"connections/:connectionId/type-mappings/:userType/status-mappings/:userStatus": {
-			"PUT":    api.PutIssueStatusMapping,
-			"DELETE": api.DeleteIssueStatusMapping,
-		},
 		"connections/:connectionId/proxy/rest/*path": {
 			"GET": api.Proxy,
 		},
diff --git a/plugins/jira/models/connection.go b/plugins/jira/models/connection.go
index edb6b88f..f1ed2f8a 100644
--- a/plugins/jira/models/connection.go
+++ b/plugins/jira/models/connection.go
@@ -18,7 +18,7 @@ limitations under the License.
 package models
 
 import (
-	"github.com/apache/incubator-devlake/models/common"
+	"github.com/apache/incubator-devlake/plugins/helper"
 )
 
 type EpicResponse struct {
@@ -28,9 +28,9 @@ type EpicResponse struct {
 }
 
 type TestConnectionRequest struct {
-	Endpoint string `json:"endpoint"`
-	Auth     string `json:"auth"`
-	Proxy    string `json:"proxy"`
+	Endpoint         string `json:"endpoint"`
+	Proxy            string `json:"proxy"`
+	helper.BasicAuth `mapstructure:",squash"`
 }
 
 type BoardResponse struct {
@@ -40,15 +40,11 @@ type BoardResponse struct {
 }
 
 type JiraConnection struct {
-	common.Model
-	Name                       string `gorm:"type:varchar(100);uniqueIndex" json:"name" validate:"required"`
-	Endpoint                   string `json:"endpoint" validate:"required"`
-	BasicAuthEncoded           string `json:"basicAuthEncoded" validate:"required"`
+	helper.RestConnection      `mapstructure:",squash"`
+	helper.BasicAuth           `mapstructure:",squash"`
 	EpicKeyField               string `gorm:"type:varchar(50);" json:"epicKeyField"`
 	StoryPointField            string `gorm:"type:varchar(50);" json:"storyPointField"`
 	RemotelinkCommitShaPattern string `gorm:"type:varchar(255);comment='golang regexp, the first group will be recognized as commit sha, ref https://github.com/google/re2/wiki/Syntax'" json:"remotelinkCommitShaPattern"`
-	Proxy                      string `json:"proxy"`
-	RateLimit                  int    `comment:"api request rate limt per hour" json:"rateLimit"`
 }
 
 type JiraIssueTypeMapping struct {
diff --git a/plugins/jira/models/migrationscripts/updateSchemas20220505.go b/plugins/jira/models/migrationscripts/updateSchemas20220505.go
index 8e0e1610..ff4a6c81 100644
--- a/plugins/jira/models/migrationscripts/updateSchemas20220505.go
+++ b/plugins/jira/models/migrationscripts/updateSchemas20220505.go
@@ -19,16 +19,32 @@ package migrationscripts
 
 import (
 	"context"
-	"github.com/apache/incubator-devlake/plugins/jira/models"
+	"github.com/apache/incubator-devlake/models/common"
 	"github.com/apache/incubator-devlake/plugins/jira/models/migrationscripts/archived"
 
 	"gorm.io/gorm"
 )
 
+type JiraConnection20220505 struct {
+	common.Model
+	Name                       string `gorm:"type:varchar(100);uniqueIndex" json:"name" validate:"required"`
+	Endpoint                   string `json:"endpoint" validate:"required"`
+	BasicAuthEncoded           string `json:"basicAuthEncoded" validate:"required"`
+	EpicKeyField               string `gorm:"type:varchar(50);" json:"epicKeyField"`
+	StoryPointField            string `gorm:"type:varchar(50);" json:"storyPointField"`
+	RemotelinkCommitShaPattern string `gorm:"type:varchar(255);comment='golang regexp, the first group will be recognized as commit sha, ref https://github.com/google/re2/wiki/Syntax'" json:"remotelinkCommitShaPattern"`
+	Proxy                      string `json:"proxy"`
+	RateLimit                  int    `comment:"api request rate limt per hour" json:"rateLimit"`
+}
+
+func (JiraConnection20220505) TableName() string {
+	return "_tool_jira_connections"
+}
+
 type UpdateSchemas20220505 struct{}
 
 func (*UpdateSchemas20220505) Up(ctx context.Context, db *gorm.DB) error {
-	err := db.Migrator().RenameTable(archived.JiraSource{}, models.JiraConnection{})
+	err := db.Migrator().RenameTable(archived.JiraSource{}, JiraConnection20220505{})
 	if err != nil {
 		return err
 	}
diff --git a/plugins/jira/models/migrationscripts/updateSchemas20220524.go b/plugins/jira/models/migrationscripts/updateSchemas20220524.go
new file mode 100644
index 00000000..9c753610
--- /dev/null
+++ b/plugins/jira/models/migrationscripts/updateSchemas20220524.go
@@ -0,0 +1,72 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"context"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"gorm.io/gorm"
+)
+
+type JiraConnection20220524 struct {
+	helper.RestConnection
+	helper.BasicAuth
+	EpicKeyField               string `gorm:"type:varchar(50);" json:"epicKeyField"`
+	StoryPointField            string `gorm:"type:varchar(50);" json:"storyPointField"`
+	RemotelinkCommitShaPattern string `gorm:"type:varchar(255);comment='golang regexp, the first group will be recognized as commit sha, ref https://github.com/google/re2/wiki/Syntax'" json:"remotelinkCommitShaPattern"`
+}
+
+func (JiraConnection20220524) TableName() string {
+	return "_tool_jira_connections"
+}
+
+type UpdateSchemas20220524 struct{}
+
+func (*UpdateSchemas20220524) Up(ctx context.Context, db *gorm.DB) error {
+	var err error
+	if !db.Migrator().HasColumn(&JiraConnection20220505{}, "password") {
+		err = db.Migrator().AddColumn(&JiraConnection20220524{}, "password")
+		if err != nil {
+			return err
+		}
+	}
+
+	if !db.Migrator().HasColumn(&JiraConnection20220505{}, "username") {
+		err = db.Migrator().AddColumn(&JiraConnection20220524{}, "username")
+		if err != nil {
+			return err
+		}
+	}
+
+	if db.Migrator().HasColumn(&JiraConnection20220505{}, "basic_auth_encoded") {
+		err = db.Migrator().DropColumn(&JiraConnection20220505{}, "basic_auth_encoded")
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (*UpdateSchemas20220524) Version() uint64 {
+	return 20220507154646
+}
+
+func (*UpdateSchemas20220524) Name() string {
+	return "Add icon_url column to JiraIssue"
+}
diff --git a/plugins/jira/tasks/api_client.go b/plugins/jira/tasks/api_client.go
index 4896c05a..3673bee5 100644
--- a/plugins/jira/tasks/api_client.go
+++ b/plugins/jira/tasks/api_client.go
@@ -29,10 +29,12 @@ import (
 func NewJiraApiClient(taskCtx core.TaskContext, connection *models.JiraConnection) (*helper.ApiAsyncClient, error) {
 	// load configuration
 	encKey := taskCtx.GetConfig(core.EncodeKeyEnvStr)
-	auth, err := core.Decrypt(encKey, connection.BasicAuthEncoded)
+	decodedPassword, err := core.Decrypt(encKey, connection.Password)
 	if err != nil {
-		return nil, fmt.Errorf("Failed to decrypt Auth Token: %w", err)
+		return nil, fmt.Errorf("Failed to decrypt Auth AccessToken: %w", err)
 	}
+	connection.Password = decodedPassword
+	auth := connection.GetEncodedToken()
 
 	// create synchronize api client so we can calculate api rate limit dynamically
 	headers := map[string]string{
@@ -44,7 +46,7 @@ func NewJiraApiClient(taskCtx core.TaskContext, connection *models.JiraConnectio
 	}
 	apiClient.SetAfterFunction(func(res *http.Response) error {
 		if res.StatusCode == http.StatusUnauthorized {
-			return fmt.Errorf("authentication failed, please check your Basic Auth Token")
+			return fmt.Errorf("authentication failed, please check your AccessToken")
 		}
 		return nil
 	})
diff --git a/plugins/tapd/tasks/api_client.go b/plugins/tapd/tasks/api_client.go
index 406bc97a..977d076f 100644
--- a/plugins/tapd/tasks/api_client.go
+++ b/plugins/tapd/tasks/api_client.go
@@ -31,7 +31,7 @@ func NewTapdApiClient(taskCtx core.TaskContext, connection *models.TapdConnectio
 	encKey := taskCtx.GetConfig(core.EncodeKeyEnvStr)
 	auth, err := core.Decrypt(encKey, connection.BasicAuthEncoded)
 	if err != nil {
-		return nil, fmt.Errorf("Failed to decrypt Auth Token: %w", err)
+		return nil, fmt.Errorf("Failed to decrypt Auth AccessToken: %w", err)
 	}
 
 	// create synchronize api client so we can calculate api rate limit dynamically
@@ -44,7 +44,7 @@ func NewTapdApiClient(taskCtx core.TaskContext, connection *models.TapdConnectio
 	}
 	apiClient.SetAfterFunction(func(res *http.Response) error {
 		if res.StatusCode == http.StatusUnprocessableEntity {
-			return fmt.Errorf("authentication failed, please check your Basic Auth Token")
+			return fmt.Errorf("authentication failed, please check your AccessToken")
 		}
 		return nil
 	})