github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/move/sharing.go (about)

     1  package move
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"net/http"
     8  	"net/url"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/cozy/cozy-stack/client/request"
    13  	"github.com/cozy/cozy-stack/model/instance"
    14  	"github.com/cozy/cozy-stack/model/oauth"
    15  	"github.com/cozy/cozy-stack/model/permission"
    16  	"github.com/cozy/cozy-stack/model/sharing"
    17  	"github.com/cozy/cozy-stack/pkg/consts"
    18  	"github.com/cozy/cozy-stack/pkg/couchdb"
    19  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    20  	multierror "github.com/hashicorp/go-multierror"
    21  	"github.com/labstack/echo/v4"
    22  )
    23  
    24  // NotifySharings will notify other instances with a common sharing that this
    25  // instance has moved, and will tell them to the new URL to use for the
    26  // sharing.
    27  func NotifySharings(inst *instance.Instance) error {
    28  	// Let the dust settle a bit before starting the notifications
    29  	time.Sleep(3 * time.Second)
    30  
    31  	var sharings []*sharing.Sharing
    32  	req := couchdb.AllDocsRequest{Limit: 1000}
    33  	if err := couchdb.GetAllDocs(inst, consts.Sharings, &req, &sharings); err != nil {
    34  		return err
    35  	}
    36  
    37  	var errm error
    38  	for _, s := range sharings {
    39  		if strings.HasPrefix(s.ID(), "_design") {
    40  			continue
    41  		}
    42  		time.Sleep(100 * time.Millisecond)
    43  		if err := notifySharing(inst, s); err != nil {
    44  			errm = multierror.Append(errm, err)
    45  		}
    46  	}
    47  	return errm
    48  }
    49  
    50  func notifySharing(inst *instance.Instance, s *sharing.Sharing) error {
    51  	if !s.Owner {
    52  		return notifyMember(inst, s, 0)
    53  	}
    54  
    55  	var errm error
    56  	for i := range s.Members {
    57  		if i == 0 {
    58  			continue // skip the owner
    59  		}
    60  		if err := notifyMember(inst, s, i); err != nil {
    61  			errm = multierror.Append(errm, err)
    62  		}
    63  	}
    64  	return errm
    65  }
    66  
    67  func notifyMember(inst *instance.Instance, s *sharing.Sharing, index int) error {
    68  	u, err := url.Parse(s.Members[index].Instance)
    69  	if s.Members[index].Instance == "" || err != nil {
    70  		return err
    71  	}
    72  
    73  	clientID := s.Credentials[0].InboundClientID
    74  	if index > 0 {
    75  		clientID = s.Credentials[index-1].InboundClientID
    76  	}
    77  	cli := &oauth.Client{ClientID: clientID}
    78  	newToken, err := sharing.CreateAccessToken(inst, cli, s.ID(), permission.ALL)
    79  	if err != nil {
    80  		return err
    81  	}
    82  	moved := sharing.APIMoved{
    83  		SharingID:    s.ID(),
    84  		NewInstance:  inst.PageURL("", nil),
    85  		AccessToken:  newToken.AccessToken,
    86  		RefreshToken: newToken.RefreshToken,
    87  	}
    88  	data, err := jsonapi.MarshalObject(&moved)
    89  	if err != nil {
    90  		return err
    91  	}
    92  	body, err := json.Marshal(jsonapi.Document{Data: &data})
    93  	if err != nil {
    94  		return err
    95  	}
    96  
    97  	credIndex := 0
    98  	if s.Owner {
    99  		credIndex = index - 1
   100  	}
   101  	if len(s.Credentials) <= credIndex || s.Credentials[credIndex].AccessToken == nil {
   102  		return errors.New("sharing in invalid state")
   103  	}
   104  	token := s.Credentials[credIndex].AccessToken.AccessToken
   105  
   106  	opts := &request.Options{
   107  		Method: http.MethodPost,
   108  		Scheme: u.Scheme,
   109  		Domain: u.Host,
   110  		Path:   "/sharings/" + s.SID + "/recipients/self/moved",
   111  		Headers: request.Headers{
   112  			echo.HeaderAccept:        jsonapi.ContentType,
   113  			echo.HeaderContentType:   jsonapi.ContentType,
   114  			echo.HeaderAuthorization: "Bearer " + token,
   115  		},
   116  		Body:       bytes.NewReader(body),
   117  		ParseError: sharing.ParseRequestError,
   118  	}
   119  	res, err := request.Req(opts)
   120  	if res != nil && res.StatusCode/100 == 4 {
   121  		res, err = sharing.RefreshToken(inst, err, s, &s.Members[index], &s.Credentials[credIndex], opts, body)
   122  	}
   123  	if err != nil {
   124  		return err
   125  	}
   126  	defer res.Body.Close()
   127  
   128  	return nil
   129  }