1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-11-09 20:00:12 +00:00

Compare commits

...

3 Commits

Author SHA1 Message Date
b546aeb440
userpage: don't wrap contact methods, ellipsise 2023-06-20 22:18:38 +01:00
99679a800d
userpage: add customizable message on page 2023-06-20 21:54:55 +01:00
7b9b0d8a84
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.
2023-06-20 21:43:25 +01:00
17 changed files with 209 additions and 91 deletions

View File

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

View File

@ -46,7 +46,7 @@ func (app *appContext) loadConfig() error {
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json")))) 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")))) 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"} { 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.customEmails_path = app.config.Section("files").Key("custom_emails").String()
app.storage.loadCustomEmails() 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("") substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("")
if substituteStrings != "" { if substituteStrings != "" {

View File

@ -1551,6 +1551,14 @@
"value": "", "value": "",
"description": "JSON file generated by program in settings, different from email_html/email_text. See wiki for more info." "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": { "telegram_users": {
"name": "Telegram users", "name": "Telegram users",
"required": false, "required": false,

View File

@ -13,6 +13,8 @@
--border-width-2: 3px; --border-width-2: 3px;
--border-width-4: 5px; --border-width-4: 5px;
--border-width-8: 8px; --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 { .light {

View File

@ -24,7 +24,7 @@ import (
sMail "github.com/xhit/go-simple-mail/v2" 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. // EmailClient implements email sending, right now via smtp, mailgun or a dummy client.
type EmailClient interface { 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]}) subject = templateEmail(subject, []string{"{username}"}, nil, map[string]interface{}{"username": username[0]})
} }
email := &Message{Subject: subject} email := &Message{Subject: subject}
html := markdown.ToHTML([]byte(md), nil, renderer) html := markdown.ToHTML([]byte(md), nil, markdownRenderer)
text := stripMarkdown(md) text := stripMarkdown(md)
message := app.config.Section("messages").Key("message").String() message := app.config.Section("messages").Key("message").String()
var err error var err error

View File

@ -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="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="Description" content="jfa-go, a better way to manage Jellyfin users.">
<meta name="color-scheme" content="dark light"> <meta name="color-scheme" content="dark light">

View File

@ -1,5 +1,13 @@
<div id="modal-login" class="modal"> <div id="modal-login" class="modal">
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 form-login" href=""> <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> <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="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"> <input type="password" class="field input ~neutral @high mb-4" placeholder="{{ .strings.password }}" id="login-password">
@ -9,3 +17,4 @@
</label> </label>
</form> </form>
</div> </div>
</div>

View File

@ -43,6 +43,7 @@
</div> </div>
</div> </div>
</div> </div>
{{ template "login-modal.html" . }}
{{ template "account-linking.html" . }} {{ template "account-linking.html" . }}
<div id="notification-box"></div> <div id="notification-box"></div>
<div class="top-4 left-4 absolute"> <div class="top-4 left-4 absolute">
@ -68,12 +69,11 @@
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span> <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> <span class="button ~critical @low mb-4 unfocused" id="logout-button">{{ .strings.logout }}</span>
</div> </div>
{{ template "login-modal.html" . }}
<div class="page-container"> <div class="page-container">
<div class="card @low dark:~d_neutral mb-4" id="card-user"> <div class="card @low dark:~d_neutral mb-4" id="card-user">
<span class="heading mb-2"></span> <span class="heading mb-2"></span>
</div> </div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="card @low dark:~d_neutral unfocused" id="card-status"> <div class="card @low dark:~d_neutral unfocused" id="card-status">
<span class="heading mb-2">{{ .strings.expiry }}</span> <span class="heading mb-2">{{ .strings.expiry }}</span>
<aside class="aside ~warning user-expiry my-4"></aside> <aside class="aside ~warning user-expiry my-4"></aside>
@ -83,6 +83,13 @@
<span class="heading mb-2">{{ .strings.contactMethods }}</span> <span class="heading mb-2">{{ .strings.contactMethods }}</span>
<div class="content flex justify-between flex-col h-100"></div> <div class="content flex justify-between flex-col h-100"></div>
</div> </div>
{{ if index . "PageMessageEnabled" }}
{{ if .PageMessageEnabled }}
<div class="card @low dark:~d_neutral content">
{{ .PageMessageContent }}
</div>
{{ end }}
{{ end }}
</div> </div>
</div> </div>
<script src="{{ .urlBase }}/js/user.js" type="module"></script> <script src="{{ .urlBase }}/js/user.js" type="module"></script>

View File

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

View File

@ -21,6 +21,8 @@
"sendPINDiscord": "Type {command} in {server_channel} on Discord, then send the PIN below.", "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.", "matrixEnterUser": "Enter your User ID, press submit, and a PIN will be sent to you. Enter it here to continue.",
"welcomeUser": "Welcome, {user}!", "welcomeUser": "Welcome, {user}!",
"addContactMethod": "Add Contact Method",
"editContactMethod": "Edit Contact Method",
"joinTheServer": "Join the server:" "joinTheServer": "Join the server:"
}, },
"notifications": { "notifications": {

View File

@ -249,7 +249,7 @@ func (d *MatrixDaemon) Send(message *Message, users ...MatrixUser) (err error) {
md := "" md := ""
if message.Markdown != "" { if message.Markdown != "" {
// Convert images to links // 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{ content := &event.MessageEventContent{
MsgType: "m.text", MsgType: "m.text",

View File

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

View File

@ -22,7 +22,7 @@ type emailStore map[string]EmailAddress
type Storage struct { type Storage struct {
timePattern string 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 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. users map[string]time.Time // Map of Jellyfin User IDs to their expiry times.
invites Invites invites Invites
profiles map[string]Profile profiles map[string]Profile
@ -33,6 +33,7 @@ type Storage struct {
discord discordStore // Map of Jellyfin user IDs to discord users. discord discordStore // Map of Jellyfin user IDs to discord users.
matrix matrixStore // Map of Jellyfin user IDs to Matrix users. matrix matrixStore // Map of Jellyfin user IDs to Matrix users.
customEmails customEmails customEmails customEmails
userPage userPageContent
policy mediabrowser.Policy policy mediabrowser.Policy
configuration mediabrowser.Configuration configuration mediabrowser.Configuration
lang Lang lang Lang
@ -172,25 +173,30 @@ type EmailAddress struct {
} }
type customEmails struct { type customEmails struct {
UserCreated customEmail `json:"userCreated"` UserCreated customContent `json:"userCreated"`
InviteExpiry customEmail `json:"inviteExpiry"` InviteExpiry customContent `json:"inviteExpiry"`
PasswordReset customEmail `json:"passwordReset"` PasswordReset customContent `json:"passwordReset"`
UserDeleted customEmail `json:"userDeleted"` UserDeleted customContent `json:"userDeleted"`
UserDisabled customEmail `json:"userDisabled"` UserDisabled customContent `json:"userDisabled"`
UserEnabled customEmail `json:"userEnabled"` UserEnabled customContent `json:"userEnabled"`
InviteEmail customEmail `json:"inviteEmail"` InviteEmail customContent `json:"inviteEmail"`
WelcomeEmail customEmail `json:"welcomeEmail"` WelcomeEmail customContent `json:"welcomeEmail"`
EmailConfirmation customEmail `json:"emailConfirmation"` EmailConfirmation customContent `json:"emailConfirmation"`
UserExpired customEmail `json:"userExpired"` UserExpired customContent `json:"userExpired"`
} }
type customEmail struct { type customContent struct {
Enabled bool `json:"enabled,omitempty"` Enabled bool `json:"enabled,omitempty"`
Content string `json:"content"` Content string `json:"content"`
Variables []string `json:"variables,omitempty"` Variables []string `json:"variables,omitempty"`
Conditionals []string `json:"conditionals,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 // timePattern: %Y-%m-%dT%H:%M:%S.%f
type Profile struct { type Profile struct {
@ -981,6 +987,14 @@ func (st *Storage) storeCustomEmails() error {
return storeJSON(st.customEmails_path, st.customEmails) 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 { func (st *Storage) loadPolicy() error {
return loadJSON(st.policy_path, &st.policy) return loadJSON(st.policy_path, &st.policy)
} }

View File

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

View File

@ -94,17 +94,17 @@ class ContactMethods {
append = (name: string, details: MyDetailsContactMethod, icon: string, addEditFunc?: (add: boolean) => void, required?: boolean) => { append = (name: string, details: MyDetailsContactMethod, icon: string, addEditFunc?: (add: boolean) => void, required?: boolean) => {
const row = document.createElement("div"); const row = document.createElement("div");
row.classList.add("row", "flex-expand", "my-2"); row.classList.add("flex", "flex-expand", "my-2", "flex-nowrap");
let innerHTML = ` let innerHTML = `
<div class="inline align-middle"> <div class="flex items-baseline flex-nowrap ellipsis">
<span class="shield ~urge" alt="${name}"> <span class="shield ~urge" alt="${name}">
<span class="icon"> <span class="icon">
${icon} ${icon}
</span> </span>
</span> </span>
<span class="ml-2 font-bold">${(details.value == "") ? window.lang.strings("notSet") : details.value}</span> <span class="ml-2 font-bold text-ellipsis overflow-hidden">${(details.value == "") ? window.lang.strings("notSet") : details.value}</span>
</div> </div>
<div class="flex items-center"> <div class="flex items-center ml-2">
<button class="user-contact-enabled-disabled button ~neutral" ${details.value == "" ? "disabled" : ""}> <button class="user-contact-enabled-disabled button ~neutral" ${details.value == "" ? "disabled" : ""}>
<input type="checkbox" class="mr-2" ${details.value == "" ? "disabled" : ""}> <input type="checkbox" class="mr-2" ${details.value == "" ? "disabled" : ""}>
<span>${window.lang.strings("enabled")}</span> <span>${window.lang.strings("enabled")}</span>

View File

@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
"github.com/gomarkdown/markdown"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
"github.com/steambap/captcha" "github.com/steambap/captcha"
) )
@ -203,6 +204,24 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
data["discordServerName"] = app.discord.serverName data["discordServerName"] = app.discord.serverName
data["discordInviteLink"] = app.discord.inviteChannelName != "" 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) gcHTML(gc, http.StatusOK, "user.html", data)
} }