Skip to content

Commit 2235d30

Browse files
committed
Add a new public page for end users to subscribe to public lists.
In addition to generating HTML forms for selected public lists, the form page now shows a URL (/subscription/form) that can be publicly shared to solicit subscriptions. The page lists all public lists in the database. This page can be disabled on the Settings UI.
1 parent a7b72a6 commit 2235d30

File tree

16 files changed

+175
-44
lines changed

16 files changed

+175
-44
lines changed

cmd/admin.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ import (
1414
)
1515

1616
type configScript struct {
17-
RootURL string `json:"rootURL"`
18-
FromEmail string `json:"fromEmail"`
19-
Messengers []string `json:"messengers"`
20-
MediaProvider string `json:"mediaProvider"`
21-
NeedsRestart bool `json:"needsRestart"`
22-
Update *AppUpdate `json:"update"`
23-
Langs []i18nLang `json:"langs"`
24-
Lang json.RawMessage `json:"lang"`
17+
RootURL string `json:"rootURL"`
18+
FromEmail string `json:"fromEmail"`
19+
Messengers []string `json:"messengers"`
20+
MediaProvider string `json:"mediaProvider"`
21+
NeedsRestart bool `json:"needsRestart"`
22+
Update *AppUpdate `json:"update"`
23+
Langs []i18nLang `json:"langs"`
24+
EnablePublicSubPage bool `json:"enablePublicSubscriptionPage"`
25+
Lang json.RawMessage `json:"lang"`
2526
}
2627

