Add support for custom CSS/JS in settings for admin and public pages.
This feature was originally authored by @sweetppro in PR #438. However, since the PR ended up in an unclean state with multiple master merges (instead of rebase) from the upstream, there are several commits that are out of order and can can no longer be be squashed for a clean feature merge. This commit aggregates the changes from the original PR and applies the following fixes on top of it. - Add custom admin JS box to appearance UI. - Refactor i18n language strings. - Add handlers and migrations for the new `appearance.admin.custom_js` field. - Fix migration version to `v2.1.0` - Load custom appearance CSS/JS bytes into global constants during boot instead of making a DB call on every request. - Fix and canonicalize URIs from `/api/custom*` to `/public/*.css` and `/admin/*.css`. Add proxy paths to yarn proxy config. - Remove redundant HTTP handlers for different custom appearance files and refactor into a single handler `serveCustomApperance()` - Fix content-type and UTF8 encoding headers for different file types. - Fix incorrect registration of public facing custom CSS/JS handlers in the authenticated admin URI group. - Fix merge conflicts in `Settings.vue`. - Minor HTML and style fixes. - Remove the `AppearanceEditor` component and use the existing `HTMLEditor` component instead. - Add `language` prop to the `HTMLEditor` component. Co-authored-by: SweetPPro <sweetppro@users.noreply.github.com>
This commit is contained in:
parent
920645f90e
commit
fabe06e339
28 changed files with 280 additions and 5 deletions
|
@ -55,6 +55,8 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
|
||||||
return c.Render(http.StatusOK, "home", publicTpl{Title: "listmonk"})
|
return c.Render(http.StatusOK, "home", publicTpl{Title: "listmonk"})
|
||||||
})
|
})
|
||||||
g.GET(path.Join(adminRoot, ""), handleAdminPage)
|
g.GET(path.Join(adminRoot, ""), handleAdminPage)
|
||||||
|
g.GET(path.Join(adminRoot, "/custom.css"), serveCustomApperance("admin.custom_css"))
|
||||||
|
g.GET(path.Join(adminRoot, "/custom.js"), serveCustomApperance("admin.custom_js"))
|
||||||
g.GET(path.Join(adminRoot, "/*"), handleAdminPage)
|
g.GET(path.Join(adminRoot, "/*"), handleAdminPage)
|
||||||
|
|
||||||
// API endpoints.
|
// API endpoints.
|
||||||
|
@ -142,6 +144,7 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
|
||||||
e.POST("/webhooks/service/:service", handleBounceWebhook)
|
e.POST("/webhooks/service/:service", handleBounceWebhook)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /public/static/* file server is registered in initHTTPServer().
|
||||||
// Public subscriber facing views.
|
// Public subscriber facing views.
|
||||||
e.GET("/subscription/form", handleSubscriptionFormPage)
|
e.GET("/subscription/form", handleSubscriptionFormPage)
|
||||||
e.POST("/subscription/form", handleSubscriptionForm)
|
e.POST("/subscription/form", handleSubscriptionForm)
|
||||||
|
@ -161,6 +164,10 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
|
||||||
"campUUID", "subUUID")))
|
"campUUID", "subUUID")))
|
||||||
e.GET("/campaign/:campUUID/:subUUID/px.png", noIndex(validateUUID(handleRegisterCampaignView,
|
e.GET("/campaign/:campUUID/:subUUID/px.png", noIndex(validateUUID(handleRegisterCampaignView,
|
||||||
"campUUID", "subUUID")))
|
"campUUID", "subUUID")))
|
||||||
|
|
||||||
|
e.GET("/public/custom.css", serveCustomApperance("public.custom_css"))
|
||||||
|
e.GET("/public/custom.js", serveCustomApperance("public.custom_js"))
|
||||||
|
|
||||||
// Public health API endpoint.
|
// Public health API endpoint.
|
||||||
e.GET("/health", handleHealthCheck)
|
e.GET("/health", handleHealthCheck)
|
||||||
}
|
}
|
||||||
|
@ -182,6 +189,39 @@ func handleHealthCheck(c echo.Context) error {
|
||||||
return c.JSON(http.StatusOK, okResp{true})
|
return c.JSON(http.StatusOK, okResp{true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serveCustomApperance serves the given custom CSS/JS apperance blob
|
||||||
|
// meant for customizing public and admin pages from the admin settings UI.
|
||||||
|
func serveCustomApperance(name string) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
var (
|
||||||
|
app = c.Get("app").(*App)
|
||||||
|
|
||||||
|
out []byte
|
||||||
|
hdr string
|
||||||
|
)
|
||||||
|
|
||||||
|
switch name {
|
||||||
|
case "admin.custom_css":
|
||||||
|
out = app.constants.Appearance.AdminCSS
|
||||||
|
hdr = "text/css; charset=utf-8"
|
||||||
|
|
||||||
|
case "admin.custom_js":
|
||||||
|
out = app.constants.Appearance.AdminJS
|
||||||
|
hdr = "application/javascript; charset=utf-8"
|
||||||
|
|
||||||
|
case "public.custom_css":
|
||||||
|
out = app.constants.Appearance.PublicCSS
|
||||||
|
hdr = "text/css; charset=utf-8"
|
||||||
|
|
||||||
|
case "public.custom_js":
|
||||||
|
out = app.constants.Appearance.PublicJS
|
||||||
|
hdr = "application/javascript; charset=utf-8"
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Blob(http.StatusOK, hdr, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// basicAuth middleware does an HTTP BasicAuth authentication for admin handlers.
|
// basicAuth middleware does an HTTP BasicAuth authentication for admin handlers.
|
||||||
func basicAuth(username, password string, c echo.Context) (bool, error) {
|
func basicAuth(username, password string, c echo.Context) (bool, error) {
|
||||||
app := c.Get("app").(*App)
|
app := c.Get("app").(*App)
|
||||||
|
|
14
cmd/init.go
14
cmd/init.go
|
@ -68,6 +68,13 @@ type constants struct {
|
||||||
AdminUsername []byte `koanf:"admin_username"`
|
AdminUsername []byte `koanf:"admin_username"`
|
||||||
AdminPassword []byte `koanf:"admin_password"`
|
AdminPassword []byte `koanf:"admin_password"`
|
||||||
|
|
||||||
|
Appearance struct {
|
||||||
|
AdminCSS []byte `koanf:"admin.custom_css"`
|
||||||
|
AdminJS []byte `koanf:"admin.custom_js"`
|
||||||
|
PublicCSS []byte `koanf:"public.custom_css"`
|
||||||
|
PublicJS []byte `koanf:"public.custom_js"`
|
||||||
|
}
|
||||||
|
|
||||||
UnsubURL string
|
UnsubURL string
|
||||||
LinkTrackURL string
|
LinkTrackURL string
|
||||||
ViewTrackURL string
|
ViewTrackURL string
|
||||||
|
@ -293,7 +300,10 @@ func initConstants() *constants {
|
||||||
lo.Fatalf("error loading app config: %v", err)
|
lo.Fatalf("error loading app config: %v", err)
|
||||||
}
|
}
|
||||||
if err := ko.Unmarshal("privacy", &c.Privacy); err != nil {
|
if err := ko.Unmarshal("privacy", &c.Privacy); err != nil {
|
||||||
lo.Fatalf("error loading app config: %v", err)
|
lo.Fatalf("error loading app.privacy config: %v", err)
|
||||||
|
}
|
||||||
|
if err := ko.UnmarshalWithConf("appearance", &c.Appearance, koanf.UnmarshalConf{FlatPaths: true}); err != nil {
|
||||||
|
lo.Fatalf("error loading app.appearance config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.RootURL = strings.TrimRight(c.RootURL, "/")
|
c.RootURL = strings.TrimRight(c.RootURL, "/")
|
||||||
|
@ -622,7 +632,7 @@ func initHTTPServer(app *App) *echo.Echo {
|
||||||
fSrv := app.fs.FileServer()
|
fSrv := app.fs.FileServer()
|
||||||
|
|
||||||
// Public (subscriber) facing static files.
|
// Public (subscriber) facing static files.
|
||||||
srv.GET("/public/*", echo.WrapHandler(fSrv))
|
srv.GET("/public/static/*", echo.WrapHandler(fSrv))
|
||||||
|
|
||||||
// Admin (frontend) facing static files.
|
// Admin (frontend) facing static files.
|
||||||
srv.GET("/admin/static/*", echo.WrapHandler(fSrv))
|
srv.GET("/admin/static/*", echo.WrapHandler(fSrv))
|
||||||
|
|
|
@ -105,6 +105,11 @@ type settings struct {
|
||||||
TLSSkipVerify bool `json:"tls_skip_verify"`
|
TLSSkipVerify bool `json:"tls_skip_verify"`
|
||||||
ScanInterval string `json:"scan_interval"`
|
ScanInterval string `json:"scan_interval"`
|
||||||
} `json:"bounce.mailboxes"`
|
} `json:"bounce.mailboxes"`
|
||||||
|
|
||||||
|
AdminCustomCSS string `json:"appearance.admin.custom_css"`
|
||||||
|
AdminCustomJS string `json:"appearance.admin.custom_js"`
|
||||||
|
PublicCustomCSS string `json:"appearance.public.custom_css"`
|
||||||
|
PublicCustomJS string `json:"appearance.public.custom_js"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -31,6 +31,7 @@ var migList = []migFunc{
|
||||||
{"v0.9.0", migrations.V0_9_0},
|
{"v0.9.0", migrations.V0_9_0},
|
||||||
{"v1.0.0", migrations.V1_0_0},
|
{"v1.0.0", migrations.V1_0_0},
|
||||||
{"v2.0.0", migrations.V2_0_0},
|
{"v2.0.0", migrations.V2_0_0},
|
||||||
|
{"v2.1.0", migrations.V2_1_0},
|
||||||
}
|
}
|
||||||
|
|
||||||
// upgrade upgrades the database to the current version by running SQL migration files
|
// upgrade upgrades the database to the current version by running SQL migration files
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
<link rel="icon" href="<%= BASE_URL %>static/favicon.png" />
|
<link rel="icon" href="<%= BASE_URL %>static/favicon.png" />
|
||||||
|
<link href="<%= BASE_URL %>custom.css" rel="stylesheet" type="text/css">
|
||||||
|
<script src="<%= BASE_URL %>custom.js" async defer></script>
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -802,6 +802,10 @@ section.analytics {
|
||||||
.box {
|
.box {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
.html-editor {
|
||||||
|
height: auto;
|
||||||
|
min-height: 350px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Logs */
|
/* Logs */
|
||||||
|
|
|
@ -9,6 +9,10 @@ import { colors } from '../constants';
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: String,
|
value: String,
|
||||||
|
language: {
|
||||||
|
type: String,
|
||||||
|
default: 'html',
|
||||||
|
},
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -38,7 +42,7 @@ export default {
|
||||||
this.$refs.htmlEditor.appendChild(el);
|
this.$refs.htmlEditor.appendChild(el);
|
||||||
|
|
||||||
this.flask = new CodeFlask(el.shadowRoot.getElementById('area'), {
|
this.flask = new CodeFlask(el.shadowRoot.getElementById('area'), {
|
||||||
language: 'html',
|
language: this.$props.language,
|
||||||
lineNumbers: false,
|
lineNumbers: false,
|
||||||
styleParent: el.shadowRoot,
|
styleParent: el.shadowRoot,
|
||||||
readonly: this.disabled,
|
readonly: this.disabled,
|
||||||
|
|
|
@ -49,6 +49,10 @@
|
||||||
<b-tab-item :label="$t('settings.messengers.name')">
|
<b-tab-item :label="$t('settings.messengers.name')">
|
||||||
<messenger-settings :form="form" :key="key" />
|
<messenger-settings :form="form" :key="key" />
|
||||||
</b-tab-item><!-- messengers -->
|
</b-tab-item><!-- messengers -->
|
||||||
|
|
||||||
|
<b-tab-item :label="$t('settings.appearance.name')">
|
||||||
|
<appearance-settings :form="form" :key="key" />
|
||||||
|
</b-tab-item><!-- appearance -->
|
||||||
</b-tabs>
|
</b-tabs>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
@ -66,6 +70,7 @@ import MediaSettings from './settings/media.vue';
|
||||||
import SmtpSettings from './settings/smtp.vue';
|
import SmtpSettings from './settings/smtp.vue';
|
||||||
import BounceSettings from './settings/bounces.vue';
|
import BounceSettings from './settings/bounces.vue';
|
||||||
import MessengerSettings from './settings/messengers.vue';
|
import MessengerSettings from './settings/messengers.vue';
|
||||||
|
import AppearanceSettings from './settings/appearance.vue';
|
||||||
|
|
||||||
const dummyPassword = ' '.repeat(8);
|
const dummyPassword = ' '.repeat(8);
|
||||||
|
|
||||||
|
@ -78,6 +83,7 @@ export default Vue.extend({
|
||||||
SmtpSettings,
|
SmtpSettings,
|
||||||
BounceSettings,
|
BounceSettings,
|
||||||
MessengerSettings,
|
MessengerSettings,
|
||||||
|
AppearanceSettings,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|
66
frontend/src/views/settings/appearance.vue
Normal file
66
frontend/src/views/settings/appearance.vue
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<template>
|
||||||
|
<div class="items">
|
||||||
|
<b-tabs :animated="false">
|
||||||
|
<b-tab-item :label="$t('settings.appearance.adminName')" label-position="on-border">
|
||||||
|
<div class="block">
|
||||||
|
{{ $t('settings.appearance.adminHelp') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<b-field :label="$t('settings.appearance.customCSS')" label-position="on-border">
|
||||||
|
<html-editor v-model="data['appearance.admin.custom_css']" name="body"
|
||||||
|
language="css" />
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field :label="$t('settings.appearance.customJS')" label-position="on-border">
|
||||||
|
<html-editor v-model="data['appearance.admin.custom_js']" name="body"
|
||||||
|
language="css" />
|
||||||
|
</b-field>
|
||||||
|
</b-tab-item><!-- admin -->
|
||||||
|
|
||||||
|
<b-tab-item :label="$t('settings.appearance.publicName')" label-position="on-border">
|
||||||
|
<div class="block">
|
||||||
|
{{ $t('settings.appearance.publicHelp') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<b-field :label="$t('settings.appearance.customCSS')" label-position="on-border">
|
||||||
|
<html-editor v-model="data['appearance.public.custom_css']" name="body"
|
||||||
|
language="css" />
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field :label="$t('settings.appearance.customJS')" label-position="on-border">
|
||||||
|
<html-editor v-model="data['appearance.public.custom_js']" name="body"
|
||||||
|
language="js" />
|
||||||
|
</b-field>
|
||||||
|
</b-tab-item><!-- public -->
|
||||||
|
</b-tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { mapState } from 'vuex';
|
||||||
|
import HTMLEditor from '../../components/HTMLEditor.vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
components: {
|
||||||
|
'html-editor': HTMLEditor,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
form: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
data: this.form,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(['settings']),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
5
frontend/vue.config.js
vendored
5
frontend/vue.config.js
vendored
|
@ -29,7 +29,10 @@ module.exports = {
|
||||||
'^/$': {
|
'^/$': {
|
||||||
target: process.env.LISTMONK_API_URL || 'http://127.0.0.1:9000'
|
target: process.env.LISTMONK_API_URL || 'http://127.0.0.1:9000'
|
||||||
},
|
},
|
||||||
'^/(api|webhooks|subscription|public)': {
|
'^/(api|webhooks|subscription|public|health)': {
|
||||||
|
target: process.env.LISTMONK_API_URL || 'http://127.0.0.1:9000'
|
||||||
|
},
|
||||||
|
'^/(admin\/custom\.(css|js))': {
|
||||||
target: process.env.LISTMONK_API_URL || 'http://127.0.0.1:9000'
|
target: process.env.LISTMONK_API_URL || 'http://127.0.0.1:9000'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -295,6 +295,13 @@
|
||||||
"public.unsubbedInfo": "Odběr jste zrušili úspěšně.",
|
"public.unsubbedInfo": "Odběr jste zrušili úspěšně.",
|
||||||
"public.unsubbedTitle": "Zrušen odběr",
|
"public.unsubbedTitle": "Zrušen odběr",
|
||||||
"public.unsubscribeTitle": "Zrušit odběr ze seznamu adresátů",
|
"public.unsubscribeTitle": "Zrušit odběr ze seznamu adresátů",
|
||||||
|
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
|
||||||
|
"settings.appearance.adminName": "Admin",
|
||||||
|
"settings.appearance.customCSS": "Custom CSS",
|
||||||
|
"settings.appearance.customJS": "Custom JavaScript",
|
||||||
|
"settings.appearance.name": "Appearance",
|
||||||
|
"settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.",
|
||||||
|
"settings.appearance.publicName": "Public",
|
||||||
"settings.bounces.action": "Akce",
|
"settings.bounces.action": "Akce",
|
||||||
"settings.bounces.blocklist": "Seznam blokovaných",
|
"settings.bounces.blocklist": "Seznam blokovaných",
|
||||||
"settings.bounces.count": "Počet případů nedoručitelnosti",
|
"settings.bounces.count": "Počet případů nedoručitelnosti",
|
||||||
|
|
|
@ -295,6 +295,13 @@
|
||||||
"public.unsubbedInfo": "Du wurdest erfolgreich abgemeldet",
|
"public.unsubbedInfo": "Du wurdest erfolgreich abgemeldet",
|
||||||
"public.unsubbedTitle": "Abgemeldet",
|
"public.unsubbedTitle": "Abgemeldet",
|
||||||
"public.unsubscribeTitle": "Von E-Mail Liste abmelden.",
|
"public.unsubscribeTitle": "Von E-Mail Liste abmelden.",
|
||||||
|
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
|
||||||
|
"settings.appearance.adminName": "Admin",
|
||||||
|
"settings.appearance.customCSS": "Custom CSS",
|
||||||
|
"settings.appearance.customJS": "Custom JavaScript",
|
||||||
|
"settings.appearance.name": "Appearance",
|
||||||
|
"settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.",
|
||||||
|
"settings.appearance.publicName": "Public",
|
||||||
"settings.bounces.action": "Aktion",
|
"settings.bounces.action": "Aktion",
|
||||||
"settings.bounces.blocklist": "Sperrliste",
|
"settings.bounces.blocklist": "Sperrliste",
|
||||||
"settings.bounces.count": "Bounce Anzahl",
|
"settings.bounces.count": "Bounce Anzahl",
|
||||||
|
|
|
@ -295,6 +295,13 @@
|
||||||
"public.unsubbedInfo": "You have unsubscribed successfully.",
|
"public.unsubbedInfo": "You have unsubscribed successfully.",
|
||||||
"public.unsubbedTitle": "Unsubscribed",
|
"public.unsubbedTitle": "Unsubscribed",
|
||||||
"public.unsubscribeTitle": "Unsubscribe from mailing list",
|
"public.unsubscribeTitle": "Unsubscribe from mailing list",
|
||||||
|
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
|
||||||
|
"settings.appearance.adminName": "Admin",
|
||||||
|
"settings.appearance.customCSS": "Custom CSS",
|
||||||
|
"settings.appearance.customJS": "Custom JavaScript",
|
||||||
|
"settings.appearance.name": "Appearance",
|
||||||
|
"settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.",
|
||||||
|
"settings.appearance.publicName": "Public",
|
||||||
"settings.bounces.action": "Action",
|
"settings.bounces.action": "Action",
|
||||||
"settings.bounces.blocklist": "Blocklist",
|
"settings.bounces.blocklist": "Blocklist",
|
||||||
"settings.bounces.count": "Bounce count",
|
"settings.bounces.count": "Bounce count",
|
||||||
|
|
|
@ -295,6 +295,13 @@
|
||||||
"public.unsubbedInfo": "Ud. se ha des-subscrito de forma satisfactoria",
|
"public.unsubbedInfo": "Ud. se ha des-subscrito de forma satisfactoria",
|
||||||
"public.unsubbedTitle": "Des-subscrito.",
|
"public.unsubbedTitle": "Des-subscrito.",
|
||||||
"public.unsubscribeTitle": "Des-subscribirse de una lista de correo",
|
"public.unsubscribeTitle": "Des-subscribirse de una lista de correo",
|
||||||
|
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
|
||||||
|
"settings.appearance.adminName": "Admin",
|
||||||
|
"settings.appearance.customCSS": "Custom CSS",
|
||||||
|
"settings.appearance.customJS": "Custom JavaScript",
|
||||||
|
"settings.appearance.name": "Appearance",
|
||||||
|
"settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.",
|
||||||
|
"settings.appearance.publicName": "Public",
|
||||||
"settings.bounces.action": "Acción",
|
"settings.bounces.action": "Acción",
|
||||||
"settings.bounces.blocklist": "Lista de bloqueo",
|
"settings.bounces.blocklist": "Lista de bloqueo",
|
||||||
"settings.bounces.count": "Conteo de rebotes",
|
"settings.bounces.count": "Conteo de rebotes",
|
||||||
|
|
|
@ -295,6 +295,13 @@
|
||||||
"public.unsubbedInfo": "Vous vous êtes désabonné·e avec succès.",
|
"public.unsubbedInfo": "Vous vous êtes désabonné·e avec succès.",
|
||||||
"public.unsubbedTitle": "Désabonné·e",
|
"public.unsubbedTitle": "Désabonné·e",
|
||||||
"public.unsubscribeTitle": "Se désabonner de la liste de diffusion",
|
"public.unsubscribeTitle": "Se désabonner de la liste de diffusion",
|
||||||
|
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
|
||||||
|
"settings.appearance.adminName": "Admin",
|
||||||
|
"settings.appearance.customCSS": "Custom CSS",
|
||||||
|
"settings.appearance.customJS": "Custom JavaScript",
|
||||||
|
"settings.appearance.name": "Appearance",
|
||||||
|
"settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.",
|
||||||
|
"settings.appearance.publicName": "Public",
|
||||||
"settings.bounces.action": "Action",
|
"settings.bounces.action": "Action",
|
||||||
"settings.bounces.blocklist": "Liste de bloquage",
|
"settings.bounces.blocklist": "Liste de bloquage",
|
||||||
"settings.bounces.count": "Comptage des rebonds",
|
"settings.bounces.count": "Comptage des rebonds",
|
||||||
|
|
|
@ -295,6 +295,13 @@
|
||||||
"public.unsubbedInfo": "Sikeresen leiratkozott.",
|
"public.unsubbedInfo": "Sikeresen leiratkozott.",
|
||||||
"public.unsubbedTitle": "Leiratkozott",
|
"public.unsubbedTitle": "Leiratkozott",
|
||||||
"public.unsubscribeTitle": "Leiratkozás a levelezőlistáról",
|
"public.unsubscribeTitle": "Leiratkozás a levelezőlistáról",
|
||||||
|
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
|
||||||
|
"settings.appearance.adminName": "Admin",
|
||||||
|
"settings.appearance.customCSS": "Custom CSS",
|
||||||
|
"settings.appearance.customJS": "Custom JavaScript",
|
||||||
|
"settings.appearance.name": "Appearance",
|
||||||
|
"settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.",
|
||||||
|
"settings.appearance.publicName": "Public",
|
||||||
"settings.bounces.action": "Action",
|
"settings.bounces.action": "Action",
|
||||||
"settings.bounces.blocklist": "Tiltólista",
|
"settings.bounces.blocklist": "Tiltólista",
|
||||||
"settings.bounces.count": "Visszapattanások száma",
|
"settings.bounces.count": "Visszapattanások száma",
|
||||||
|
|
|
@ -295,6 +295,13 @@
|
||||||
"public.unsubbedInfo": "La cancellazione è avvenuta con successo.",
|
"public.unsubbedInfo": "La cancellazione è avvenuta con successo.",
|
||||||
"public.unsubbedTitle": "Iscrizione annullata",
|
"public.unsubbedTitle": "Iscrizione annullata",
|
||||||
"public.unsubscribeTitle": "Cancella l'iscrizione dalla newsletter",
|
"public.unsubscribeTitle": "Cancella l'iscrizione dalla newsletter",
|
||||||
|
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
|
||||||
|
"settings.appearance.adminName": "Admin",
|
||||||
|
"settings.appearance.customCSS": "Custom CSS",
|
||||||
|
"settings.appearance.customJS": "Custom JavaScript",
|
||||||
|
"settings.appearance.name": "Appearance",
|
||||||
|
"settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.",
|
||||||
|
"settings.appearance.publicName": "Public",
|
||||||
"settings.bounces.action": "Action",
|
"settings.bounces.action": "Action",
|
||||||
"settings.bounces.blocklist": "Blocklist",
|
"settings.bounces.blocklist": "Blocklist",
|
||||||
"settings.bounces.count": "Bounce count",
|
"settings.bounces.count": "Bounce count",
|
||||||
|
|
|
@ -295,6 +295,13 @@
|
||||||
"public.unsubbedInfo": "നിങ്ങൾ വരിക്കാരനല്ലാതായി",
|
"public.unsubbedInfo": "നിങ്ങൾ വരിക്കാരനല്ലാതായി",
|
||||||
"public.unsubbedTitle": "വരിക്കാരനല്ലാതാകുക",
|
"public.unsubbedTitle": "വരിക്കാരനല്ലാതാകുക",
|
||||||
"public.unsubscribeTitle": "മെയിലിങ് ലിസ്റ്റിന്റെ വരിക്കാരനല്ലാതാകുക",
|
"public.unsubscribeTitle": "മെയിലിങ് ലിസ്റ്റിന്റെ വരിക്കാരനല്ലാതാകുക",
|
||||||
|
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
|
||||||
|
"settings.appearance.adminName": "Admin",
|
||||||
|
"settings.appearance.customCSS": "Custom CSS",
|
||||||
|
"settings.appearance.customJS": "Custom JavaScript",
|
||||||
|
"settings.appearance.name": "Appearance",
|
||||||
|
"settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.",
|
||||||
|
"settings.appearance.publicName": "Public",
|
||||||
"settings.bounces.action": "Action",
|
"settings.bounces.action": "Action",
|
||||||
"settings.bounces.blocklist": "Blocklist",
|
"settings.bounces.blocklist": "Blocklist",
|
||||||
"settings.bounces.count": "Bounce count",
|
"settings.bounces.count": "Bounce count",
|
||||||
|
|
|
@ -295,6 +295,13 @@
|
||||||
"public.unsubbedInfo": "Je bent met succes uitgeschreven.",
|
"public.unsubbedInfo": "Je bent met succes uitgeschreven.",
|
||||||
"public.unsubbedTitle": "Uitgeschreven",
|
"public.unsubbedTitle": "Uitgeschreven",
|
||||||
"public.unsubscribeTitle": "Uitschrijven van mailinglijst",
|
"public.unsubscribeTitle": "Uitschrijven van mailinglijst",
|
||||||
|
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
|
||||||
|
"settings.appearance.adminName": "Admin",
|
||||||
|
"settings.appearance.customCSS": "Custom CSS",
|
||||||
|
"settings.appearance.customJS": "Custom JavaScript",
|
||||||
|
"settings.appearance.name": "Appearance",
|
||||||
|
"settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.",
|
||||||
|
"settings.appearance.publicName": "Public",
|
||||||
"settings.bounces.action": "Actie",
|
"settings.bounces.action": "Actie",
|
||||||
"settings.bounces.blocklist": "Geblokkeerd",
|
"settings.bounces.blocklist": "Geblokkeerd",
|
||||||
"settings.bounces.count": "Aantal bounces",
|
"settings.bounces.count": "Aantal bounces",
|
||||||
|
|
|
@ -295,6 +295,13 @@
|
||||||
"public.unsubbedInfo": "Pomyślnie odsubskrybowano",
|
"public.unsubbedInfo": "Pomyślnie odsubskrybowano",
|
||||||
"public.unsubbedTitle": "Odsubskrybowano",
|
"public.unsubbedTitle": "Odsubskrybowano",
|
||||||
"public.unsubscribeTitle": "Wypisz się z listy mailingowej",
|
"public.unsubscribeTitle": "Wypisz się z listy mailingowej",
|
||||||
|
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
|
||||||
|
"settings.appearance.adminName": "Admin",
|
||||||
|
"settings.appearance.customCSS": "Custom CSS",
|
||||||
|
"settings.appearance.customJS": "Custom JavaScript",
|
||||||
|
"settings.appearance.name": "Appearance",
|
||||||
|
"settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.",
|
||||||
|
"settings.appearance.publicName": "Public",
|
||||||
"settings.bounces.action": "Akcja",
|
"settings.bounces.action": "Akcja",
|
||||||
"settings.bounces.blocklist": "Lista zablokowanych",
|
"settings.bounces.blocklist": "Lista zablokowanych",
|
||||||
"settings.bounces.count": "Liczba odbić",
|
"settings.bounces.count": "Liczba odbić",
|
||||||
|
|
|
@ -295,6 +295,13 @@
|
||||||
"public.unsubbedInfo": "Você cancelou a inscrição com sucesso.",
|
"public.unsubbedInfo": "Você cancelou a inscrição com sucesso.",
|
||||||
"public.unsubbedTitle": "Inscrição cancelada",
|
"public.unsubbedTitle": "Inscrição cancelada",
|
||||||
"public.unsubscribeTitle": "Cancelar inscrição na lista de e-mails",
|
"public.unsubscribeTitle": "Cancelar inscrição na lista de e-mails",
|
||||||
|
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
|
||||||
|
"settings.appearance.adminName": "Admin",
|
||||||
|
"settings.appearance.customCSS": "Custom CSS",
|
||||||
|
"settings.appearance.customJS": "Custom JavaScript",
|
||||||
|
"settings.appearance.name": "Appearance",
|
||||||
|
"settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.",
|
||||||
|
"settings.appearance.publicName": "Public",
|
||||||
"settings.bounces.action": "Action",
|
"settings.bounces.action": "Action",
|
||||||
"settings.bounces.blocklist": "Blocklist",
|
"settings.bounces.blocklist": "Blocklist",
|
||||||
"settings.bounces.count": "Bounce count",
|
"settings.bounces.count": "Bounce count",
|
||||||
|
|
|
@ -295,6 +295,13 @@
|
||||||
"public.unsubbedInfo": "A sua subscrição foi cancelada com sucesso.",
|
"public.unsubbedInfo": "A sua subscrição foi cancelada com sucesso.",
|
||||||
"public.unsubbedTitle": "Subscrição cancelada",
|
"public.unsubbedTitle": "Subscrição cancelada",
|
||||||
"public.unsubscribeTitle": "Cancelar subscrição da lista de emails",
|
"public.unsubscribeTitle": "Cancelar subscrição da lista de emails",
|
||||||
|
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
|
||||||
|
"settings.appearance.adminName": "Admin",
|
||||||
|
"settings.appearance.customCSS": "Custom CSS",
|
||||||
|
"settings.appearance.customJS": "Custom JavaScript",
|
||||||
|
"settings.appearance.name": "Appearance",
|
||||||
|
"settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.",
|
||||||
|
"settings.appearance.publicName": "Public",
|
||||||
"settings.bounces.action": "Action",
|
"settings.bounces.action": "Action",
|
||||||
"settings.bounces.blocklist": "Blocklist",
|
"settings.bounces.blocklist": "Blocklist",
|
||||||
"settings.bounces.count": "Bounce count",
|
"settings.bounces.count": "Bounce count",
|
||||||
|
|
|
@ -295,6 +295,13 @@
|
||||||
"public.unsubbedInfo": "Te-ai dezabonat cu succes.",
|
"public.unsubbedInfo": "Te-ai dezabonat cu succes.",
|
||||||
"public.unsubbedTitle": "Dezabonat",
|
"public.unsubbedTitle": "Dezabonat",
|
||||||
"public.unsubscribeTitle": "Dezabonează-te de la lista de discuții",
|
"public.unsubscribeTitle": "Dezabonează-te de la lista de discuții",
|
||||||
|
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
|
||||||
|
"settings.appearance.adminName": "Admin",
|
||||||
|
"settings.appearance.customCSS": "Custom CSS",
|
||||||
|
"settings.appearance.customJS": "Custom JavaScript",
|
||||||
|
"settings.appearance.name": "Appearance",
|
||||||
|
"settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.",
|
||||||
|
"settings.appearance.publicName": "Public",
|
||||||
"settings.bounces.action": "Acțiune",
|
"settings.bounces.action": "Acțiune",
|
||||||
"settings.bounces.blocklist": "Lista de blocare",
|
"settings.bounces.blocklist": "Lista de blocare",
|
||||||
"settings.bounces.count": "Numarul de respingeri",
|
"settings.bounces.count": "Numarul de respingeri",
|
||||||
|
|
|
@ -295,6 +295,13 @@
|
||||||
"public.unsubbedInfo": "Вы были отписаны.",
|
"public.unsubbedInfo": "Вы были отписаны.",
|
||||||
"public.unsubbedTitle": "Отписано",
|
"public.unsubbedTitle": "Отписано",
|
||||||
"public.unsubscribeTitle": "Отписаться от списков рассылки",
|
"public.unsubscribeTitle": "Отписаться от списков рассылки",
|
||||||
|
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
|
||||||
|
"settings.appearance.adminName": "Admin",
|
||||||
|
"settings.appearance.customCSS": "Custom CSS",
|
||||||
|
"settings.appearance.customJS": "Custom JavaScript",
|
||||||
|
"settings.appearance.name": "Appearance",
|
||||||
|
"settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.",
|
||||||
|
"settings.appearance.publicName": "Public",
|
||||||
"settings.bounces.action": "Action",
|
"settings.bounces.action": "Action",
|
||||||
"settings.bounces.blocklist": "Blocklist",
|
"settings.bounces.blocklist": "Blocklist",
|
||||||
"settings.bounces.count": "Bounce count",
|
"settings.bounces.count": "Bounce count",
|
||||||
|
|
|
@ -295,6 +295,13 @@
|
||||||
"public.unsubbedInfo": "Başarı ile üyeliğinizi bitirdiniz.",
|
"public.unsubbedInfo": "Başarı ile üyeliğinizi bitirdiniz.",
|
||||||
"public.unsubbedTitle": "Üyelik bitirildi.",
|
"public.unsubbedTitle": "Üyelik bitirildi.",
|
||||||
"public.unsubscribeTitle": "e-posta listesi üyeliğini bitir",
|
"public.unsubscribeTitle": "e-posta listesi üyeliğini bitir",
|
||||||
|
"settings.appearance.adminHelp": "Custom CSS to apply to the admin UI.",
|
||||||
|
"settings.appearance.adminName": "Admin",
|
||||||
|
"settings.appearance.customCSS": "Custom CSS",
|
||||||
|
"settings.appearance.customJS": "Custom JavaScript",
|
||||||
|
"settings.appearance.name": "Appearance",
|
||||||
|
"settings.appearance.publicHelp": "Custom CSS and JavaScript to apply to the public pages.",
|
||||||
|
"settings.appearance.publicName": "Public",
|
||||||
"settings.bounces.action": "Action",
|
"settings.bounces.action": "Action",
|
||||||
"settings.bounces.blocklist": "Blocklist",
|
"settings.bounces.blocklist": "Blocklist",
|
||||||
"settings.bounces.count": "Bounce count",
|
"settings.bounces.count": "Bounce count",
|
||||||
|
|
23
internal/migrations/v2.1.0.go
Normal file
23
internal/migrations/v2.1.0.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/knadh/koanf"
|
||||||
|
"github.com/knadh/stuffbin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// V2_1_0 performs the DB migrations for v.2.1.0.
|
||||||
|
func V2_1_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
|
||||||
|
if _, err := db.Exec(`
|
||||||
|
INSERT INTO settings (key, value) VALUES
|
||||||
|
('appearance.admin.custom_css', '""'),
|
||||||
|
('appearance.admin.custom_js', '""'),
|
||||||
|
('appearance.public.custom_css', '""'),
|
||||||
|
('appearance.public.custom_js', '""')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -218,7 +218,11 @@ INSERT INTO settings (key, value) VALUES
|
||||||
('bounce.sendgrid_enabled', 'false'),
|
('bounce.sendgrid_enabled', 'false'),
|
||||||
('bounce.sendgrid_key', '""'),
|
('bounce.sendgrid_key', '""'),
|
||||||
('bounce.mailboxes',
|
('bounce.mailboxes',
|
||||||
'[{"enabled":false, "type": "pop", "host":"pop.yoursite.com","port":995,"auth_protocol":"userpass","username":"username","password":"password","return_path": "bounce@listmonk.yoursite.com","scan_interval":"15m","tls_enabled":true,"tls_skip_verify":false}]');
|
'[{"enabled":false, "type": "pop", "host":"pop.yoursite.com","port":995,"auth_protocol":"userpass","username":"username","password":"password","return_path": "bounce@listmonk.yoursite.com","scan_interval":"15m","tls_enabled":true,"tls_skip_verify":false}]'),
|
||||||
|
('appearance.admin.custom_css', '""'),
|
||||||
|
('appearance.admin.custom_js', '""'),
|
||||||
|
('appearance.public.custom_css', '""'),
|
||||||
|
('appearance.public.custom_js', '""');
|
||||||
|
|
||||||
-- bounces
|
-- bounces
|
||||||
DROP TABLE IF EXISTS bounces CASCADE;
|
DROP TABLE IF EXISTS bounces CASCADE;
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
<meta name="description" content="{{ .Data.Description }}" />
|
<meta name="description" content="{{ .Data.Description }}" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
|
||||||
<link href="/public/static/style.css" rel="stylesheet" type="text/css" />
|
<link href="/public/static/style.css" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="/public/custom.css" rel="stylesheet" type="text/css">
|
||||||
|
<script src="/public/custom.js" async defer></script>
|
||||||
|
|
||||||
{{ if ne .FaviconURL "" }}
|
{{ if ne .FaviconURL "" }}
|
||||||
<link rel="shortcut icon" href="{{ .FaviconURL }}" type="image/x-icon" />
|
<link rel="shortcut icon" href="{{ .FaviconURL }}" type="image/x-icon" />
|
||||||
|
|
Loading…
Reference in a new issue