github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/tequilapi/endpoints/mmn.go (about) 1 /* 2 * Copyright (C) 2019 The "MysteriumNetwork/node" Authors. 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 package endpoints 19 20 import ( 21 "encoding/json" 22 "fmt" 23 "net/http" 24 "net/url" 25 "strings" 26 27 "github.com/mysteriumnetwork/node/tequilapi/sso" 28 29 "github.com/gin-gonic/gin" 30 "github.com/mysteriumnetwork/go-rest/apierror" 31 "github.com/rs/zerolog/log" 32 33 "github.com/mysteriumnetwork/node/config" 34 "github.com/mysteriumnetwork/node/mmn" 35 "github.com/mysteriumnetwork/node/tequilapi/contract" 36 "github.com/mysteriumnetwork/node/tequilapi/utils" 37 ) 38 39 const defaultTequilaLogin = "myst" 40 const defaultTequilaPass = "mystberry" 41 42 type mmnProvider interface { 43 GetString(key string) string 44 SetUser(key string, value interface{}) 45 RemoveUser(key string) 46 SaveUserConfig() error 47 } 48 49 type mmnAPI struct { 50 config mmnProvider 51 mmn *mmn.MMN 52 ssoMystnodes *sso.Mystnodes 53 authenticator authenticator 54 } 55 56 func newMMNAPI(config mmnProvider, client *mmn.MMN, ssoMystnodes *sso.Mystnodes, authenticator authenticator) *mmnAPI { 57 return &mmnAPI{config: config, mmn: client, ssoMystnodes: ssoMystnodes, authenticator: authenticator} 58 } 59 60 // GetApiKey returns MMN's API key 61 // swagger:operation GET /mmn/report MMN getApiKey 62 // 63 // --- 64 // summary: returns MMN's API key 65 // description: returns MMN's API key 66 // responses: 67 // 200: 68 // description: MMN's API key 69 // schema: 70 // "$ref": "#/definitions/MMNApiKeyRequest" 71 func (api *mmnAPI) GetApiKey(c *gin.Context) { 72 res := contract.MMNApiKeyRequest{ApiKey: api.config.GetString(config.FlagMMNAPIKey.Name)} 73 utils.WriteAsJSON(res, c.Writer) 74 } 75 76 // SetApiKey sets MMN's API key 77 // swagger:operation POST /mmn/api-key MMN setApiKey 78 // 79 // --- 80 // summary: sets MMN's API key 81 // description: sets MMN's API key 82 // parameters: 83 // - in: body 84 // name: body 85 // description: api_key field 86 // schema: 87 // $ref: "#/definitions/MMNApiKeyRequest" 88 // responses: 89 // 200: 90 // description: API key has been set 91 // 400: 92 // description: Failed to parse or request validation failed 93 // schema: 94 // "$ref": "#/definitions/APIError" 95 // 422: 96 // description: Unable to process the request at this point 97 // schema: 98 // "$ref": "#/definitions/APIError" 99 // 500: 100 // description: Internal server error 101 // schema: 102 // "$ref": "#/definitions/APIError" 103 func (api *mmnAPI) SetApiKey(c *gin.Context) { 104 var req contract.MMNApiKeyRequest 105 err := json.NewDecoder(c.Request.Body).Decode(&req) 106 if err != nil { 107 c.Error(apierror.ParseFailed()) 108 return 109 } 110 111 if err := req.Validate(); err != nil { 112 c.Error(err) 113 return 114 } 115 116 api.config.SetUser(config.FlagMMNAPIKey.Name, req.ApiKey) 117 if err = api.config.SaveUserConfig(); err != nil { 118 c.Error(apierror.Internal("Failed to save API key", contract.ErrCodeConfigSave)) 119 return 120 } 121 122 err = api.mmn.ClaimNode() 123 if err != nil { 124 log.Error().Msgf("MMN registration error: %s", err.Error()) 125 126 switch { 127 case strings.Contains(err.Error(), "authentication needed: password or unlock"): 128 c.Error(apierror.Unprocessable("Identity is locked", contract.ErrCodeIDLocked)) 129 case strings.Contains(err.Error(), "already owned"): 130 msg := fmt.Sprintf("This node has already been claimed. Please visit %s and unclaim it first.", api.config.GetString(config.FlagMMNAddress.Name)) 131 c.Error(apierror.Unprocessable(msg, contract.ErrCodeMMNNodeAlreadyClaimed)) 132 case strings.Contains(err.Error(), "invalid api key"): 133 c.Error(apierror.Unprocessable("Invalid API key", contract.ErrCodeMMNAPIKey)) 134 default: 135 c.Error(apierror.Internal("Failed to register to MMN", contract.ErrCodeMMNRegistration)) 136 } 137 return 138 } 139 } 140 141 // ClearApiKey clears MMN's API key from config 142 // swagger:operation DELETE /mmn/api-key MMN clearApiKey 143 // 144 // --- 145 // summary: Clears MMN's API key from config 146 // description: Clears MMN's API key from config 147 // responses: 148 // 200: 149 // description: MMN API key removed 150 // 500: 151 // description: Internal server error 152 // schema: 153 // "$ref": "#/definitions/APIError" 154 func (api *mmnAPI) ClearApiKey(c *gin.Context) { 155 api.config.RemoveUser(config.FlagMMNAPIKey.Name) 156 if err := api.config.SaveUserConfig(); err != nil { 157 c.Error(apierror.Internal("Failed to clear API key", contract.ErrCodeConfigSave)) 158 return 159 } 160 } 161 162 // GetClaimLink generates claim link for mystnodes.com 163 // swagger:operation GET /mmn/claim-link MMN getClaimLink 164 // 165 // --- 166 // 167 // summary: Generate claim link 168 // description: Generates claim link to claim Node on mystnodes.com with a click 169 // responses: 170 // 200: 171 // description: Link response 172 // schema: 173 // "$ref": "#/definitions/MMNLinkRedirectResponse" 174 // 500: 175 // description: Internal server error 176 // schema: 177 // "$ref": "#/definitions/APIError" 178 func (api *mmnAPI) GetClaimLink(c *gin.Context) { 179 redirectQuery := c.Query("redirect_uri") 180 181 if redirectQuery == "" { 182 c.Error(apierror.BadRequest("redirect_uri missing", contract.ErrCodeMMNClaimRedirectURLMissing)) 183 return 184 } 185 186 redirectURL, err := url.Parse(redirectQuery) 187 if err != nil { 188 c.Error(apierror.BadRequest("redirect_uri malformed", contract.ErrCodeMMNClaimRedirectURLMissing)) 189 return 190 } 191 192 link, err := api.mmn.ClaimLink(redirectURL) 193 if err != nil { 194 c.Error(apierror.Internal("Failed to generate claim link", contract.ErrCodeMMNClaimLink)) 195 return 196 } 197 198 c.JSON(http.StatusOK, &contract.MMNLinkRedirectResponse{Link: link.String()}) 199 } 200 201 func (api *mmnAPI) isDefaultCredentials() bool { 202 err := api.authenticator.CheckCredentials(defaultTequilaLogin, defaultTequilaPass) 203 return err == nil 204 } 205 206 func (api *mmnAPI) isApiKeySet() bool { 207 apiKey := api.config.GetString(api.config.GetString(config.FlagMMNAPIKey.Name)) 208 return apiKey != "" 209 } 210 211 // GetOnboardingLink generates claim link for mystnodes.com 212 // swagger:operation GET /mmn/onboarding MMN GetOnboardingLink 213 // 214 // --- 215 // 216 // summary: Generate onboarding link 217 // description: Generates onboarding link for one click onboarding 218 // responses: 219 // 200: 220 // description: Link response 221 // schema: 222 // "$ref": "#/definitions/MMNLinkRedirectResponse" 223 // 500: 224 // description: Internal server error 225 // schema: 226 // "$ref": "#/definitions/APIError" 227 func (api *mmnAPI) GetOnboardingLink(c *gin.Context) { 228 if !api.isDefaultCredentials() || api.isApiKeySet() { 229 c.Error(apierror.Unauthorized()) 230 return 231 } 232 233 redirectQuery := c.Query("redirect_uri") 234 235 if redirectQuery == "" { 236 c.Error(apierror.BadRequest("redirect_uri missing", contract.ErrCodeMMNClaimRedirectURLMissing)) 237 return 238 } 239 240 redirectURL, err := url.Parse(redirectQuery) 241 if err != nil { 242 c.Error(apierror.BadRequest("redirect_uri malformed", contract.ErrCodeMMNClaimRedirectURLMissing)) 243 return 244 } 245 246 link, err := api.ssoMystnodes.SSOLink(redirectURL) 247 link.Path = "/clickboarding" 248 if err != nil { 249 c.Error(apierror.Internal("Failed to generate claim link", contract.ErrCodeMMNClaimLink)) 250 return 251 } 252 253 c.JSON(http.StatusOK, &contract.MMNLinkRedirectResponse{Link: link.String()}) 254 } 255 256 // VerifyGrant verify grant 257 // swagger:operation POST /mmn/onboarding MMN VerifyGrant 258 // 259 // --- 260 // 261 // summary: verify grant for onboarding 262 // description: verify grant for onboarding 263 // responses: 264 // 200: 265 // description: Link response 266 // schema: 267 // "$ref": "#/definitions/MMNGrantVerificationResponse" 268 // 500: 269 // description: Internal server error 270 // schema: 271 // "$ref": "#/definitions/APIError" 272 func (api *mmnAPI) VerifyGrant(c *gin.Context) { 273 if !api.isDefaultCredentials() || api.isApiKeySet() { 274 c.Error(apierror.Unauthorized()) 275 return 276 } 277 278 var request contract.MystnodesSSOGrantLoginRequest 279 err := json.NewDecoder(c.Request.Body).Decode(&request) 280 if err != nil { 281 log.Err(err).Msg("failed decoding request") 282 c.Error(apierror.Unauthorized()) 283 return 284 } 285 286 vi, err := api.ssoMystnodes.VerifyAuthorizationGrant(request.AuthorizationGrant, sso.VerificationOptions{SkipNodeClaimCheck: true}) 287 if err != nil { 288 log.Err(err).Msg("failed to verify mystnodes Authorization Grant") 289 c.Error(apierror.Unauthorized()) 290 return 291 } 292 293 r := contract.MMNGrantVerificationResponse{ 294 ApiKey: vi.APIkey, 295 WalletAddress: vi.WalletAddress, 296 IsEligibleForFreeRegistration: vi.IsEligibleForFreeRegistration, 297 } 298 299 c.JSON(http.StatusOK, r) 300 } 301 302 // AddRoutesForMMN registers /mmn endpoints in Tequilapi 303 func AddRoutesForMMN( 304 mmn *mmn.MMN, 305 ssoMystnodes *sso.Mystnodes, 306 authenticator authenticator, 307 ) func(*gin.Engine) error { 308 api := newMMNAPI(config.Current, mmn, ssoMystnodes, authenticator) 309 return func(e *gin.Engine) error { 310 g := e.Group("/mmn") 311 { 312 g.GET("/api-key", api.GetApiKey) 313 g.POST("/api-key", api.SetApiKey) 314 g.DELETE("/api-key", api.ClearApiKey) 315 316 g.GET("/claim-link", api.GetClaimLink) 317 318 g.GET("/onboarding", api.GetOnboardingLink) 319 g.POST("/onboarding/verify-grant", api.VerifyGrant) 320 } 321 return nil 322 } 323 }