2728
// handleGetConfigScript returns general configuration as a Javascript
@@ -30,9 +31,10 @@ func handleGetConfigScript(c echo.Context) error {
3031
var (
3132
app = c.Get("app").(*App)
3233
out = configScript{
33-
RootURL: app.constants.RootURL,
34-
FromEmail: app.constants.FromEmail,
35-
MediaProvider: app.constants.MediaProvider,
34+
RootURL: app.constants.RootURL,
35+
FromEmail: app.constants.FromEmail,
36+
MediaProvider: app.constants.MediaProvider,
37+
EnablePublicSubPage: app.constants.EnablePublicSubPage,
3638
}
3739
)
3840

cmd/handlers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ func registerHTTPHandlers(e *echo.Echo) {
126126
g.GET("/settings/logs", handleIndexPage)
127127

128128
// Public subscriber facing views.
129+
e.GET("/subscription/form", handleSubscriptionFormPage)
129130
e.POST("/subscription/form", handleSubscriptionForm)
130131
e.GET("/subscription/:campUUID/:subUUID", validateUUID(subscriberExists(handleSubscriptionPage),
131132
"campUUID", "subUUID"))

cmd/init.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,15 @@ const (
4040

4141
// constants contains static, constant config values required by the app.
4242
type constants struct {
43-
RootURL string `koanf:"root_url"`
44-
LogoURL string `koanf:"logo_url"`
45-
FaviconURL string `koanf:"favicon_url"`
46-
FromEmail string `koanf:"from_email"`
47-
NotifyEmails []string `koanf:"notify_emails"`
48-
Lang string `koanf:"lang"`
49-
DBBatchSize int `koanf:"batch_size"`
50-
Privacy struct {
43+
RootURL string `koanf:"root_url"`
44+
LogoURL string `koanf:"logo_url"`
45+
FaviconURL string `koanf:"favicon_url"`
46+
FromEmail string `koanf:"from_email"`
47+
NotifyEmails []string `koanf:"notify_emails"`
48+
EnablePublicSubPage bool `koanf:"enable_public_subscription_page"`
49+
Lang string `koanf:"lang"`
50+
DBBatchSize int `koanf:"batch_size"`
51+
Privacy struct {
5152
IndividualTracking bool `koanf:"individual_tracking"`
5253
AllowBlocklist bool `koanf:"allow_blocklist"`
5354
AllowExport bool `koanf:"allow_export"`

cmd/lists.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func handleGetLists(c echo.Context) error {
5050
order = sortAsc
5151
}
5252

53-
if err := db.Select(&out.Results, fmt.Sprintf(app.queries.GetLists, orderBy, order), listID, pg.Offset, pg.Limit); err != nil {
53+
if err := db.Select(&out.Results, fmt.Sprintf(app.queries.QueryLists, orderBy, order), listID, pg.Offset, pg.Limit); err != nil {
5454
app.log.Printf("error fetching lists: %v", err)
5555
return echo.NewHTTPError(http.StatusInternalServerError,
5656
app.i18n.Ts("globals.messages.errorFetching",

cmd/public.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ type msgTpl struct {
6868
Message string
6969
}
7070

71+
type subFormTpl struct {
72+
publicTpl
73+
Lists []models.List
74+
}
75+
7176
type subForm struct {
7277
subimporter.SubReq
7378
SubListUUIDs []string `form:"l"`
@@ -251,6 +256,40 @@ func handleOptinPage(c echo.Context) error {
251256
return c.Render(http.StatusOK, "optin", out)
252257
}
253258

259+
// handleSubscriptionFormPage handles subscription requests coming from public
260+
// HTML subscription forms.
261+
func handleSubscriptionFormPage(c echo.Context) error {
262+
var (
263+
app = c.Get("app").(*App)
264+
)
265+
266+
if !app.constants.EnablePublicSubPage {
267+
return c.Render(http.StatusNotFound, tplMessage,
268+
makeMsgTpl(app.i18n.T("public.errorTitle"), "",
269+
app.i18n.Ts("public.invalidFeature")))
270+
}
271+
272+
// Get all public lists.
273+
var lists []models.List
274+
if err := app.queries.GetLists.Select(&lists, models.ListTypePublic); err != nil {
275+
app.log.Printf("error fetching public lists for form: %s", pqErrMsg(err))
276+
return c.Render(http.StatusInternalServerError, tplMessage,
277+
makeMsgTpl(app.i18n.T("public.errorTitle"), "",
278+
app.i18n.Ts("public.errorFetchingLists")))
279+
}
280+
281+
if len(lists) == 0 {
282+
return c.Render(http.StatusInternalServerError, tplMessage,
283+
makeMsgTpl(app.i18n.T("public.errorTitle"), "",
284+
app.i18n.Ts("public.noListsAvailable")))
285+
}
286+
287+
out := subFormTpl{}
288+
out.Title = app.i18n.T("public.sub")
289+
out.Lists = lists
290+
return c.Render(http.StatusOK, "subscription-form", out)
291+
}
292+
254293
// handleSubscriptionForm handles subscription requests coming from public
255294
// HTML subscription forms.
256295
func handleSubscriptionForm(c echo.Context) error {
@@ -267,7 +306,7 @@ func handleSubscriptionForm(c echo.Context) error {
267306
if len(req.SubListUUIDs) == 0 {
268307
return c.Render(http.StatusBadRequest, tplMessage,
269308
makeMsgTpl(app.i18n.T("public.errorTitle"), "",
270-
app.i18n.T("globals.messages.invalidUUID")))
309+
app.i18n.T("public.noListsSelected")))
271310
}
272311

273312
// If there's no name, use the name bit from the e-mail.
@@ -291,7 +330,7 @@ func handleSubscriptionForm(c echo.Context) error {
291330
}
292331

293332
return c.Render(http.StatusOK, tplMessage,
294-
makeMsgTpl(app.i18n.T("public.subConfirmedTitle"), "",
333+
makeMsgTpl(app.i18n.T("public.subTitle"), "",
295334
app.i18n.Ts("public.subConfirmed")))
296335
}
297336

cmd/queries.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ type Queries struct {
4444
UnsubscribeSubscribersFromListsByQuery string `query:"unsubscribe-subscribers-from-lists-by-query"`
4545

4646
CreateList *sqlx.Stmt `query:"create-list"`
47-
GetLists string `query:"get-lists"`
47+
QueryLists string `query:"query-lists"`
48+
GetLists *sqlx.Stmt `query:"get-lists"`
4849
GetListsByOptin *sqlx.Stmt `query:"get-lists-by-optin"`
4950
UpdateList *sqlx.Stmt `query:"update-list"`
5051
UpdateListsDate *sqlx.Stmt `query:"update-lists-date"`

cmd/settings.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ import (
1414
)
1515

1616
type settings struct {
17-
AppRootURL string `json:"app.root_url"`
18-
AppLogoURL string `json:"app.logo_url"`
19-
AppFaviconURL string `json:"app.favicon_url"`
20-
AppFromEmail string `json:"app.from_email"`
21-
AppNotifyEmails []string `json:"app.notify_emails"`
22-
AppLang string `json:"app.lang"`
17+
AppRootURL string `json:"app.root_url"`
18+
AppLogoURL string `json:"app.logo_url"`
19+
AppFaviconURL string `json:"app.favicon_url"`
20+
AppFromEmail string `json:"app.from_email"`
21+
AppNotifyEmails []string `json:"app.notify_emails"`
22+
EnablePublicSubPage bool `json:"app.enable_public_subscription_page"`
23+
AppLang string `json:"app.lang"`
2324

2425
AppBatchSize int `json:"app.batch_size"`
2526
AppConcurrency int `json:"app.concurrency"`

frontend/src/views/Forms.vue

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@
1515
:native-value="l.uuid">{{ l.name }}</b-checkbox>
1616
</li>
1717
</ul>
18+
19+
20+
<template v-if="serverConfig.enablePublicSubscriptionPage">
21+
<hr />
22+
<h4>{{ $t('forms.publicSubPage') }}</h4>
23+
<p>
24+
<a :href="`${serverConfig.rootURL}/subscription/form`"
25+
target="_blank">{{ serverConfig.rootURL }}/subscription/form</a>
26+
</p>
27+
</template>
1828
</div>
1929
<div class="column">
2030
<h4>{{ $t('forms.formHTML') }}</h4>
@@ -23,23 +33,23 @@
2333
</p>
2434

2535
<!-- eslint-disable max-len -->
26-
<pre>&lt;form method=&quot;post&quot; action=&quot;{{ serverConfig.rootURL }}/subscription/form&quot; class=&quot;listmonk-form&quot;&gt;
36+
<pre v-if="checked.length > 0">&lt;form method=&quot;post&quot; action=&quot;{{ serverConfig.rootURL }}/subscription/form&quot; class=&quot;listmonk-form&quot;&gt;
2737
&lt;div&gt;
2838
&lt;h3&gt;Subscribe&lt;/h3&gt;
29-
&lt;p&gt;&lt;input type=&quot;text&quot; name=&quot;email&quot; placeholder=&quot;E-mail&quot; /&gt;&lt;/p&gt;
30-
&lt;p&gt;&lt;input type=&quot;text&quot; name=&quot;name&quot; placeholder=&quot;Name (optional)&quot; /&gt;&lt;/p&gt;
39+
&lt;p&gt;&lt;input type=&quot;text&quot; name=&quot;email&quot; placeholder=&quot;{{ $t('subscribers.email') }}&quot; /&gt;&lt;/p&gt;
40+
&lt;p&gt;&lt;input type=&quot;text&quot; name=&quot;name&quot; placeholder=&quot;{{ $t('public.subName') }}&quot; /&gt;&lt;/p&gt;
3141
<template v-for="l in publicLists"><span v-if="l.uuid in selected" :key="l.id" :set="id = l.uuid.substr(0, 5)">
3242
&lt;p&gt;
33-
&lt;input id=&quot;{{ id }}&quot; type=&quot;checkbox&quot; name=&quot;l&quot; value=&quot;{{ l.uuid }}&quot; /&gt;
43+
&lt;input id=&quot;{{ id }}&quot; type=&quot;checkbox&quot; name=&quot;l&quot; checked value=&quot;{{ l.uuid }}&quot; /&gt;
3444
&lt;label for=&quot;{{ id }}&quot;&gt;{{ l.name }}&lt;/label&gt;
3545
&lt;/p&gt;</span></template>
36-
&lt;p&gt;&lt;input type=&quot;submit&quot; value=&quot;Subscribe&quot; /&gt;&lt;/p&gt;
46+
47+
&lt;p&gt;&lt;input type=&quot;submit&quot; value=&quot;{{ $t('public.sub') }}&quot; /&gt;&lt;/p&gt;
3748
&lt;/div&gt;
3849
&lt;/form&gt;</pre>
3950
</div>
4051
</div><!-- columns -->
4152

42-
<p v-else></p>
4353
</section>
4454
</template>
4555

frontend/src/views/Settings.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@
5151
placeholder='you@yoursite.com' />
5252
</b-field>
5353

54+
<b-field :label="$t('settings.general.enablePublicSubPage')"
55+
:message="$t('settings.general.enablePublicSubPageHelp')">
56+
<b-switch v-model="form['app.enable_public_subscription_page']"
57+
name="app.enable_public_subscription_page" />
58+
</b-field>
59+
5460
<hr />
5561
<b-field :label="$t('settings.general.language')" label-position="on-border">
5662
<b-select v-model="form['app.lang']" name="app.lang">
@@ -149,7 +155,7 @@
149155
</b-field>
150156

151157
<b-field :label="$t('settings.privacy.allowBlocklist')"
152-
:message="$t('settings.privacy.allowBlocklist')">
158+
:message="$t('settings.privacy.allowBlocklistHelp')">
153159
<b-switch v-model="form['privacy.allow_blocklist']"
154160
name="privacy.allow_blocklist" />
155161
</b-field>

i18n/en.json

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"forms.formHTML": "Form HTML",
9494
"forms.formHTMLHelp": "Use the following HTML to show a subscription form on an external webpage. The form should have the email field and one or more `l` (list UUID) fields. The name field is optional.",
9595
"forms.publicLists": "Public lists",
96+
"forms.publicSubPage": "Public subscription page",
9697
"forms.selectHelp": "Select lists to add to the form.",
9798
"forms.title": "Forms",
9899
"globals.buttons.add": "Add",
@@ -237,14 +238,16 @@
237238
"public.dataRemovedTitle": "Data removed",
238239
"public.dataSent": "Your data has been e-mailed to you as an attachment.",
239240
"public.dataSentTitle": "Data e-mailed",
240-
"public.errorFetchingCampaign": "Error fetching e-mail message",
241+
"public.errorFetchingCampaign": "Error fetching e-mail message.",
241242
"public.errorFetchingEmail": "E-mail message not found",
242243
"public.errorFetchingLists": "Error fetching lists. Please retry.",
243244
"public.errorProcessingRequest": "Error processing request. Please retry.",
244245
"public.errorTitle": "Error",
245-
"public.invalidFeature": "That feature is not available",
246+
"public.invalidFeature": "That feature is not available.",
246247
"public.invalidLink": "Invalid link",
247-
"public.noSubInfo": "There are no subscriptions to confirm",
248+
"public.noListsAvailable": "No lists available to subscribe.",
249+
"public.noListsSelected": "No valid lists selected to subscribe.",
250+
"public.noSubInfo": "There are no subscriptions to confirm.",
248251
"public.noSubTitle": "No subscriptions",
249252
"public.notFoundTitle": "Not found",
250253
"public.privacyConfirmWipe": "Are you sure you want to delete all your subscription data permanently?",
@@ -253,7 +256,10 @@
253256
"public.privacyTitle": "Privacy and data",
254257
"public.privacyWipe": "Wipe your data",
255258
"public.privacyWipeHelp": "Delete all your subscriptions and related data from the database permanently.",
256-
"public.subConfirmed": "Subscribed successfully",
259+
"public.sub": "Subscribe",
260+
"public.subTitle": "Subscribe",
261+
"public.subName": "Name (optional)",
262+
"public.subConfirmed": "Subscribed successfully.",
257263
"public.subConfirmedTitle": "Confirmed",
258264
"public.subNotFound": "Subscription not found.",
259265
"public.subPrivateList": "Private list",
@@ -267,6 +273,8 @@
267273
"settings.duplicateMessengerName": "Duplicate messenger name: {name}",
268274
"settings.errorEncoding": "Error encoding settings: {error}",
269275
"settings.errorNoSMTP": "At least one SMTP block should be enabled",
276+
"settings.general.enablePublicSubPage": "Enable public subscription page",
277+
"settings.general.enablePublicSubPageHelp": "Show a public subscription page with all the public lists for people to subscribe.",
270278
"settings.general.adminNotifEmails": "Admin notification e-mails",
271279
"settings.general.adminNotifEmailsHelp": "Comma separated list of e-mail addresses to which admin notifications such as import updates, campaign completion, failure etc. should be sent.",
272280
"settings.general.faviconURL": "Favicon URL",

0 commit comments

Comments
 (0)