storj/cmd/storj-sim/console.go
Jeremy Wharton 3fa31c2077 satellite/console/consoleweb: remove trailing slash from URLs
This change removes the trailing slash from the account activation and
password recovery URLs, making them consistent with the rest. The URLs'
previous forms are still supported, however, in order to not invalidate
emails containing them.

Resolves storj/customer-issues#491

Change-Id: Ie774a87698d8e9edd1836611968fc3911c6cc56f
2023-02-21 19:15:36 +00:00

600 lines
13 KiB
Go

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"bufio"
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"strings"
"time"
"github.com/zeebo/errs"
"storj.io/common/uuid"
"storj.io/storj/satellite/console/consoleauth"
"storj.io/storj/satellite/payments"
)
func newConsoleEndpoints(address string) *consoleEndpoints {
return &consoleEndpoints{
client: http.DefaultClient,
base: "http://" + address,
cookieName: "_tokenKey",
}
}
type consoleEndpoints struct {
client *http.Client
base string
cookieName string
}
func (ce *consoleEndpoints) appendPath(suffix string) string {
return ce.base + suffix
}
func (ce *consoleEndpoints) RegToken() string {
return ce.appendPath("/registrationToken/?projectsLimit=1")
}
func (ce *consoleEndpoints) Register() string {
return ce.appendPath("/api/v0/auth/register")
}
func (ce *consoleEndpoints) SetupAccount() string {
return ce.appendPath("/api/v0/payments/account")
}
func (ce *consoleEndpoints) CreditCards() string {
return ce.appendPath("/api/v0/payments/cards")
}
func (ce *consoleEndpoints) Activation(token string) string {
return ce.appendPath("/activation?token=" + token)
}
func (ce *consoleEndpoints) Token() string {
return ce.appendPath("/api/v0/auth/token")
}
func (ce *consoleEndpoints) GraphQL() string {
return ce.appendPath("/api/v0/graphql")
}
func (ce *consoleEndpoints) graphqlDo(request *http.Request, jsonResponse interface{}) error {
resp, err := ce.client.Do(request)
if err != nil {
return err
}
defer func() { err = errs.Combine(err, resp.Body.Close()) }()
b, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var response struct {
Data json.RawMessage
Errors []interface{}
}
if err = json.NewDecoder(bytes.NewReader(b)).Decode(&response); err != nil {
return err
}
if response.Errors != nil {
return errs.New("inner graphql error: %v", response.Errors)
}
if jsonResponse == nil {
return errs.New("empty response: %q", b)
}
return json.NewDecoder(bytes.NewReader(response.Data)).Decode(jsonResponse)
}
func (ce *consoleEndpoints) createOrGetAPIKey(ctx context.Context) (string, error) {
authToken, err := ce.tryLogin(ctx)
if err != nil {
_ = ce.tryCreateAndActivateUser(ctx)
authToken, err = ce.tryLogin(ctx)
if err != nil {
return "", errs.Wrap(err)
}
}
err = ce.setupAccount(ctx, authToken)
if err != nil {
return "", errs.Wrap(err)
}
err = ce.addCreditCard(ctx, authToken, "test")
if err != nil {
return "", errs.Wrap(err)
}
cards, err := ce.listCreditCards(ctx, authToken)
if err != nil {
return "", errs.Wrap(err)
}
if len(cards) == 0 {
return "", errs.New("no credit card(s) found")
}
err = ce.makeCreditCardDefault(ctx, authToken, cards[0].ID)
if err != nil {
return "", errs.Wrap(err)
}
projectID, err := ce.getOrCreateProject(ctx, authToken)
if err != nil {
return "", errs.Wrap(err)
}
apiKey, err := ce.createAPIKey(ctx, authToken, projectID)
if err != nil {
return "", errs.Wrap(err)
}
return apiKey, nil
}
func (ce *consoleEndpoints) tryLogin(ctx context.Context) (string, error) {
var authToken struct {
Email string `json:"email"`
Password string `json:"password"`
}
authToken.Email = "alice@mail.test"
authToken.Password = "123a123"
res, err := json.Marshal(authToken)
if err != nil {
return "", errs.Wrap(err)
}
request, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
ce.Token(),
bytes.NewReader(res))
if err != nil {
return "", errs.Wrap(err)
}
request.Header.Add("Content-Type", "application/json")
resp, err := ce.client.Do(request)
if err != nil {
return "", errs.Wrap(err)
}
defer func() { err = errs.Combine(err, resp.Body.Close()) }()
if resp.StatusCode != http.StatusOK {
return "", errs.New("unexpected status code: %d (%q)",
resp.StatusCode, tryReadLine(resp.Body))
}
var tokenInfo struct {
Token string `json:"token"`
}
err = json.NewDecoder(resp.Body).Decode(&tokenInfo)
if err != nil {
return "", errs.Wrap(err)
}
return tokenInfo.Token, nil
}
func (ce *consoleEndpoints) tryCreateAndActivateUser(ctx context.Context) error {
regToken, err := ce.createRegistrationToken(ctx)
if err != nil {
return errs.Wrap(err)
}
userID, err := ce.createUser(ctx, regToken)
if err != nil {
return errs.Wrap(err)
}
return errs.Wrap(ce.activateUser(ctx, userID))
}
func (ce *consoleEndpoints) createRegistrationToken(ctx context.Context) (string, error) {
request, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
ce.RegToken(),
nil)
if err != nil {
return "", errs.Wrap(err)
}
resp, err := ce.client.Do(request)
if err != nil {
return "", errs.Wrap(err)
}
defer func() { err = errs.Combine(err, resp.Body.Close()) }()
if resp.StatusCode != http.StatusOK {
return "", errs.New("unexpected status code: %d (%q)",
resp.StatusCode, tryReadLine(resp.Body))
}
var createTokenResponse struct {
Secret string
Error string
}
if err = json.NewDecoder(resp.Body).Decode(&createTokenResponse); err != nil {
return "", errs.Wrap(err)
}
if createTokenResponse.Error != "" {
return "", errs.New("unable to create registration token: %s", createTokenResponse.Error)
}
return createTokenResponse.Secret, nil
}
func (ce *consoleEndpoints) createUser(ctx context.Context, regToken string) (string, error) {
var registerData struct {
FullName string `json:"fullName"`
ShortName string `json:"shortName"`
Email string `json:"email"`
Password string `json:"password"`
Secret string `json:"secret"`
}
registerData.FullName = "Alice"
registerData.Email = "alice@mail.test"
registerData.Password = "123a123"
registerData.ShortName = "al"
registerData.Secret = regToken
res, err := json.Marshal(registerData)
if err != nil {
return "", errs.Wrap(err)
}
request, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
ce.Register(),
bytes.NewReader(res))
if err != nil {
return "", errs.Wrap(err)
}
request.Header.Add("Content-Type", "application/json")
resp, err := ce.client.Do(request)
if err != nil {
return "", errs.Wrap(err)
}
defer func() { err = errs.Combine(err, resp.Body.Close()) }()
if resp.StatusCode != http.StatusOK {
return "", errs.New("unexpected status code: %d (%q)",
resp.StatusCode, tryReadLine(resp.Body))
}
var userID string
if err = json.NewDecoder(resp.Body).Decode(&userID); err != nil {
return "", errs.Wrap(err)
}
return userID, nil
}
func (ce *consoleEndpoints) activateUser(ctx context.Context, userID string) error {
userUUID, err := uuid.FromString(userID)
if err != nil {
return errs.Wrap(err)
}
activationToken, err := generateActivationKey(userUUID, "alice@mail.test", time.Now())
if err != nil {
return err
}
request, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
ce.Activation(activationToken),
nil)
if err != nil {
return err
}
resp, err := ce.client.Do(request)
if err != nil {
return err
}
defer func() { err = errs.Combine(err, resp.Body.Close()) }()
if resp.StatusCode != http.StatusOK {
return errs.New("unexpected status code: %d (%q)",
resp.StatusCode, tryReadLine(resp.Body))
}
return nil
}
func (ce *consoleEndpoints) setupAccount(ctx context.Context, token string) error {
request, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
ce.SetupAccount(),
nil)
if err != nil {
return err
}
request.AddCookie(&http.Cookie{
Name: ce.cookieName,
Value: token,
})
resp, err := ce.client.Do(request)
if err != nil {
return err
}
defer func() { err = errs.Combine(err, resp.Body.Close()) }()
if resp.StatusCode != http.StatusOK {
return errs.New("unexpected status code: %d (%q)",
resp.StatusCode, tryReadLine(resp.Body))
}
return nil
}
func (ce *consoleEndpoints) addCreditCard(ctx context.Context, token, cctoken string) error {
request, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
ce.CreditCards(),
strings.NewReader(cctoken))
if err != nil {
return err
}
request.AddCookie(&http.Cookie{
Name: ce.cookieName,
Value: token,
})
resp, err := ce.client.Do(request)
if err != nil {
return err
}
defer func() { err = errs.Combine(err, resp.Body.Close()) }()
if resp.StatusCode != http.StatusOK {
return errs.New("unexpected status code: %d (%q)",
resp.StatusCode, tryReadLine(resp.Body))
}
return nil
}
func (ce *consoleEndpoints) listCreditCards(ctx context.Context, token string) ([]payments.CreditCard, error) {
request, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
ce.CreditCards(),
nil)
if err != nil {
return nil, err
}
request.AddCookie(&http.Cookie{
Name: ce.cookieName,
Value: token,
})
resp, err := ce.client.Do(request)
if err != nil {
return nil, err
}
defer func() { err = errs.Combine(err, resp.Body.Close()) }()
if resp.StatusCode != http.StatusOK {
return nil, errs.New("unexpected status code: %d (%q)",
resp.StatusCode, tryReadLine(resp.Body))
}
var list []payments.CreditCard
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&list)
if err != nil {
return nil, err
}
return list, nil
}
func (ce *consoleEndpoints) makeCreditCardDefault(ctx context.Context, token, ccID string) error {
request, err := http.NewRequestWithContext(
ctx,
http.MethodPatch,
ce.CreditCards(),
strings.NewReader(ccID))
if err != nil {
return err
}
request.AddCookie(&http.Cookie{
Name: ce.cookieName,
Value: token,
})
resp, err := ce.client.Do(request)
if err != nil {
return err
}
defer func() { err = errs.Combine(err, resp.Body.Close()) }()
if resp.StatusCode != http.StatusOK {
return errs.New("unexpected status code: %d (%q)",
resp.StatusCode, tryReadLine(resp.Body))
}
return nil
}
func (ce *consoleEndpoints) getOrCreateProject(ctx context.Context, token string) (string, error) {
projectID, err := ce.getProject(ctx, token)
if err == nil {
return projectID, nil
}
projectID, err = ce.createProject(ctx, token)
if err == nil {
return projectID, nil
}
return ce.getProject(ctx, token)
}
func (ce *consoleEndpoints) getProject(ctx context.Context, token string) (string, error) {
request, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
ce.GraphQL(),
nil)
if err != nil {
return "", errs.Wrap(err)
}
q := request.URL.Query()
q.Add("query", `query {myProjects{id}}`)
request.URL.RawQuery = q.Encode()
request.AddCookie(&http.Cookie{
Name: ce.cookieName,
Value: token,
})
request.Header.Add("Content-Type", "application/graphql")
var getProjects struct {
MyProjects []struct {
ID string
}
}
if err := ce.graphqlDo(request, &getProjects); err != nil {
return "", errs.Wrap(err)
}
if len(getProjects.MyProjects) == 0 {
return "", errs.New("no projects")
}
return getProjects.MyProjects[0].ID, nil
}
func (ce *consoleEndpoints) createProject(ctx context.Context, token string) (string, error) {
rng := rand.NewSource(time.Now().UnixNano())
createProjectQuery := fmt.Sprintf(
`mutation {createProject(input:{name:"TestProject-%d",description:""}){id}}`,
rng.Int63())
request, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
ce.GraphQL(),
bytes.NewReader([]byte(createProjectQuery)))
if err != nil {
return "", errs.Wrap(err)
}
request.AddCookie(&http.Cookie{
Name: ce.cookieName,
Value: token,
})
request.Header.Add("Content-Type", "application/graphql")
var createProject struct {
CreateProject struct {
ID string
}
}
if err := ce.graphqlDo(request, &createProject); err != nil {
return "", errs.Wrap(err)
}
return createProject.CreateProject.ID, nil
}
func (ce *consoleEndpoints) createAPIKey(ctx context.Context, token, projectID string) (string, error) {
rng := rand.NewSource(time.Now().UnixNano())
createAPIKeyQuery := fmt.Sprintf(
`mutation {createAPIKey(projectID:%q,name:"TestKey-%d"){key}}`,
projectID, rng.Int63())
request, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
ce.GraphQL(),
bytes.NewReader([]byte(createAPIKeyQuery)))
if err != nil {
return "", errs.Wrap(err)
}
request.AddCookie(&http.Cookie{
Name: ce.cookieName,
Value: token,
})
request.Header.Add("Content-Type", "application/graphql")
var createAPIKey struct {
CreateAPIKey struct {
Key string
}
}
if err := ce.graphqlDo(request, &createAPIKey); err != nil {
return "", errs.Wrap(err)
}
return createAPIKey.CreateAPIKey.Key, nil
}
func generateActivationKey(userID uuid.UUID, email string, createdAt time.Time) (string, error) {
claims := consoleauth.Claims{
ID: userID,
Email: email,
Expiration: createdAt.Add(24 * time.Hour),
}
// TODO: change it in future, when satellite/console secret will be changed
signer := &consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")}
resJSON, err := claims.JSON()
if err != nil {
return "", err
}
token := consoleauth.Token{Payload: resJSON}
encoded := base64.URLEncoding.EncodeToString(token.Payload)
signature, err := signer.Sign([]byte(encoded))
if err != nil {
return "", err
}
token.Signature = signature
return token.String(), nil
}
func tryReadLine(r io.Reader) string {
scanner := bufio.NewScanner(r)
scanner.Scan()
return scanner.Text()
}