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  }