Skip to content

Commit

Permalink
48 update collaborators workflow (#50)
Browse files Browse the repository at this point in the history
feat(collaboration): changes the collaborators implementation to an invitation based workflow
  • Loading branch information
rohitkumbhar authored Feb 9, 2025
1 parent 45ae5d6 commit 0eafa10
Show file tree
Hide file tree
Showing 27 changed files with 994 additions and 230 deletions.
41 changes: 30 additions & 11 deletions backend/app/surmai.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,36 @@ func (surmai *SurmaiApp) BuildTimezoneFinder() {

}

func (surmai *SurmaiApp) StartDemoMode() {
func (surmai *SurmaiApp) BindEventHooks() {
surmai.Pb.OnRecordCreate("trips").BindFunc(func(e *core.RecordEvent) error {
return hooks.AddTimezoneToDestinations(e, surmai.TimezoneFinder)
})

surmai.Pb.OnRecordUpdate("trips").BindFunc(func(e *core.RecordEvent) error {
return hooks.AddTimezoneToDestinations(e, surmai.TimezoneFinder)
})

surmai.Pb.OnRecordCreateRequest("invitations").BindFunc(hooks.CreateInvitationEventHook)
surmai.Pb.OnRecordUpdateRequest("invitations").BindFunc(hooks.UpdateInvitationEventHook)
}

func (surmai *SurmaiApp) StartJobs() {
surmai.startInvitationCleanupJob()
surmai.startDemoModeSetupJob()
}

func (surmai *SurmaiApp) startInvitationCleanupJob() {

job := &jobs.CleanupInvitationsJob{
Pb: surmai.Pb,
}

surmai.Pb.Cron().MustAdd("CleanupInvitationsJob", "0 * * * *", func() {
job.Execute()
})
}

func (surmai *SurmaiApp) startDemoModeSetupJob() {
if surmai.DemoMode {

password := os.Getenv("SURMAI_DEMO_PASSWORD")
Expand All @@ -81,13 +110,3 @@ func (surmai *SurmaiApp) StartDemoMode() {
})
}
}

func (surmai *SurmaiApp) BindEventHooks() {
surmai.Pb.OnRecordCreate("trips").BindFunc(func(e *core.RecordEvent) error {
return hooks.AddTimezoneToDestinations(e, surmai.TimezoneFinder)
})

surmai.Pb.OnRecordUpdate("trips").BindFunc(func(e *core.RecordEvent) error {
return hooks.AddTimezoneToDestinations(e, surmai.TimezoneFinder)
})
}
5 changes: 3 additions & 2 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ require (
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/oauth2 v0.25.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
Expand All @@ -77,7 +78,7 @@ require (
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect
modernc.org/gc/v3 v3.0.0-20250105121824-520be1a3aee6 // indirect
modernc.org/libc v1.61.10 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect
modernc.org/sqlite v1.34.4 // indirect
Expand Down
32 changes: 16 additions & 16 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,8 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand All @@ -316,8 +316,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down Expand Up @@ -405,26 +405,26 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.15 h1:wFDan71KnYqeHz4eF63vmGE6Q6Pc0PUGDpP0PRMYjDc=
modernc.org/ccgo/v4 v4.23.15/go.mod h1:nJX30dks/IWuBOnVa7VRii9Me4/9TZ1SC9GNtmARTy0=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.2 h1:YBXi5Kqp6aCK3fIxwKQ3/fErvawVKwjOLItxj1brGds=
modernc.org/gc/v2 v2.6.2/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20250105121824-520be1a3aee6 h1:JoKwHjIFumiKrjMbp1cNbC5E9UyCgA/ZcID0xOWQ2N8=
modernc.org/gc/v3 v3.0.0-20250105121824-520be1a3aee6/go.mod h1:LG5UO1Ran4OO0JRKz2oNiXhR5nNrgz0PzH7UKhz0aMU=
modernc.org/libc v1.61.10 h1:zPPaT7/dnMkTzG8b9HjIsvxWr4Ixk3Ce/WPuxakHj7Q=
modernc.org/libc v1.61.10/go.mod h1:HHX+srFdn839oaJRd0W8hBM3eg+mieyZCAjWwB08/nM=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
Expand Down
28 changes: 16 additions & 12 deletions backend/hooks/add_timezone_to_destinations.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package hooks

import (
"backend/trips"
bt "backend/types"
"encoding/json"
"github.com/pocketbase/pocketbase/core"
"github.com/ringsaturn/tzf"
Expand All @@ -13,16 +13,24 @@ func AddTimezoneToDestinations(e *core.RecordEvent, finder tzf.F) error {
record := e.Record
destinations := record.GetString("destinations")

var payload []trips.Destination
var payload []bt.Destination
err := json.Unmarshal([]byte((destinations)), &payload)

var updatedDestinations = make([]trips.Destination, len(payload))
var updatedDestinations = make([]bt.Destination, len(payload))

if err != nil {
return err
}

for i, destination := range payload {

updatedDestination := bt.Destination{
Id: destination.Id,
Name: destination.Name,
}

updatedDestinations[i] = updatedDestination

if destination.Latitude != "" && destination.Longitude != "" {

timezone := destination.TimeZone
Expand All @@ -32,15 +40,11 @@ func AddTimezoneToDestinations(e *core.RecordEvent, finder tzf.F) error {
timezone = finder.GetTimezoneName(long, lat)
}

updatedDestinations[i] = trips.Destination{
Id: destination.Id,
Name: destination.Name,
StateName: destination.StateName,
CountryName: destination.CountryName,
Latitude: destination.Latitude,
Longitude: destination.Longitude,
TimeZone: timezone,
}
updatedDestination.CountryName = destination.CountryName
updatedDestination.StateName = destination.StateName
updatedDestination.Latitude = destination.Latitude
updatedDestination.Longitude = destination.Longitude
updatedDestination.TimeZone = timezone
}
}

Expand Down
208 changes: 208 additions & 0 deletions backend/hooks/create_invitation_event_hook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package hooks

import (
bt "backend/types"
"bytes"
"errors"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/mailer"
"html/template"
"net/mail"
"time"
)

const InvitationEmail = `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org=/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body, html {
padding: 0;
margin: 0;
border: 0;
color: #16161a;
background: #fff;
font-size: 14px;
line-height: 20px;
font-weight: normal;
font-family: Source Sans Pro, sans-serif, emoji;
}
body {
padding: 20px 30px;
}
strong {
font-weight: bold;
}
em, i {
font-style: italic;
}
p {
display: block;
margin: 10px 0;
font-family: inherit;
}
small {
font-size: 12px;
line-height: 16px;
}
hr {
display: block;
height: 1px;
border: 0;
width: 100%;
background: #e1e6ea;
margin: 10px 0;
}
a {
color: inherit;
}
.hidden {
display: none !important;
}
.btn {
display: inline-block;
vertical-align: top;
border: 0;
cursor: pointer;
color: #fff !important;
background: #16161a !important;
text-decoration: none !important;
line-height: 40px;
width: auto;
min-width: 150px;
text-align: center;
padding: 0 20px;
margin: 5px 0;
font-family: Source Sans Pro, sans-serif, emoji;;
font-size: 14px;
font-weight: bold;
border-radius: 6px;
box-sizing: border-box;
}
</style>
</head>
<body>
<p>Hello,</p>
<p>{{ .senderName }} has invited you to collaborate on "{{ .tripName }}"</p>
<p>Invitation Message:</p>
<p style="border:1px solid #ccc; padding: 5px 5px 5px 5px"> {{ .invitationMessage }}</p>
<a class="btn" href="{{ .applicationUrl }}/invitations" target="_blank">View Invitation</a>
<p>This invitation will expire in 1 week.</p>
<p><i>If you do not have an account, you will have to create with this email address.</i></p>
<p></p>
<p>
Thanks,<br/>
Surmai team
</p>
</body>
</html>
`

func CreateInvitationEventHook(e *core.RecordRequestEvent) error {

info, err := e.RequestInfo()
if err != nil {
return err
}

record := e.Record
tripId := record.GetString("trip")
recipientEmail := record.GetString("recipientEmail")

trip, err := e.App.FindRecordById("trips", tripId)
if err != nil {
return err
}

// user can edit the trip
accessRecord, err := e.App.CanAccessRecord(trip, info, trip.Collection().UpdateRule)
if err != nil {
return err
}

if !accessRecord {
return errors.New("user cannot access this record")
}

// Verify open invitations for the trip
existingInvitations, err := e.App.FindAllRecords("invitations",
dbx.NewExp("trip = {:tripId} and recipientEmail = {:email} and status = {:status}",
dbx.Params{"email": recipientEmail, "tripId": tripId, "status": "open"}))
if err != nil {
return err
}

if len(existingInvitations) > 0 {
// don't add another invitation
return nil
}

senderId, metadata, err2 := buildMetadata(e, info, trip)
if err2 != nil {
return err2
}

record.Set("metadata", metadata)
record.Set("from", senderId)
record.Set("expiresOn", time.Now().Add(24*7*time.Hour))
record.Set("status", bt.Open.String())

err = e.Next()

if err != nil {
return err
}

// send email

var emailContents bytes.Buffer

invitationEmailTemplate := template.Must(template.New("InvitationEmail").Parse(InvitationEmail))
err = invitationEmailTemplate.Execute(&emailContents, map[string]interface{}{
"senderName": info.Auth.GetString("name"),
"applicationUrl": e.App.Settings().Meta.AppURL,
"tripId": tripId,
"tripName": trip.GetString("name"),
"invitationMessage": record.GetString("message"),
})
if err != nil {
return err
}

message := &mailer.Message{
From: mail.Address{
Address: e.App.Settings().Meta.SenderAddress,
Name: e.App.Settings().Meta.SenderName,
},
To: []mail.Address{{Address: recipientEmail}},
Subject: "[surmai] Invitation to collaborate",
HTML: emailContents.String(),
}

return e.App.NewMailClient().Send(message)
}

func buildMetadata(e *core.RecordRequestEvent, info *core.RequestInfo, trip *core.Record) (string, map[string]interface{}, error) {
senderId := info.Auth.Id
sender, err := e.App.FindRecordById("users", senderId)
if err != nil {
return "", nil, err
}

metadata := make(map[string]interface{})
senderMetadata := make(map[string]string)
tripMetadata := make(map[string]interface{})

tripMetadata["name"] = trip.GetString("name")
tripMetadata["description"] = trip.GetString("description")
tripMetadata["startDate"] = trip.GetDateTime("startDate")
tripMetadata["endDate"] = trip.GetDateTime("endDate")
metadata["trip"] = tripMetadata

senderMetadata["name"] = sender.GetString("name")
metadata["sender"] = senderMetadata
return senderId, metadata, nil
}
Loading

0 comments on commit 0eafa10

Please sign in to comment.