|
| 1 | +/* |
| 2 | +Licensed to the Apache Software Foundation (ASF) under one or more |
| 3 | +contributor license agreements. See the NOTICE file distributed with |
| 4 | +this work for additional information regarding copyright ownership. |
| 5 | +The ASF licenses this file to You under the Apache License, Version 2.0 |
| 6 | +(the "License"); you may not use this file except in compliance with |
| 7 | +the License. You may obtain a copy of the License at |
| 8 | +
|
| 9 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | +
|
| 11 | +Unless required by applicable law or agreed to in writing, software |
| 12 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | +See the License for the specific language governing permissions and |
| 15 | +limitations under the License. |
| 16 | +*/ |
| 17 | + |
| 18 | +package api |
| 19 | + |
| 20 | +import ( |
| 21 | + "context" |
| 22 | + "fmt" |
| 23 | + "net/http" |
| 24 | + |
| 25 | + "github.com/apache/incubator-devlake/core/errors" |
| 26 | + "github.com/apache/incubator-devlake/core/plugin" |
| 27 | + "github.com/apache/incubator-devlake/helpers/pluginhelper/api" |
| 28 | + "github.com/apache/incubator-devlake/plugins/taiga/models" |
| 29 | + "github.com/apache/incubator-devlake/server/api/shared" |
| 30 | +) |
| 31 | + |
| 32 | +// TaigaTestConnResponse is the response struct for testing a connection |
| 33 | +type TaigaTestConnResponse struct { |
| 34 | + shared.ApiBody |
| 35 | + Connection *models.TaigaConnection |
| 36 | +} |
| 37 | + |
| 38 | +// testConnection tests the Taiga connection |
| 39 | +func testConnection(ctx context.Context, connection models.TaigaConnection) (*TaigaTestConnResponse, errors.Error) { |
| 40 | + // If username and password are provided, authenticate to get a token |
| 41 | + if connection.Username != "" && connection.Password != "" && connection.Token == "" { |
| 42 | + // Create a temporary connection without token for authentication |
| 43 | + tempConnection := connection |
| 44 | + tempConnection.Token = "" |
| 45 | + |
| 46 | + // Create a temporary API client to call the auth endpoint |
| 47 | + tempApiClient, err := api.NewApiClientFromConnection(ctx, basicRes, &tempConnection) |
| 48 | + if err != nil { |
| 49 | + return nil, errors.Default.Wrap(err, "error creating API client") |
| 50 | + } |
| 51 | + |
| 52 | + // Prepare auth request body |
| 53 | + authBody := map[string]interface{}{ |
| 54 | + "type": "normal", |
| 55 | + "username": connection.Username, |
| 56 | + "password": connection.Password, |
| 57 | + } |
| 58 | + |
| 59 | + // Authenticate to get token |
| 60 | + authResponse := struct { |
| 61 | + AuthToken string `json:"auth_token"` |
| 62 | + }{} |
| 63 | + |
| 64 | + res, err := tempApiClient.Post("auth", nil, authBody, nil) |
| 65 | + if err != nil { |
| 66 | + return nil, errors.Default.Wrap(err, "error authenticating with Taiga") |
| 67 | + } |
| 68 | + |
| 69 | + if res.StatusCode == http.StatusUnauthorized || res.StatusCode == http.StatusBadRequest { |
| 70 | + return nil, errors.HttpStatus(http.StatusBadRequest).New("authentication failed - please check your username and password") |
| 71 | + } |
| 72 | + |
| 73 | + if res.StatusCode != http.StatusOK { |
| 74 | + return nil, errors.HttpStatus(res.StatusCode).New(fmt.Sprintf("unexpected status code during auth: %d", res.StatusCode)) |
| 75 | + } |
| 76 | + |
| 77 | + // Parse the auth response |
| 78 | + err = api.UnmarshalResponse(res, &authResponse) |
| 79 | + if err != nil { |
| 80 | + return nil, errors.Default.Wrap(err, "error parsing authentication response") |
| 81 | + } |
| 82 | + |
| 83 | + // Set the token for validation |
| 84 | + connection.Token = authResponse.AuthToken |
| 85 | + } |
| 86 | + |
| 87 | + // validate - but make Token optional if we have username/password |
| 88 | + if vld != nil { |
| 89 | + if connection.Token == "" && (connection.Username == "" || connection.Password == "") { |
| 90 | + return nil, errors.Default.New("either token or username/password must be provided") |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + apiClient, err := api.NewApiClientFromConnection(ctx, basicRes, &connection) |
| 95 | + if err != nil { |
| 96 | + return nil, err |
| 97 | + } |
| 98 | + |
| 99 | + // test connection by making a request to the user endpoint |
| 100 | + res, err := apiClient.Get("users/me", nil, nil) |
| 101 | + if err != nil { |
| 102 | + return nil, errors.Default.Wrap(err, "error testing connection") |
| 103 | + } |
| 104 | + |
| 105 | + if res.StatusCode == http.StatusUnauthorized || res.StatusCode == http.StatusForbidden { |
| 106 | + return nil, errors.HttpStatus(http.StatusBadRequest).New("authentication error when testing connection - please check your credentials") |
| 107 | + } |
| 108 | + |
| 109 | + if res.StatusCode != http.StatusOK { |
| 110 | + return nil, errors.HttpStatus(res.StatusCode).New(fmt.Sprintf("unexpected status code: %d", res.StatusCode)) |
| 111 | + } |
| 112 | + |
| 113 | + connection = connection.Sanitize() |
| 114 | + body := TaigaTestConnResponse{} |
| 115 | + body.Success = true |
| 116 | + body.Message = "success" |
| 117 | + body.Connection = &connection |
| 118 | + |
| 119 | + return &body, nil |
| 120 | +} |
| 121 | + |
| 122 | +// TestConnection tests the Taiga connection |
| 123 | +// @Summary test taiga connection |
| 124 | +// @Description Test Taiga Connection |
| 125 | +// @Tags plugins/taiga |
| 126 | +// @Param body body models.TaigaConnection true "json body" |
| 127 | +// @Success 200 {object} TaigaTestConnResponse "Success" |
| 128 | +// @Failure 400 {string} errcode.Error "Bad Request" |
| 129 | +// @Failure 500 {string} errcode.Error "Internal Error" |
| 130 | +// @Router /plugins/taiga/test [POST] |
| 131 | +func TestConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { |
| 132 | + // decode |
| 133 | + var connection models.TaigaConnection |
| 134 | + err := api.DecodeMapStruct(input.Body, &connection, false) |
| 135 | + if err != nil { |
| 136 | + return nil, err |
| 137 | + } |
| 138 | + // test connection |
| 139 | + result, err := testConnection(context.TODO(), connection) |
| 140 | + if err != nil { |
| 141 | + return nil, plugin.WrapTestConnectionErrResp(basicRes, err) |
| 142 | + } |
| 143 | + return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, nil |
| 144 | +} |
| 145 | + |
| 146 | +// TestExistingConnection tests an existing Taiga connection |
| 147 | +// @Summary test existing taiga connection |
| 148 | +// @Description Test Existing Taiga Connection |
| 149 | +// @Tags plugins/taiga |
| 150 | +// @Success 200 {object} TaigaTestConnResponse "Success" |
| 151 | +// @Failure 400 {string} errcode.Error "Bad Request" |
| 152 | +// @Failure 500 {string} errcode.Error "Internal Error" |
| 153 | +// @Router /plugins/taiga/connections/:connectionId/test [POST] |
| 154 | +func TestExistingConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { |
| 155 | + connection, err := dsHelper.ConnApi.GetMergedConnection(input) |
| 156 | + if err != nil { |
| 157 | + return nil, errors.BadInput.Wrap(err, "find connection from db") |
| 158 | + } |
| 159 | + // test connection |
| 160 | + result, err := testConnection(context.TODO(), *connection) |
| 161 | + if err != nil { |
| 162 | + return nil, plugin.WrapTestConnectionErrResp(basicRes, err) |
| 163 | + } |
| 164 | + return &plugin.ApiResourceOutput{Body: result, Status: http.StatusOK}, nil |
| 165 | +} |
| 166 | + |
| 167 | +// PostConnections creates a new Taiga connection |
| 168 | +// @Summary create taiga connection |
| 169 | +// @Description Create Taiga Connection |
| 170 | +// @Tags plugins/taiga |
| 171 | +// @Success 200 {object} models.TaigaConnection |
| 172 | +// @Failure 400 |
| 173 | +// @Failure 500 |
| 174 | +// @Router /plugins/taiga/connections [POST] |
| 175 | +func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { |
| 176 | + return dsHelper.ConnApi.Post(input) |
| 177 | +} |
| 178 | + |
| 179 | +// ListConnections lists all Taiga connections |
| 180 | +// @Summary list taiga connections |
| 181 | +// @Description List Taiga Connections |
| 182 | +// @Tags plugins/taiga |
| 183 | +// @Success 200 {object} []models.TaigaConnection |
| 184 | +// @Failure 400 |
| 185 | +// @Failure 500 |
| 186 | +// @Router /plugins/taiga/connections [GET] |
| 187 | +func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { |
| 188 | + return dsHelper.ConnApi.GetAll(input) |
| 189 | +} |
| 190 | + |
| 191 | +// GetConnection gets a Taiga connection by ID |
| 192 | +// @Summary get taiga connection |
| 193 | +// @Description Get Taiga Connection |
| 194 | +// @Tags plugins/taiga |
| 195 | +// @Success 200 {object} models.TaigaConnection |
| 196 | +// @Failure 400 |
| 197 | +// @Failure 500 |
| 198 | +// @Router /plugins/taiga/connections/:connectionId [GET] |
| 199 | +func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { |
| 200 | + return dsHelper.ConnApi.GetDetail(input) |
| 201 | +} |
| 202 | + |
| 203 | +// PatchConnection updates a Taiga connection |
| 204 | +// @Summary patch taiga connection |
| 205 | +// @Description Patch Taiga Connection |
| 206 | +// @Tags plugins/taiga |
| 207 | +// @Success 200 {object} models.TaigaConnection |
| 208 | +// @Failure 400 |
| 209 | +// @Failure 500 |
| 210 | +// @Router /plugins/taiga/connections/:connectionId [PATCH] |
| 211 | +func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { |
| 212 | + return dsHelper.ConnApi.Patch(input) |
| 213 | +} |
| 214 | + |
| 215 | +// DeleteConnection deletes a Taiga connection |
| 216 | +// @Summary delete taiga connection |
| 217 | +// @Description Delete Taiga Connection |
| 218 | +// @Tags plugins/taiga |
| 219 | +// @Success 200 |
| 220 | +// @Failure 400 |
| 221 | +// @Failure 500 |
| 222 | +// @Router /plugins/taiga/connections/:connectionId [DELETE] |
| 223 | +func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) { |
| 224 | + return dsHelper.ConnApi.Delete(input) |
| 225 | +} |
0 commit comments