Skip to content

Commit

Permalink
Add data privacy export / wipe features (aimed at GDPR compliance).
Browse files Browse the repository at this point in the history
- Toggle options to enable self-service data export and wipe
  options on the public unsubscription page. Subscribers can get
  a copy of all data on them e-mailed to them as JSON, or
  instantly wipe all their data.
- Refactor "unsubscribe" pages and URIs to "subscription".
- Add export icon to subscriber admin view.
  • Loading branch information
knadh committed Jul 21, 2019
1 parent d390bc9 commit 3b79028
Show file tree
Hide file tree
Showing 17 changed files with 659 additions and 125 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ Alternatively, to run a demo of listmonk, you can quickly spin up a container `d
- DB migrations
- Bounce tracking
- User auth, management, permissions
- Privacy features for subscribers (Download and wipe all tracking data)
- Ability to write raw campaign logs to a target
- Analytics views and reports
- Make Ant design UI components responsive
Expand Down
21 changes: 19 additions & 2 deletions config.toml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ concurrency = 100
max_send_errors = 1000


[privacy]
# Allow subscribers to export data recorded on them?
allow_export = false

# Items to include in the data export.
# profile Subscriber's profile including custom attributes
# subscriptions Subscriber's subscription lists (private list names are masked)
# campaign_views Campaigns the subscriber has viewed and the view counts
# link_clicks Links that the subscriber has clicked and the click counts
exportable = ["profile", "subscriptions", "campaign_views", "link_clicks"]

# Allow subscribers to delete themselves from the database?
# This deletes the subscriber and all their subscriptions.
# Their association to campaign views and link clicks are also
# removed while views and click counts remain (with no subscriber
# associated to them) so that stats and analytics aren't affected.
allow_wipe = false


# Database.
[db]
host = "demo-db"
Expand All @@ -53,8 +72,6 @@ password = "listmonk"
database = "listmonk"
ssl_mode = "disable"

# TQekh4quVgGc3HQ


# SMTP servers.
[smtp]
Expand Down
9 changes: 9 additions & 0 deletions email-templates/subscriber-data.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{{ define "subscriber-data" }}
{{ template "header" . }}
<h2>Your data</h2>
<p>
A copy of all data recorded on you is attached as a file in the JSON format.
It can be viewed in a text editor.
</p>
{{ template "footer" }}
{{ end }}
15 changes: 13 additions & 2 deletions frontend/src/Subscriber.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
Select,
Button,
Tag,
Tooltip,
Icon,
Spin,
Popconfirm,
notification
Expand Down Expand Up @@ -350,7 +352,7 @@ class Subscriber extends React.PureComponent {
<section className="content">
<header className="header">
<Row>
<Col span={20}>
<Col span={22}>
{!this.state.record.id && <h1>Add subscriber</h1>}
{this.state.record.id && (
<div>
Expand All @@ -372,7 +374,16 @@ class Subscriber extends React.PureComponent {
</div>
)}
</Col>
<Col span={2} />
<Col span={2} className="right">
<Tooltip title="Export data" placement="top">
<a
role="button"
href={"/api/subscribers/" + this.state.record.id + "/export"}
>
<Icon type="export" />
</a>
</Tooltip>
</Col>
</Row>
</header>
<div>
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ require (
github.com/jmoiron/sqlx v1.2.0
github.com/jordan-wright/email v0.0.0-20181027021455-480bedc4908b
github.com/knadh/goyesql v2.0.0+incompatible
github.com/knadh/koanf v0.4.3
github.com/knadh/koanf v0.4.4
github.com/knadh/stuffbin v1.0.0
github.com/kr/pretty v0.1.0 // indirect
github.com/labstack/echo v3.3.10+incompatible
github.com/labstack/gommon v0.2.7 // indirect
github.com/lib/pq v1.0.0
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,17 @@ github.com/knadh/koanf v0.4.2 h1:A/bb9+eRoHHHQ57O6y66vzRCYui915CK3FdDYzNs56Q=
github.com/knadh/koanf v0.4.2/go.mod h1:Qd5yvXN39ZzjoRJdXMKN2QqHzQKhSx/K8fU5gyn4LPs=
github.com/knadh/koanf v0.4.3 h1:aeCEnL10SVOIxnhhS3FeFtfvzC3RBphdhhrESE9qfCI=
github.com/knadh/koanf v0.4.3/go.mod h1:Qd5yvXN39ZzjoRJdXMKN2QqHzQKhSx/K8fU5gyn4LPs=
github.com/knadh/koanf v0.4.4 h1:Pg+eR7wuJtCGHLeip31K20eJojjZ3lXE8ILQQGj2PTM=
github.com/knadh/koanf v0.4.4/go.mod h1:Qd5yvXN39ZzjoRJdXMKN2QqHzQKhSx/K8fU5gyn4LPs=
github.com/knadh/stuffbin v0.0.0-20190103171338-6379e949be48 h1:lRb28d0+iiVwqF7Li25IJXjNRaVCQPH6n/fHwk9Qo+E=
github.com/knadh/stuffbin v0.0.0-20190103171338-6379e949be48/go.mod h1:afUOPBWr6bZ09aS3wbSOqXVGaO6rKcyvXYTcuG9LYpI=
github.com/knadh/stuffbin v1.0.0 h1:NQon6PTpLXies4bRFhS3VpLCf6y+jn6YVXU3i2wPQ+M=
github.com/knadh/stuffbin v1.0.0/go.mod h1:yVCFaWaKPubSNibBsTAJ939q2ABHudJQxRWZWV5yh+4=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/gommon v0.2.7 h1:2qOPq/twXDrQ6ooBGrn3mrmVOC+biLlatwgIu8lbzRM=
Expand Down Expand Up @@ -75,6 +82,7 @@ google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO50
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/volatiletech/null.v6 v6.0.0-20170828023728-0bef4e07ae1b h1:P+3+n9hUbqSDkSdtusWHVPQRrpRpLiLFzlZ02xXskM0=
gopkg.in/volatiletech/null.v6 v6.0.0-20170828023728-0bef4e07ae1b/go.mod h1:0LRKfykySnChgQpG3Qpk+bkZFWazQ+MMfc5oldQCwnY=
Expand Down
9 changes: 5 additions & 4 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"strings"

"github.com/asaskevich/govalidator"

"github.com/labstack/echo"
)

Expand Down Expand Up @@ -45,6 +44,7 @@ func registerHandlers(e *echo.Echo) {
e.GET("/api/dashboard/stats", handleGetDashboardStats)

e.GET("/api/subscribers/:id", handleGetSubscriber)
e.GET("/api/subscribers/:id/export", handleExportSubscriberData)
e.POST("/api/subscribers", handleCreateSubscriber)
e.PUT("/api/subscribers/:id", handleUpdateSubscriber)
e.PUT("/api/subscribers/blacklist", handleBlacklistSubscribers)
Expand All @@ -59,7 +59,6 @@ func registerHandlers(e *echo.Echo) {
e.POST("/api/subscribers/query/delete", handleDeleteSubscribersByQuery)
e.PUT("/api/subscribers/query/blacklist", handleBlacklistSubscribersByQuery)
e.PUT("/api/subscribers/query/lists", handleManageSubscriberListsByQuery)

e.GET("/api/subscribers", handleQuerySubscribers)

e.GET("/api/import/subscribers", handleGetImportSubscribers)
Expand Down Expand Up @@ -98,8 +97,10 @@ func registerHandlers(e *echo.Echo) {
e.DELETE("/api/templates/:id", handleDeleteTemplate)

// Subscriber facing views.
e.GET("/unsubscribe/:campUUID/:subUUID", handleUnsubscribePage)
e.POST("/unsubscribe/:campUUID/:subUUID", handleUnsubscribePage)
e.GET("/subscription/:campUUID/:subUUID", handleSubscriptionPage)
e.POST("/subscription/:campUUID/:subUUID", handleSubscriptionPage)
e.POST("/subscription/export/:subUUID", handleSelfExportSubscriberData)
e.POST("/subscription/wipe/:subUUID", handleWipeSubscriberData)
e.GET("/link/:linkUUID/:campUUID/:subUUID", handleLinkRedirect)
e.GET("/campaign/:campUUID/:subUUID/px.png", handleRegisterCampaignView)

Expand Down
26 changes: 18 additions & 8 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/jmoiron/sqlx"
"github.com/knadh/goyesql"
"github.com/knadh/koanf"
"github.com/knadh/koanf/maps"
"github.com/knadh/koanf/parsers/toml"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/posflag"
Expand All @@ -25,13 +26,20 @@ import (
)

type constants struct {
RootURL string `koanf:"root"`
LogoURL string `koanf:"logo_url"`
FaviconURL string `koanf:"favicon_url"`
UploadPath string `koanf:"upload_path"`
UploadURI string `koanf:"upload_uri"`
FromEmail string `koanf:"from_email"`
NotifyEmails []string `koanf:"notify_emails"`
RootURL string `koanf:"root"`
LogoURL string `koanf:"logo_url"`
FaviconURL string `koanf:"favicon_url"`
UploadPath string `koanf:"upload_path"`
UploadURI string `koanf:"upload_uri"`
FromEmail string `koanf:"from_email"`
NotifyEmails []string `koanf:"notify_emails"`
Privacy privacyOptions `koanf:"privacy"`
}

type privacyOptions struct {
AllowExport bool `koanf:"allow_export"`
AllowWipe bool `koanf:"allow_wipe"`
Exportable map[string]bool `koanf:"-"`
}

// App contains the "global" components that are
Expand Down Expand Up @@ -183,9 +191,11 @@ func main() {

var c constants
ko.Unmarshal("app", &c)
ko.Unmarshal("privacy", &c.Privacy)
c.RootURL = strings.TrimRight(c.RootURL, "/")
c.UploadURI = filepath.Clean(c.UploadURI)
c.UploadPath = filepath.Clean(c.UploadPath)
c.Privacy.Exportable = maps.StringSliceToLookupMap(ko.Strings("privacy.exportable"))

// Initialize the static file system into which all
// required static assets (.sql, .js files etc.) are loaded.
Expand Down Expand Up @@ -253,7 +263,7 @@ func main() {
FromEmail: app.Constants.FromEmail,

// url.com/unsubscribe/{campaign_uuid}/{subscriber_uuid}
UnsubscribeURL: fmt.Sprintf("%s/unsubscribe/%%s/%%s", app.Constants.RootURL),
UnsubscribeURL: fmt.Sprintf("%s/subscription/%%s/%%s", app.Constants.RootURL),

// url.com/link/{campaign_uuid}/{subscriber_uuid}/{link_uuid}
LinkTrackURL: fmt.Sprintf("%s/link/%%s/%%s/%%s", app.Constants.RootURL),
Expand Down
15 changes: 15 additions & 0 deletions notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,18 @@ func sendNotification(tpl, subject string, data map[string]interface{}, app *App

return nil
}

func getNotificationTemplate(tpl string, data map[string]interface{}, app *App) ([]byte, error) {
if data == nil {
data = make(map[string]interface{})
}
data["RootURL"] = app.Constants.RootURL

var b bytes.Buffer
err := app.NotifTpls.ExecuteTemplate(&b, tpl, data)
if err != nil {
return nil, err
}

return b.Bytes(), err
}
Loading

0 comments on commit 3b79028

Please sign in to comment.