diff --git a/changelog/unreleased/send-invite-link-ocm.md b/changelog/unreleased/send-invite-link-ocm.md new file mode 100644 index 0000000000..9961211f68 --- /dev/null +++ b/changelog/unreleased/send-invite-link-ocm.md @@ -0,0 +1,8 @@ +Enhancement: Send invitation link from mesh directory +when generating and listing OCM tokens + +To enhance user expirience, instead of only sending +the token, we send directly the URL for accepting the +invitation workflow. + +https://github.com/cs3org/reva/pull/3724 \ No newline at end of file diff --git a/internal/http/services/sciencemesh/email.go b/internal/http/services/sciencemesh/email.go index fa7792e220..d64545fc22 100644 --- a/internal/http/services/sciencemesh/email.go +++ b/internal/http/services/sciencemesh/email.go @@ -31,6 +31,7 @@ type emailParams struct { User *userpb.User Token string MeshDirectoryURL string + InviteLink string } const defaultSubject = `ScienceMesh: {{.User.DisplayName}} wants to collaborate with you` @@ -39,7 +40,7 @@ const defaultBody = `Hi {{.User.DisplayName}} ({{.User.Mail}}) wants to start sharing OCM resources with you. To accept the invite, please visit the following URL: -{{.MeshDirectoryURL}}?token={{.Token}}&providerDomain={{.User.Id.Idp}} +{{.InviteLink}} Alternatively, you can visit your mesh provider and use the following details: Token: {{.Token}} @@ -116,3 +117,19 @@ func (h *tokenHandler) initSubjectTemplate(subjTempl string) error { h.tplSubj = tpl return nil } + +func (h *tokenHandler) initInviteLinkTemplate(inviteTempl string) error { + var t string + if inviteTempl == "" { + t = defaultInviteLink + } else { + t = inviteTempl + } + + tpl, err := template.New("tpl_invite").Parse(t) + if err != nil { + return err + } + h.tplInviteLink = tpl + return nil +} diff --git a/internal/http/services/sciencemesh/sciencemesh.go b/internal/http/services/sciencemesh/sciencemesh.go index c1dd7946b1..e832583051 100644 --- a/internal/http/services/sciencemesh/sciencemesh.go +++ b/internal/http/services/sciencemesh/sciencemesh.go @@ -62,14 +62,15 @@ func (s *svc) Close() error { } type config struct { - Prefix string `mapstructure:"prefix"` - SMTPCredentials *smtpclient.SMTPCredentials `mapstructure:"smtp_credentials"` - GatewaySvc string `mapstructure:"gatewaysvc"` - MeshDirectoryURL string `mapstructure:"mesh_directory_url"` - ProviderDomain string `mapstructure:"provider_domain"` - SubjectTemplate string `mapstructure:"subject_template"` - BodyTemplatePath string `mapstructure:"body_template_path"` - OCMMountPoint string `mapstructure:"ocm_mount_point"` + Prefix string `mapstructure:"prefix"` + SMTPCredentials *smtpclient.SMTPCredentials `mapstructure:"smtp_credentials"` + GatewaySvc string `mapstructure:"gatewaysvc"` + MeshDirectoryURL string `mapstructure:"mesh_directory_url"` + ProviderDomain string `mapstructure:"provider_domain"` + SubjectTemplate string `mapstructure:"subject_template"` + BodyTemplatePath string `mapstructure:"body_template_path"` + OCMMountPoint string `mapstructure:"ocm_mount_point"` + InviteLinkTemplate string `mapstructure:"invite_link_template"` } func (c *config) init() { diff --git a/internal/http/services/sciencemesh/token.go b/internal/http/services/sciencemesh/token.go index bfe26efd9c..a87dfca2e4 100644 --- a/internal/http/services/sciencemesh/token.go +++ b/internal/http/services/sciencemesh/token.go @@ -24,8 +24,10 @@ import ( "html/template" "mime" "net/http" + "strings" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" @@ -36,13 +38,16 @@ import ( "github.com/cs3org/reva/pkg/smtpclient" ) +const defaultInviteLink = "{{.MeshDirectoryURL}}?token={{.Token}}&providerDomain={{.User.Id.Idp}}" + type tokenHandler struct { gatewayClient gateway.GatewayAPIClient smtpCredentials *smtpclient.SMTPCredentials meshDirectoryURL string - tplSubj *template.Template - tplBody *template.Template + tplSubj *template.Template + tplBody *template.Template + tplInviteLink *template.Template } func (h *tokenHandler) init(c *config) error { @@ -65,7 +70,21 @@ func (h *tokenHandler) init(c *config) error { if err := h.initBodyTemplate(c.BodyTemplatePath); err != nil { return err } - return nil + + return h.initInviteLinkTemplate(c.InviteLinkTemplate) +} + +type token struct { + Token string `json:"token"` + Description string `json:"description,omitempty"` + Expiration uint64 `json:"expiration,omitempty"` + InviteLink string `json:"invite_link"` +} + +type inviteLinkParams struct { + User *userpb.User + Token string + MeshDirectoryURL string } // Generate generates an invitation token and if a recipient is specified, @@ -83,10 +102,11 @@ func (h *tokenHandler) Generate(w http.ResponseWriter, r *http.Request) { return } + user := ctxpkg.ContextMustGetUser(ctx) recipient := query.Get("recipient") if recipient != "" && h.smtpCredentials != nil { templObj := &emailParams{ - User: ctxpkg.ContextMustGetUser(ctx), + User: user, Token: token.InviteToken.Token, MeshDirectoryURL: h.meshDirectoryURL, } @@ -96,7 +116,13 @@ func (h *tokenHandler) Generate(w http.ResponseWriter, r *http.Request) { } } - if err := json.NewEncoder(w).Encode(token.InviteToken); err != nil { + tknRes, err := h.prepareGenerateTokenResponse(user, token.InviteToken) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error generating response", err) + return + } + + if err := json.NewEncoder(w).Encode(tknRes); err != nil { reqres.WriteError(w, r, reqres.APIErrorServerError, "error marshalling token data", err) return } @@ -105,6 +131,36 @@ func (h *tokenHandler) Generate(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } +func (h *tokenHandler) generateInviteLink(user *userpb.User, token *invitepb.InviteToken) (string, error) { + var inviteLink strings.Builder + if err := h.tplInviteLink.Execute(&inviteLink, inviteLinkParams{ + User: user, + Token: token.Token, + MeshDirectoryURL: h.meshDirectoryURL, + }); err != nil { + return "", err + } + + return inviteLink.String(), nil +} + +func (h *tokenHandler) prepareGenerateTokenResponse(user *userpb.User, tkn *invitepb.InviteToken) (*token, error) { + inviteLink, err := h.generateInviteLink(user, tkn) + if err != nil { + return nil, err + } + res := &token{ + Token: tkn.Token, + Description: tkn.Description, + InviteLink: inviteLink, + } + if tkn.Expiration != nil { + res.Expiration = tkn.Expiration.Seconds + } + + return res, nil +} + type acceptInviteRequest struct { Token string `json:"token"` ProviderDomain string `json:"providerDomain"` @@ -221,7 +277,26 @@ func (h *tokenHandler) ListInvite(w http.ResponseWriter, r *http.Request) { return } - if err := json.NewEncoder(w).Encode(res.InviteTokens); err != nil { + tokens := make([]*token, 0, len(res.InviteTokens)) + user := ctxpkg.ContextMustGetUser(ctx) + for _, tkn := range res.InviteTokens { + inviteURL, err := h.generateInviteLink(user, tkn) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error generating invite URL from OCM token", err) + return + } + t := &token{ + Token: tkn.Token, + Description: tkn.Description, + InviteLink: inviteURL, + } + if tkn.Expiration != nil { + t.Expiration = tkn.Expiration.Seconds + } + tokens = append(tokens, t) + } + + if err := json.NewEncoder(w).Encode(tokens); err != nil { reqres.WriteError(w, r, reqres.APIErrorServerError, "error marshalling token data", err) return } diff --git a/tests/integration/grpc/ocm_invitation_test.go b/tests/integration/grpc/ocm_invitation_test.go index f44f6dbd87..0cfcfb29cb 100644 --- a/tests/integration/grpc/ocm_invitation_test.go +++ b/tests/integration/grpc/ocm_invitation_test.go @@ -44,6 +44,13 @@ import ( "google.golang.org/grpc/metadata" ) +type generateInviteResponse struct { + Token string `json:"token"` + Description string `json:"descriptions"` + Expiration uint64 `json:"expiration"` + InviteLink string `json:"invite_link"` +} + func ctxWithAuthToken(tokenManager token.Manager, user *userpb.User) context.Context { ctx := context.Background() scope, err := scope.AddOwnerScope(nil) @@ -327,7 +334,7 @@ var _ = Describe("ocm invitation workflow", func() { return users, res.StatusCode } - generateToken := func(revaToken, domain string) (*invitepb.InviteToken, int) { + generateToken := func(revaToken, domain string) (*generateInviteResponse, int) { req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, fmt.Sprintf("http://%s/sciencemesh/generate-invite", domain), nil) Expect(err).ToNot(HaveOccurred()) req.Header.Set("x-access-token", revaToken) @@ -336,9 +343,9 @@ var _ = Describe("ocm invitation workflow", func() { Expect(err).ToNot(HaveOccurred()) defer res.Body.Close() - var token invitepb.InviteToken - Expect(json.NewDecoder(res.Body).Decode(&token)).To(Succeed()) - return &token, res.StatusCode + var inviteRes generateInviteResponse + Expect(json.NewDecoder(res.Body).Decode(&inviteRes)).To(Succeed()) + return &inviteRes, res.StatusCode } Context("einstein and marie do not know each other", func() {