Skip to content

Commit

Permalink
Add support for custom CSS/JS in settings for admin and public pages.
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
knadh and sweetppro committed Dec 18, 2021
1 parent 920645f commit fabe06e
Show file tree
Hide file tree
Showing 28 changed files with 280 additions and 5 deletions.
40 changes: 40 additions & 0 deletions cmd/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
return c.Render(http.StatusOK, "home", publicTpl{Title: "listmonk"})
})
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)

// API endpoints.
Expand Down Expand Up @@ -142,6 +144,7 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
e.POST("/webhooks/service/:service", handleBounceWebhook)
}

// /public/static/* file server is registered in initHTTPServer().
// Public subscriber facing views.
e.GET("/subscription/form", handleSubscriptionFormPage)
e.POST("/subscription/form", handleSubscriptionForm)
Expand All @@ -161,6 +164,10 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
"campUUID", "subUUID")))
e.GET("/campaign/:campUUID/:subUUID/px.png", noIndex(validateUUID(handleRegisterCampaignView,
"campUUID", "subUUID")))

e.GET("/public/custom.css", serveCustomApperance("public.custom_css"))
e.GET("/public/custom.js", serveCustomApperance("public.custom_js"))

// Public health API endpoint.
e.GET("/health", handleHealthCheck)
}
Expand All @@ -182,6 +189,39 @@ func handleHealthCheck(c echo.Context) error {
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.
func basicAuth(username, password string, c echo.Context) (bool, error) {
app := c.Get("app").(*App)
Expand Down
14 changes: 12 additions & 2 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ type constants struct {
AdminUsername []byte `koanf:"admin_username"`
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
LinkTrackURL string
ViewTrackURL string
Expand Down Expand Up @@ -293,7 +300,10 @@ func initConstants() *constants {
lo.Fatalf("error loading app config: %v", err)
}
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, "/")
Expand Down Expand Up @@ -622,7 +632,7 @@ func initHTTPServer(app *App) *echo.Echo {
fSrv := app.fs.FileServer()

// Public (subscriber) facing static files.
srv.GET("/public/*", echo.WrapHandler(fSrv))
srv.GET("/public/static/*", echo.WrapHandler(fSrv))

// Admin (frontend) facing static files.
srv.GET("/admin/static/*", echo.WrapHandler(fSrv))
Expand Down
5 changes: 5 additions & 0 deletions cmd/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ type settings struct {
TLSSkipVerify bool `json:"tls_skip_verify"`
ScanInterval string `json:"scan_interval"`
} `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 (
Expand Down
1 change: 1 addition & 0 deletions cmd/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var migList = []migFunc{
{"v0.9.0", migrations.V0_9_0},
{"v1.0.0", migrations.V1_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
Expand Down
2 changes: 2 additions & 0 deletions frontend/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<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>
</head>
<body>
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/assets/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,10 @@ section.analytics {
.box {
margin-bottom: 30px;
}
.html-editor {
height: auto;
min-height: 350px;
}
}

/* Logs */
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/HTMLEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { colors } from '../constants';
export default {
props: {
value: String,
language: {
type: String,
default: 'html',
},
disabled: Boolean,
},
Expand Down Expand Up @@ -38,7 +42,7 @@ export default {
this.$refs.htmlEditor.appendChild(el);
this.flask = new CodeFlask(el.shadowRoot.getElementById('area'), {
language: 'html',
language: this.$props.language,
lineNumbers: false,
styleParent: el.shadowRoot,
readonly: this.disabled,
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/views/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
<b-tab-item :label="$t('settings.messengers.name')">
<messenger-settings :form="form" :key="key" />
</b-tab-item><!-- messengers -->

<b-tab-item :label="$t('settings.appearance.name')">
<appearance-settings :form="form" :key="key" />
</b-tab-item><!-- appearance -->
</b-tabs>

</section>
Expand All @@ -66,6 +70,7 @@ import MediaSettings from './settings/media.vue';
import SmtpSettings from './settings/smtp.vue';
import BounceSettings from './settings/bounces.vue';
import MessengerSettings from './settings/messengers.vue';
import AppearanceSettings from './settings/appearance.vue';
const dummyPassword = ' '.repeat(8);
Expand All @@ -78,6 +83,7 @@ export default Vue.extend({
SmtpSettings,
BounceSettings,
MessengerSettings,
AppearanceSettings,
},
data() {
Expand Down
66 changes: 66 additions & 0 deletions frontend/src/views/settings/appearance.vue
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 1 deletion frontend/vue.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ module.exports = {
'^/$': {
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'
}
}
Expand Down
7 changes: 7 additions & 0 deletions i18n/cs-cz.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@
"public.unsubbedInfo": "Odběr jste zrušili úspěšně.",
"public.unsubbedTitle": "Zrušen odběr",
"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.blocklist": "Seznam blokovaných",
"settings.bounces.count": "Počet případů nedoručitelnosti",
Expand Down
7 changes: 7 additions & 0 deletions i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@
"public.unsubbedInfo": "Du wurdest erfolgreich abgemeldet",
"public.unsubbedTitle": "Abgemeldet",
"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.blocklist": "Sperrliste",
"settings.bounces.count": "Bounce Anzahl",
Expand Down
7 changes: 7 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@
"public.unsubbedInfo": "You have unsubscribed successfully.",
"public.unsubbedTitle": "Unsubscribed",
"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.blocklist": "Blocklist",
"settings.bounces.count": "Bounce count",
Expand Down
7 changes: 7 additions & 0 deletions i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@
"public.unsubbedInfo": "Ud. se ha des-subscrito de forma satisfactoria",
"public.unsubbedTitle": "Des-subscrito.",
"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.blocklist": "Lista de bloqueo",
"settings.bounces.count": "Conteo de rebotes",
Expand Down
7 changes: 7 additions & 0 deletions i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@
"public.unsubbedInfo": "Vous vous êtes désabonné·e avec succès.",
"public.unsubbedTitle": "Désabonné·e",
"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.blocklist": "Liste de bloquage",
"settings.bounces.count": "Comptage des rebonds",
Expand Down
7 changes: 7 additions & 0 deletions i18n/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@
"public.unsubbedInfo": "Sikeresen leiratkozott.",
"public.unsubbedTitle": "Leiratkozott",
"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.blocklist": "Tiltólista",
"settings.bounces.count": "Visszapattanások száma",
Expand Down
7 changes: 7 additions & 0 deletions i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@
"public.unsubbedInfo": "La cancellazione è avvenuta con successo.",
"public.unsubbedTitle": "Iscrizione annullata",
"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.blocklist": "Blocklist",
"settings.bounces.count": "Bounce count",
Expand Down
7 changes: 7 additions & 0 deletions i18n/ml.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@
"public.unsubbedInfo": "നിങ്ങൾ വരിക്കാരനല്ലാതായി",
"public.unsubbedTitle": "വരിക്കാരനല്ലാതാകുക",
"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.blocklist": "Blocklist",
"settings.bounces.count": "Bounce count",
Expand Down
7 changes: 7 additions & 0 deletions i18n/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@
"public.unsubbedInfo": "Je bent met succes uitgeschreven.",
"public.unsubbedTitle": "Uitgeschreven",
"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.blocklist": "Geblokkeerd",
"settings.bounces.count": "Aantal bounces",
Expand Down
Loading

0 comments on commit fabe06e

Please sign in to comment.