userpage: implement login message card

Shares code with custom emails, so most related functions have had a
%s/Email/Message/g. Press the edit button on the user page setting to
add a message.
user-page
Harvey Tindall 11 months ago
parent 8e153cd92f
commit 7b9b0d8a84
No known key found for this signature in database
GPG Key ID: BBC65952848FB1A2

@ -15,12 +15,16 @@ import (
// @Router /config/emails [get]
// @Security Bearer
// @tags Configuration
func (app *appContext) GetCustomEmails(gc *gin.Context) {
func (app *appContext) GetCustomContent(gc *gin.Context) {
lang := gc.Query("lang")
if _, ok := app.storage.lang.Email[lang]; !ok {
lang = app.storage.lang.chosenEmailLang
}
gc.JSON(200, emailListDTO{
adminLang := lang
if _, ok := app.storage.lang.Admin[lang]; !ok {
adminLang = app.storage.lang.chosenAdminLang
}
list := emailListDTO{
"UserCreated": {Name: app.storage.lang.Email[lang].UserCreated["name"], Enabled: app.storage.customEmails.UserCreated.Enabled},
"InviteExpiry": {Name: app.storage.lang.Email[lang].InviteExpiry["name"], Enabled: app.storage.customEmails.InviteExpiry.Enabled},
"PasswordReset": {Name: app.storage.lang.Email[lang].PasswordReset["name"], Enabled: app.storage.customEmails.PasswordReset.Enabled},
@ -31,13 +35,25 @@ func (app *appContext) GetCustomEmails(gc *gin.Context) {
"WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.customEmails.WelcomeEmail.Enabled},
"EmailConfirmation": {Name: app.storage.lang.Email[lang].EmailConfirmation["name"], Enabled: app.storage.customEmails.EmailConfirmation.Enabled},
"UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.customEmails.UserExpired.Enabled},
})
"UserLogin": {Name: app.storage.lang.Admin[adminLang].Strings["userPageLogin"], Enabled: app.storage.userPage.Login.Enabled},
"UserPage": {Name: app.storage.lang.Admin[adminLang].Strings["userPagePage"], Enabled: app.storage.userPage.Page.Enabled},
}
filter := gc.Query("filter")
if filter == "user" {
list = emailListDTO{"UserLogin": list["UserLogin"], "UserPage": list["UserPage"]}
} else {
delete(list, "UserLogin")
delete(list, "UserPage")
}
gc.JSON(200, list)
}
func (app *appContext) getCustomEmail(id string) *customEmail {
func (app *appContext) getCustomMessage(id string) *customContent {
switch id {
case "Announcement":
return &customEmail{}
return &customContent{}
case "UserCreated":
return &app.storage.customEmails.UserCreated
case "InviteExpiry":
@ -58,13 +74,17 @@ func (app *appContext) getCustomEmail(id string) *customEmail {
return &app.storage.customEmails.EmailConfirmation
case "UserExpired":
return &app.storage.customEmails.UserExpired
case "UserLogin":
return &app.storage.userPage.Login
case "UserPage":
return &app.storage.userPage.Page
}
return nil
}
// @Summary Sets the corresponding custom email.
// @Produce json
// @Param customEmail body customEmail true "Content = email (in markdown)."
// @Param customEmails body customEmails true "Content = email (in markdown)."
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
@ -72,25 +92,29 @@ func (app *appContext) getCustomEmail(id string) *customEmail {
// @Router /config/emails/{id} [post]
// @Security Bearer
// @tags Configuration
func (app *appContext) SetCustomEmail(gc *gin.Context) {
var req customEmail
func (app *appContext) SetCustomMessage(gc *gin.Context) {
var req customContent
gc.BindJSON(&req)
id := gc.Param("id")
if req.Content == "" {
respondBool(400, false, gc)
return
}
email := app.getCustomEmail(id)
if email == nil {
message := app.getCustomMessage(id)
if message == nil {
respondBool(400, false, gc)
return
}
email.Content = req.Content
email.Enabled = true
message.Content = req.Content
message.Enabled = true
if app.storage.storeCustomEmails() != nil {
respondBool(500, false, gc)
return
}
if app.storage.storeUserPageContent() != nil {
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
@ -104,7 +128,7 @@ func (app *appContext) SetCustomEmail(gc *gin.Context) {
// @Router /config/emails/{id}/state/{enable/disable} [post]
// @Security Bearer
// @tags Configuration
func (app *appContext) SetCustomEmailState(gc *gin.Context) {
func (app *appContext) SetCustomMessageState(gc *gin.Context) {
id := gc.Param("id")
s := gc.Param("state")
enabled := false
@ -113,20 +137,24 @@ func (app *appContext) SetCustomEmailState(gc *gin.Context) {
} else if s != "disable" {
respondBool(400, false, gc)
}
email := app.getCustomEmail(id)
if email == nil {
message := app.getCustomMessage(id)
if message == nil {
respondBool(400, false, gc)
return
}
email.Enabled = enabled
message.Enabled = enabled
if app.storage.storeCustomEmails() != nil {
respondBool(500, false, gc)
return
}
if app.storage.storeUserPageContent() != nil {
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Returns the custom email (generating it if not set) and list of used variables in it.
// @Summary Returns the custom email/message (generating it if not set) and list of used variables in it.
// @Produce json
// @Success 200 {object} customEmailDTO
// @Failure 400 {object} boolResponse
@ -135,7 +163,7 @@ func (app *appContext) SetCustomEmailState(gc *gin.Context) {
// @Router /config/emails/{id} [get]
// @Security Bearer
// @tags Configuration
func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
lang := app.storage.lang.chosenEmailLang
id := gc.Param("id")
var content string
@ -146,20 +174,26 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
var values map[string]interface{}
username := app.storage.lang.Email[lang].Strings.get("username")
emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress")
email := app.getCustomEmail(id)
if email == nil {
app.err.Printf("Failed to get custom email with ID \"%s\"", id)
customMessage := app.getCustomMessage(id)
if customMessage == nil {
app.err.Printf("Failed to get custom message with ID \"%s\"", id)
respondBool(400, false, gc)
return
}
if id == "WelcomeEmail" {
conditionals = []string{"{yourAccountWillExpire}"}
email.Conditionals = conditionals
customMessage.Conditionals = conditionals
} else if id == "UserPage" {
variables = []string{"{username}"}
customMessage.Variables = variables
} else if id == "UserLogin" {
variables = []string{}
customMessage.Variables = variables
}
content = email.Content
content = customMessage.Content
noContent := content == ""
if !noContent {
variables = email.Variables
variables = customMessage.Variables
}
switch id {
case "Announcement":
@ -215,12 +249,14 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
msg, err = app.email.constructUserExpired(app, true)
}
values = app.email.userExpiredValues(app, false)
case "UserLogin", "UserPage":
values = map[string]interface{}{}
}
if err != nil {
respondBool(500, false, gc)
return
}
if noContent && id != "Announcement" {
if noContent && id != "Announcement" && id != "UserPage" && id != "UserLogin" {
content = msg.Text
variables = make([]string, strings.Count(content, "{"))
i := 0
@ -239,7 +275,7 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
i++
}
}
email.Variables = variables
customMessage.Variables = variables
}
if variables == nil {
variables = []string{}
@ -248,10 +284,21 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
respondBool(500, false, gc)
return
}
mail, err := app.email.constructTemplate("", "<div class=\"preview-content\"></div>", app)
if err != nil {
if app.storage.storeUserPageContent() != nil {
respondBool(500, false, gc)
return
}
var mail *Message
if id != "UserLogin" && id != "UserPage" {
mail, err = app.email.constructTemplate("", "<div class=\"preview-content\"></div>", app)
if err != nil {
respondBool(500, false, gc)
return
}
} else {
mail = &Message{
HTML: "<div class=\"card ~neutral dark:~d_neutral @low preview-content\"></div>",
Markdown: "<div class=\"card ~neutral dark:~d_neutral @low preview-content\"></div>",
}
}
gc.JSON(200, customEmailDTO{Content: content, Variables: variables, Conditionals: conditionals, Values: values, HTML: mail.HTML, Plaintext: mail.Text})
}

@ -46,7 +46,7 @@ func (app *appContext) loadConfig() error {
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
}
}
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users", "matrix_users", "announcements"} {
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users", "matrix_users", "announcements", "custom_user_page_content"} {
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
}
for _, key := range []string{"matrix_sql"} {
@ -160,6 +160,12 @@ func (app *appContext) loadConfig() error {
app.storage.customEmails_path = app.config.Section("files").Key("custom_emails").String()
app.storage.loadCustomEmails()
app.MustSetValue("user_page", "enabled", "true")
if app.config.Section("user_page").Key("enabled").MustBool(false) {
app.storage.userPage_path = app.config.Section("files").Key("custom_user_page_content").String()
app.storage.loadUserPageContent()
}
substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("")
if substituteStrings != "" {

@ -1551,6 +1551,14 @@
"value": "",
"description": "JSON file generated by program in settings, different from email_html/email_text. See wiki for more info."
},
"custom_user_page_content": {
"name": "Custom user page content",
"required": false,
"requires_restart": false,
"type": "text",
"value": "",
"description": "JSON file generated by program in settings, containing user page messages. See wiki for more info."
},
"telegram_users": {
"name": "Telegram users",
"required": false,

@ -13,6 +13,8 @@
--border-width-2: 3px;
--border-width-4: 5px;
--border-width-8: 8px;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
.light {

@ -24,7 +24,7 @@ import (
sMail "github.com/xhit/go-simple-mail/v2"
)
var renderer = html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
var markdownRenderer = html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
// EmailClient implements email sending, right now via smtp, mailgun or a dummy client.
type EmailClient interface {
@ -353,7 +353,7 @@ func (emailer *Emailer) constructTemplate(subject, md string, app *appContext, u
subject = templateEmail(subject, []string{"{username}"}, nil, map[string]interface{}{"username": username[0]})
}
email := &Message{Subject: subject}
html := markdown.ToHTML([]byte(md), nil, renderer)
html := markdown.ToHTML([]byte(md), nil, markdownRenderer)
text := stripMarkdown(md)
message := app.config.Section("messages").Key("message").String()
var err error

@ -1,4 +1,4 @@
<meta charset="utf-8">
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="Description" content="jfa-go, a better way to manage Jellyfin users.">
<meta name="color-scheme" content="dark light">

@ -1,11 +1,20 @@
<div id="modal-login" class="modal">
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 form-login" href="">
<span class="heading">{{ .strings.login }}</span>
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="login-user">
<input type="password" class="field input ~neutral @high mb-4" placeholder="{{ .strings.password }}" id="login-password">
<label>
<input type="submit" class="unfocused">
<span class="button ~urge @low full-width center supra submit">{{ .strings.login }}</span>
</label>
</form>
<div class="my-[10%] row items-stretch relative mx-auto w-[40%] lg:w-[60%]">
{{ if index . "LoginMessageEnabled" }}
{{ if .LoginMessageEnabled }}
<div class="card mx-2 flex-initial w-[100%] xl:w-[35%] mb-4 xl:mb-0 ~neutral @low content">
{{ .LoginMessageContent }}
</div>
{{ end }}
{{ end }}
<form class="card mx-2 flex-auto form-login w-[100%] xl:w-[55%] mb-0" href="">
<span class="heading">{{ .strings.login }}</span>
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="login-user">
<input type="password" class="field input ~neutral @high mb-4" placeholder="{{ .strings.password }}" id="login-password">
<label>
<input type="submit" class="unfocused">
<span class="button ~urge @low full-width center supra submit">{{ .strings.login }}</span>
</label>
</form>
</div>
</div>

@ -43,6 +43,7 @@
</div>
</div>
</div>
{{ template "login-modal.html" . }}
{{ template "account-linking.html" . }}
<div id="notification-box"></div>
<div class="top-4 left-4 absolute">
@ -68,7 +69,6 @@
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
<span class="button ~critical @low mb-4 unfocused" id="logout-button">{{ .strings.logout }}</span>
</div>
{{ template "login-modal.html" . }}
<div class="page-container">
<div class="card @low dark:~d_neutral mb-4" id="card-user">
<span class="heading mb-2"></span>

@ -113,7 +113,9 @@
"actions": "Actions",
"searchOptions": "Search Options",
"matchText": "Match Text",
"jellyfinID": "Jellyfin ID"
"jellyfinID": "Jellyfin ID",
"userPageLogin": "User Page: Login",
"userPagePage": "User Page: Page"
},
"notifications": {
"changedEmailAddress": "Changed email address of {n}.",
@ -209,4 +211,4 @@
"plural": "Extended expiry for {n} users."
}
}
}
}

@ -61,4 +61,4 @@
"plural": "{n} Days"
}
}
}
}

@ -21,6 +21,8 @@
"sendPINDiscord": "Type {command} in {server_channel} on Discord, then send the PIN below.",
"matrixEnterUser": "Enter your User ID, press submit, and a PIN will be sent to you. Enter it here to continue.",
"welcomeUser": "Welcome, {user}!",
"addContactMethod": "Add Contact Method",
"editContactMethod": "Edit Contact Method",
"joinTheServer": "Join the server:"
},
"notifications": {
@ -61,4 +63,4 @@
"plural": "Must have at least {n} special characters"
}
}
}
}

@ -249,7 +249,7 @@ func (d *MatrixDaemon) Send(message *Message, users ...MatrixUser) (err error) {
md := ""
if message.Markdown != "" {
// Convert images to links
md = string(markdown.ToHTML([]byte(strings.ReplaceAll(message.Markdown, "![", "[")), nil, renderer))
md = string(markdown.ToHTML([]byte(strings.ReplaceAll(message.Markdown, "![", "[")), nil, markdownRenderer))
}
content := &event.MessageEventContent{
MsgType: "m.text",

@ -196,10 +196,10 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/config/update", app.CheckUpdate)
api.POST(p+"/config/update", app.ApplyUpdate)
api.GET(p+"/config/emails", app.GetCustomEmails)
api.GET(p+"/config/emails/:id", app.GetCustomEmailTemplate)
api.POST(p+"/config/emails/:id", app.SetCustomEmail)
api.POST(p+"/config/emails/:id/state/:state", app.SetCustomEmailState)
api.GET(p+"/config/emails", app.GetCustomContent)
api.GET(p+"/config/emails/:id", app.GetCustomMessageTemplate)
api.POST(p+"/config/emails/:id", app.SetCustomMessage)
api.POST(p+"/config/emails/:id/state/:state", app.SetCustomMessageState)
api.GET(p+"/config", app.GetConfig)
api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart)

@ -21,23 +21,24 @@ type matrixStore map[string]MatrixUser
type emailStore map[string]EmailAddress
type Storage struct {
timePattern string
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path, announcements_path, matrix_sql_path string
users map[string]time.Time // Map of Jellyfin User IDs to their expiry times.
invites Invites
profiles map[string]Profile
defaultProfile string
displayprefs, ombi_template map[string]interface{}
emails emailStore
telegram telegramStore // Map of Jellyfin User IDs to telegram users.
discord discordStore // Map of Jellyfin user IDs to discord users.
matrix matrixStore // Map of Jellyfin user IDs to Matrix users.
customEmails customEmails
policy mediabrowser.Policy
configuration mediabrowser.Configuration
lang Lang
announcements map[string]announcementTemplate
invitesLock, usersLock, discordLock, telegramLock, matrixLock, emailsLock sync.Mutex
timePattern string
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path, announcements_path, matrix_sql_path, userPage_path string
users map[string]time.Time // Map of Jellyfin User IDs to their expiry times.
invites Invites
profiles map[string]Profile
defaultProfile string
displayprefs, ombi_template map[string]interface{}
emails emailStore
telegram telegramStore // Map of Jellyfin User IDs to telegram users.
discord discordStore // Map of Jellyfin user IDs to discord users.
matrix matrixStore // Map of Jellyfin user IDs to Matrix users.
customEmails customEmails
userPage userPageContent
policy mediabrowser.Policy
configuration mediabrowser.Configuration
lang Lang
announcements map[string]announcementTemplate
invitesLock, usersLock, discordLock, telegramLock, matrixLock, emailsLock sync.Mutex
}
// GetEmails returns a copy of the store.
@ -172,25 +173,30 @@ type EmailAddress struct {
}
type customEmails struct {
UserCreated customEmail `json:"userCreated"`
InviteExpiry customEmail `json:"inviteExpiry"`
PasswordReset customEmail `json:"passwordReset"`
UserDeleted customEmail `json:"userDeleted"`
UserDisabled customEmail `json:"userDisabled"`
UserEnabled customEmail `json:"userEnabled"`
InviteEmail customEmail `json:"inviteEmail"`
WelcomeEmail customEmail `json:"welcomeEmail"`
EmailConfirmation customEmail `json:"emailConfirmation"`
UserExpired customEmail `json:"userExpired"`
}
type customEmail struct {
UserCreated customContent `json:"userCreated"`
InviteExpiry customContent `json:"inviteExpiry"`
PasswordReset customContent `json:"passwordReset"`
UserDeleted customContent `json:"userDeleted"`
UserDisabled customContent `json:"userDisabled"`
UserEnabled customContent `json:"userEnabled"`
InviteEmail customContent `json:"inviteEmail"`
WelcomeEmail customContent `json:"welcomeEmail"`
EmailConfirmation customContent `json:"emailConfirmation"`
UserExpired customContent `json:"userExpired"`
}
type customContent struct {
Enabled bool `json:"enabled,omitempty"`
Content string `json:"content"`
Variables []string `json:"variables,omitempty"`
Conditionals []string `json:"conditionals,omitempty"`
}
type userPageContent struct {
Login customContent `json:"login"`
Page customContent `json:"page"`
}
// timePattern: %Y-%m-%dT%H:%M:%S.%f
type Profile struct {
@ -981,6 +987,14 @@ func (st *Storage) storeCustomEmails() error {
return storeJSON(st.customEmails_path, st.customEmails)
}
func (st *Storage) loadUserPageContent() error {
return loadJSON(st.userPage_path, &st.userPage)
}
func (st *Storage) storeUserPageContent() error {
return storeJSON(st.userPage_path, st.userPage)
}
func (st *Storage) loadPolicy() error {
return loadJSON(st.policy_path, &st.policy)
}

@ -542,7 +542,7 @@ export class settingsList {
private _sections: { [name: string]: sectionPanel }
private _buttons: { [name: string]: HTMLSpanElement }
private _needsRestart: boolean = false;
private _emailEditor = new EmailEditor();
private _messageEditor = new MessageEditor();
addSection = (name: string, s: Section, subButton?: HTMLElement) => {
const section = new sectionPanel(s, name);
@ -713,7 +713,7 @@ export class settingsList {
if (name in this._sections) {
this._sections[name].update(settings.sections[name]);
} else {
if (name == "messages") {
if (name == "messages" || name == "user_page") {
const editButton = document.createElement("div");
editButton.classList.add("tooltip", "left");
editButton.innerHTML = `
@ -724,7 +724,9 @@ export class settingsList {
${window.lang.get("strings", "customizeMessages")}
</span>
`;
(editButton.querySelector("span.button") as HTMLSpanElement).onclick = this._emailEditor.showList;
(editButton.querySelector("span.button") as HTMLSpanElement).onclick = () => {
this._messageEditor.showList(name == "messages" ? "email" : "user");
};
this.addSection(name, settings.sections[name], editButton);
} else if (name == "updates") {
const icon = document.createElement("span") as HTMLSpanElement;
@ -773,7 +775,7 @@ interface emailListEl {
enabled: boolean;
}
class EmailEditor {
class MessageEditor {
private _currentID: string;
private _names: { [id: string]: emailListEl };
private _content: string;
@ -884,8 +886,8 @@ class EmailEditor {
// }, true);
}
showList = () => {
_get("/config/emails?lang=" + window.language, null, (req: XMLHttpRequest) => {
showList = (filter?: string) => {
_get("/config/emails?lang=" + window.language + (filter ? "&filter=" + filter : ""), null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
window.notifications.customError("loadTemplateError", window.lang.notif("errorFailureCheckLogs"));

@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/gomarkdown/markdown"
"github.com/hrfee/mediabrowser"
"github.com/steambap/captcha"
)
@ -203,6 +204,24 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
data["discordServerName"] = app.discord.serverName
data["discordInviteLink"] = app.discord.inviteChannelName != ""
}
pageMessages := map[string]*customContent{
"Login": app.getCustomMessage("UserLogin"),
"Page": app.getCustomMessage("UserPage"),
}
for name, msg := range pageMessages {
if msg == nil {
continue
}
data[name+"MessageEnabled"] = msg.Enabled
if !msg.Enabled {
continue
}
// We don't template here, since the username is only known after login.
data[name+"MessageContent"] = template.HTML(markdown.ToHTML([]byte(msg.Content), nil, markdownRenderer))
}
gcHTML(gc, http.StatusOK, "user.html", data)
}

Loading…
Cancel
Save