mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-11-01 01:58:34 -05:00
Merge pull request #3723 from aduffeck/send-guestlink-renewal-emails
Add email notifications when a guest link has been renewed
This commit is contained in:
6 files changed
+106
-16
No files matched your search
@@ -18,7 +18,9 @@ links feature and start the service.
|
||||
- **Consumes** the share lifecycle events `ShareCreated`, `ShareRemoved` and
|
||||
`ShareExpired`.
|
||||
- **Publishes** the `GuestTokenCreated` event (initial token) and the
|
||||
`GuestTokenRenewed` event (refresh, carrying the new token and the PIN).
|
||||
`GuestTokenRenewed` event (refresh, carrying the new token and the PIN). The
|
||||
`notifications` service consumes both and mails the link (and the PIN) to
|
||||
the guest.
|
||||
- Exposes unauthenticated endpoints to redeem a token, renew it and verify a
|
||||
PIN; redeeming a token or verifying a PIN sets a session cookie.
|
||||
- Stores only hashes of the token secret and of the PIN and deletes the stored
|
||||
@@ -111,7 +113,7 @@ sequenceDiagram
|
||||
Web->>+Renew: POST renew { permissionId } (+ session cookie or link token)
|
||||
Note right of Renew: Validate share, generate token + PIN, store hashes
|
||||
Renew->>Bus: publish GuestTokenRenewed
|
||||
Note right of Bus: External consumer delivers token + PIN (out of scope)
|
||||
Note right of Bus: notifications service mails the new link + PIN to the guest
|
||||
Renew->>-Web: 200 OK
|
||||
|
||||
alt Guest opens the new link
|
||||
|
||||
@@ -92,6 +92,7 @@ func Server(cfg *config.Config) *cobra.Command {
|
||||
events.SendEmailsEvent{},
|
||||
ocEvents.ResourceMention{},
|
||||
ocEvents.GuestTokenCreated{},
|
||||
ocEvents.GuestTokenRenewed{},
|
||||
}
|
||||
registeredEvents := make(map[string]events.Unmarshaller)
|
||||
for _, e := range evs {
|
||||
|
||||
@@ -67,6 +67,25 @@ The link is only valid for 30 minutes.`),
|
||||
CallToAction: l10n.Template(`Click here to view it: {ShareLink}`),
|
||||
}
|
||||
|
||||
GuestLinkShareRenewed = MessageTemplate{
|
||||
textTemplate: _textTemplate,
|
||||
htmlTemplate: _htmlTemplate,
|
||||
// GuestLinkShareRenewed email template, Subject field (resolves directly)
|
||||
Subject: l10n.Template(`Your new link to '{ShareFolder}'`),
|
||||
// GuestLinkShareRenewed email template, resolves via {{ .Greeting }}
|
||||
Greeting: l10n.Template(`Hello,`),
|
||||
// GuestLinkShareRenewed email template, resolves via {{ .MessageBody }}
|
||||
MessageBody: l10n.Template(`A new link to "{ShareFolder}", shared with you by {ShareSharer}, has been requested.
|
||||
|
||||
Your PIN: {SharePin}
|
||||
|
||||
Open the link below or enter the PIN on the page where you requested the new link. The link and the PIN are personal and can only be used once. Please do not forward them.
|
||||
|
||||
The link and the PIN are only valid for 30 minutes.`),
|
||||
// GuestLinkShareRenewed email template, resolves via {{ .CallToAction }}
|
||||
CallToAction: l10n.Template(`Click here to view it: {ShareLink}`),
|
||||
}
|
||||
|
||||
// Spaces templates
|
||||
SharedSpace = MessageTemplate{
|
||||
textTemplate: _textTemplate,
|
||||
@@ -166,6 +185,7 @@ var _placeholders = map[string]string{
|
||||
"{ShareFolder}": "{{ .ShareFolder }}",
|
||||
"{ShareGrantee}": "{{ .ShareGrantee }}",
|
||||
"{ShareLink}": "{{ .ShareLink }}",
|
||||
"{SharePin}": "{{ .SharePin }}",
|
||||
"{SpaceName}": "{{ .SpaceName }}",
|
||||
"{SpaceGrantee}": "{{ .SpaceGrantee }}",
|
||||
"{SpaceSharer}": "{{ .SpaceSharer }}",
|
||||
|
||||
@@ -6,7 +6,10 @@ package service
|
||||
import (
|
||||
"context"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
||||
|
||||
ocevents "github.com/opencloud-eu/opencloud/pkg/events"
|
||||
@@ -23,11 +26,36 @@ func (s eventsNotifier) handleGuestTokenCreated(e ocevents.GuestTokenCreated) {
|
||||
Str("itemid", e.ItemID.GetOpaqueId()).
|
||||
Logger()
|
||||
|
||||
if err := validate.Var(e.GranteeEmail, "required,email"); err != nil {
|
||||
s.sendGuestLinkMail(logger, email.GuestLinkShareCreated, e.Sharer, e.GranteeEmail, e.ItemID, e.ResourceName, e.Token, nil)
|
||||
}
|
||||
|
||||
// handleGuestTokenRenewed sends the renewed guest link and the PIN to the
|
||||
// guest's email address, like handleGuestTokenCreated.
|
||||
func (s eventsNotifier) handleGuestTokenRenewed(e ocevents.GuestTokenRenewed) {
|
||||
logger := s.logger.With().
|
||||
Str("event", "GuestTokenRenewed").
|
||||
Str("itemid", e.ItemID.GetOpaqueId()).
|
||||
Logger()
|
||||
|
||||
if e.Pin == "" {
|
||||
logger.Error().Msg("guest pin is empty")
|
||||
return
|
||||
}
|
||||
|
||||
s.sendGuestLinkMail(logger, email.GuestLinkShareRenewed, e.Sharer, e.GranteeEmail, e.ItemID, "", e.Token,
|
||||
map[string]string{"SharePin": e.Pin})
|
||||
}
|
||||
|
||||
// sendGuestLinkMail renders the guest link template and sends it to the guest.
|
||||
// The resource is stat'ed when resourceName is empty. vars are passed to the
|
||||
// template in addition to ShareSharer, ShareFolder and ShareLink.
|
||||
func (s eventsNotifier) sendGuestLinkMail(logger zerolog.Logger, template email.MessageTemplate, sharer *user.UserId,
|
||||
granteeEmail string, itemID *provider.ResourceId, resourceName, token string, vars map[string]string) {
|
||||
if err := validate.Var(granteeEmail, "required,email"); err != nil {
|
||||
logger.Error().Err(err).Msg("invalid guest email address")
|
||||
return
|
||||
}
|
||||
if e.Token == "" {
|
||||
if token == "" {
|
||||
logger.Error().Msg("guest token is empty")
|
||||
return
|
||||
}
|
||||
@@ -44,15 +72,15 @@ func (s eventsNotifier) handleGuestTokenCreated(e ocevents.GuestTokenCreated) {
|
||||
return
|
||||
}
|
||||
|
||||
owner, err := utils.GetUserNoGroups(ctx, e.Sharer, gatewayClient)
|
||||
owner, err := utils.GetUserNoGroups(ctx, sharer, gatewayClient)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not get user")
|
||||
return
|
||||
}
|
||||
|
||||
shareFolder := e.ResourceName
|
||||
shareFolder := resourceName
|
||||
if shareFolder == "" {
|
||||
resourceInfo, err := s.getResourceInfo(ctx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
|
||||
resourceInfo, err := s.getResourceInfo(ctx, itemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}})
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not stat resource")
|
||||
return
|
||||
@@ -60,25 +88,29 @@ func (s eventsNotifier) handleGuestTokenCreated(e ocevents.GuestTokenCreated) {
|
||||
shareFolder = resourceInfo.GetName()
|
||||
}
|
||||
|
||||
shareLink, err := urlJoinPath(s.openCloudURL, "g", e.Token)
|
||||
shareLink, err := urlJoinPath(s.openCloudURL, "g", token)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not create guest link")
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := email.RenderEmailTemplate(email.GuestLinkShareCreated, s.defaultLanguage, s.defaultLanguage,
|
||||
s.emailTemplatePath, s.translationPath,
|
||||
map[string]string{
|
||||
"ShareSharer": owner.GetDisplayName(),
|
||||
"ShareFolder": shareFolder,
|
||||
"ShareLink": shareLink,
|
||||
})
|
||||
templateVars := map[string]string{
|
||||
"ShareSharer": owner.GetDisplayName(),
|
||||
"ShareFolder": shareFolder,
|
||||
"ShareLink": shareLink,
|
||||
}
|
||||
for k, v := range vars {
|
||||
templateVars[k] = v
|
||||
}
|
||||
|
||||
msg, err := email.RenderEmailTemplate(template, s.defaultLanguage, s.defaultLanguage,
|
||||
s.emailTemplatePath, s.translationPath, templateVars)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not render the email")
|
||||
return
|
||||
}
|
||||
msg.Sender = owner.GetDisplayName()
|
||||
msg.Recipient = []string{e.GranteeEmail}
|
||||
msg.Recipient = []string{granteeEmail}
|
||||
|
||||
s.send(ctx, []*channels.Message{msg})
|
||||
}
|
||||
@@ -133,6 +133,8 @@ EventLoop:
|
||||
s.handleShareRemoved(e, evt.ID)
|
||||
case ocEvents.GuestTokenCreated:
|
||||
s.handleGuestTokenCreated(e)
|
||||
case ocEvents.GuestTokenRenewed:
|
||||
s.handleGuestTokenRenewed(e)
|
||||
case events.ScienceMeshInviteTokenGenerated:
|
||||
s.handleScienceMeshInviteTokenGenerated(e)
|
||||
case events.SendEmailsEvent:
|
||||
|
||||
@@ -338,6 +338,39 @@ https://opencloud.eu
|
||||
Token: "v1.sharehash.secret",
|
||||
},
|
||||
}),
|
||||
|
||||
Entry("Guest Token Renewed", testChannel{
|
||||
expectedReceipients: []string{"guest@example.com"},
|
||||
expectedSubject: "Your new link to 'secrets of the board'",
|
||||
expectedTextBody: `Hello,
|
||||
|
||||
A new link to "secrets of the board", shared with you by Dr. S. Harer, has been requested.
|
||||
|
||||
Your PIN: 123456
|
||||
|
||||
Open the link below or enter the PIN on the page where you requested the new link. The link and the PIN are personal and can only be used once. Please do not forward them.
|
||||
|
||||
The link and the PIN are only valid for 30 minutes.
|
||||
|
||||
Click here to view it: https://cloud.example.com/g/v1.sharehash.newsecret
|
||||
|
||||
|
||||
---
|
||||
OpenCloud - a safe home for all your data
|
||||
https://opencloud.eu
|
||||
`,
|
||||
expectedSender: sharer.GetDisplayName(),
|
||||
expectedLink: testOpenCloudURL + "/g/v1.sharehash.newsecret",
|
||||
done: make(chan struct{}),
|
||||
}, events.Event{
|
||||
Event: ocEvents.GuestTokenRenewed{
|
||||
Sharer: sharer.GetId(),
|
||||
GranteeEmail: "guest@example.com",
|
||||
ItemID: resourceid,
|
||||
Token: "v1.sharehash.newsecret",
|
||||
Pin: "123456",
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user