mirror of
https://github.com/hrfee/jfa-go.git
synced 2025-01-01 14:00:12 +00:00
Compare commits
6 Commits
6ffdd4dad7
...
f40fb9d3f7
Author | SHA1 | Date | |
---|---|---|---|
|
f40fb9d3f7 | ||
|
9536ceaaa4 | ||
|
72beee1322 | ||
|
0ec822988d | ||
d1b1b90de3 | |||
058cac2e7b |
108
api.go
108
api.go
@ -1276,13 +1276,13 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
|
|||||||
// @tags Configuration
|
// @tags Configuration
|
||||||
func (app *appContext) GetEmails(gc *gin.Context) {
|
func (app *appContext) GetEmails(gc *gin.Context) {
|
||||||
gc.JSON(200, emailListDTO{
|
gc.JSON(200, emailListDTO{
|
||||||
"UserCreated": app.storage.lang.Email["en-us"].UserCreated["name"],
|
"UserCreated": {Name: app.storage.lang.Email["en-us"].UserCreated["name"], Enabled: app.storage.customEmails.UserCreated.Enabled},
|
||||||
"InviteExpiry": app.storage.lang.Email["en-us"].InviteExpiry["name"],
|
"InviteExpiry": {Name: app.storage.lang.Email["en-us"].InviteExpiry["name"], Enabled: app.storage.customEmails.InviteExpiry.Enabled},
|
||||||
"PasswordReset": app.storage.lang.Email["en-us"].PasswordReset["name"],
|
"PasswordReset": {Name: app.storage.lang.Email["en-us"].PasswordReset["name"], Enabled: app.storage.customEmails.PasswordReset.Enabled},
|
||||||
"UserDeleted": app.storage.lang.Email["en-us"].UserDeleted["name"],
|
"UserDeleted": {Name: app.storage.lang.Email["en-us"].UserDeleted["name"], Enabled: app.storage.customEmails.UserDeleted.Enabled},
|
||||||
"InviteEmail": app.storage.lang.Email["en-us"].InviteEmail["name"],
|
"InviteEmail": {Name: app.storage.lang.Email["en-us"].InviteEmail["name"], Enabled: app.storage.customEmails.InviteEmail.Enabled},
|
||||||
"WelcomeEmail": app.storage.lang.Email["en-us"].WelcomeEmail["name"],
|
"WelcomeEmail": {Name: app.storage.lang.Email["en-us"].WelcomeEmail["name"], Enabled: app.storage.customEmails.WelcomeEmail.Enabled},
|
||||||
"EmailConfirmation": app.storage.lang.Email["en-us"].EmailConfirmation["name"],
|
"EmailConfirmation": {Name: app.storage.lang.Email["en-us"].EmailConfirmation["name"], Enabled: app.storage.customEmails.EmailConfirmation.Enabled},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1339,7 +1339,7 @@ func (app *appContext) SetEmail(gc *gin.Context) {
|
|||||||
// @Success 200 {object} boolResponse
|
// @Success 200 {object} boolResponse
|
||||||
// @Failure 400 {object} boolResponse
|
// @Failure 400 {object} boolResponse
|
||||||
// @Failure 500 {object} boolResponse
|
// @Failure 500 {object} boolResponse
|
||||||
// @Router /config/emails/{id}/{enable/disable} [post]
|
// @Router /config/emails/{id}/state/{enable/disable} [post]
|
||||||
// @tags Configuration
|
// @tags Configuration
|
||||||
func (app *appContext) SetEmailState(gc *gin.Context) {
|
func (app *appContext) SetEmailState(gc *gin.Context) {
|
||||||
id := gc.Param("id")
|
id := gc.Param("id")
|
||||||
@ -1375,6 +1375,84 @@ func (app *appContext) SetEmailState(gc *gin.Context) {
|
|||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary Render and return an email for testing purposes.
|
||||||
|
// @Produce json
|
||||||
|
// @Param customEmail body customEmail true "Content = email (in markdown)."
|
||||||
|
// @Success 200 {object} Email
|
||||||
|
// @Failure 400 {object} boolResponse
|
||||||
|
// @Failure 500 {object} boolResponse
|
||||||
|
// @Router /config/emails/{id}/test [post]
|
||||||
|
// @tags Configuration
|
||||||
|
func (app *appContext) GetTestEmail(gc *gin.Context) {
|
||||||
|
var req customEmail
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
if req.Content == "" {
|
||||||
|
app.debug.Println("Test failed: Content was empty")
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := gc.Param("id")
|
||||||
|
var msg *Email
|
||||||
|
var err error
|
||||||
|
var cache customEmail
|
||||||
|
var restore func(cache customEmail)
|
||||||
|
username := app.storage.lang.Email[app.storage.lang.chosenEmailLang].Strings.get("username")
|
||||||
|
emailAddress := app.storage.lang.Email[app.storage.lang.chosenEmailLang].Strings.get("emailAddress")
|
||||||
|
if id == "UserCreated" {
|
||||||
|
cache = app.storage.customEmails.UserCreated
|
||||||
|
restore = func(cache customEmail) { app.storage.customEmails.UserCreated = cache }
|
||||||
|
app.storage.customEmails.UserCreated.Content = req.Content
|
||||||
|
app.storage.customEmails.UserCreated.Enabled = true
|
||||||
|
msg, err = app.email.constructCreated("xxxxxx", username, emailAddress, Invite{}, app, false)
|
||||||
|
} else if id == "InviteExpiry" {
|
||||||
|
cache = app.storage.customEmails.InviteExpiry
|
||||||
|
restore = func(cache customEmail) { app.storage.customEmails.InviteExpiry = cache }
|
||||||
|
app.storage.customEmails.InviteExpiry.Content = req.Content
|
||||||
|
app.storage.customEmails.InviteExpiry.Enabled = true
|
||||||
|
msg, err = app.email.constructExpiry("xxxxxx", Invite{}, app, false)
|
||||||
|
} else if id == "PasswordReset" {
|
||||||
|
cache = app.storage.customEmails.PasswordReset
|
||||||
|
restore = func(cache customEmail) { app.storage.customEmails.PasswordReset = cache }
|
||||||
|
app.storage.customEmails.PasswordReset.Content = req.Content
|
||||||
|
app.storage.customEmails.PasswordReset.Enabled = true
|
||||||
|
msg, err = app.email.constructReset(PasswordReset{Pin: "12-34-56", Username: username}, app, false)
|
||||||
|
} else if id == "UserDeleted" {
|
||||||
|
cache = app.storage.customEmails.UserDeleted
|
||||||
|
restore = func(cache customEmail) { app.storage.customEmails.UserDeleted = cache }
|
||||||
|
app.storage.customEmails.UserDeleted.Content = req.Content
|
||||||
|
app.storage.customEmails.UserDeleted.Enabled = true
|
||||||
|
msg, err = app.email.constructDeleted(app.storage.lang.Email[app.storage.lang.chosenEmailLang].UserDeleted.get("reason"), app, false)
|
||||||
|
} else if id == "InviteEmail" {
|
||||||
|
cache = app.storage.customEmails.InviteEmail
|
||||||
|
restore = func(cache customEmail) { app.storage.customEmails.InviteEmail = cache }
|
||||||
|
app.storage.customEmails.InviteEmail.Content = req.Content
|
||||||
|
app.storage.customEmails.InviteEmail.Enabled = true
|
||||||
|
msg, err = app.email.constructInvite("xxxxxx", Invite{}, app, false)
|
||||||
|
} else if id == "WelcomeEmail" {
|
||||||
|
cache = app.storage.customEmails.WelcomeEmail
|
||||||
|
restore = func(cache customEmail) { app.storage.customEmails.WelcomeEmail = cache }
|
||||||
|
app.storage.customEmails.WelcomeEmail.Content = req.Content
|
||||||
|
app.storage.customEmails.WelcomeEmail.Enabled = true
|
||||||
|
msg, err = app.email.constructWelcome(username, app, false)
|
||||||
|
} else if id == "EmailConfirmation" {
|
||||||
|
cache = app.storage.customEmails.EmailConfirmation
|
||||||
|
restore = func(cache customEmail) { app.storage.customEmails.EmailConfirmation = cache }
|
||||||
|
app.storage.customEmails.EmailConfirmation.Content = req.Content
|
||||||
|
app.storage.customEmails.EmailConfirmation.Enabled = true
|
||||||
|
msg, err = app.email.constructConfirmation("xxxxxx", username, "xxxxxx", app, false)
|
||||||
|
} else {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
restore(cache)
|
||||||
|
if err != nil {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
app.err.Printf("Failed to construct test email: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gc.JSON(200, msg)
|
||||||
|
}
|
||||||
|
|
||||||
// @Summary Returns the boilerplate email and list of used variables in it.
|
// @Summary Returns the boilerplate email and list of used variables in it.
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} customEmail
|
// @Success 200 {object} customEmail
|
||||||
@ -1395,7 +1473,7 @@ func (app *appContext) GetEmail(gc *gin.Context) {
|
|||||||
if content == "" {
|
if content == "" {
|
||||||
newEmail = true
|
newEmail = true
|
||||||
msg, err = app.email.constructCreated("", "", "", Invite{}, app, true)
|
msg, err = app.email.constructCreated("", "", "", Invite{}, app, true)
|
||||||
content = msg.text
|
content = msg.Text
|
||||||
} else {
|
} else {
|
||||||
variables = app.storage.customEmails.UserCreated.Variables
|
variables = app.storage.customEmails.UserCreated.Variables
|
||||||
}
|
}
|
||||||
@ -1406,7 +1484,7 @@ func (app *appContext) GetEmail(gc *gin.Context) {
|
|||||||
if content == "" {
|
if content == "" {
|
||||||
newEmail = true
|
newEmail = true
|
||||||
msg, err = app.email.constructExpiry("", Invite{}, app, true)
|
msg, err = app.email.constructExpiry("", Invite{}, app, true)
|
||||||
content = msg.text
|
content = msg.Text
|
||||||
} else {
|
} else {
|
||||||
variables = app.storage.customEmails.InviteExpiry.Variables
|
variables = app.storage.customEmails.InviteExpiry.Variables
|
||||||
}
|
}
|
||||||
@ -1417,7 +1495,7 @@ func (app *appContext) GetEmail(gc *gin.Context) {
|
|||||||
if content == "" {
|
if content == "" {
|
||||||
newEmail = true
|
newEmail = true
|
||||||
msg, err = app.email.constructReset(PasswordReset{}, app, true)
|
msg, err = app.email.constructReset(PasswordReset{}, app, true)
|
||||||
content = msg.text
|
content = msg.Text
|
||||||
} else {
|
} else {
|
||||||
variables = app.storage.customEmails.PasswordReset.Variables
|
variables = app.storage.customEmails.PasswordReset.Variables
|
||||||
}
|
}
|
||||||
@ -1428,7 +1506,7 @@ func (app *appContext) GetEmail(gc *gin.Context) {
|
|||||||
if content == "" {
|
if content == "" {
|
||||||
newEmail = true
|
newEmail = true
|
||||||
msg, err = app.email.constructDeleted("", app, true)
|
msg, err = app.email.constructDeleted("", app, true)
|
||||||
content = msg.text
|
content = msg.Text
|
||||||
} else {
|
} else {
|
||||||
variables = app.storage.customEmails.UserDeleted.Variables
|
variables = app.storage.customEmails.UserDeleted.Variables
|
||||||
}
|
}
|
||||||
@ -1439,7 +1517,7 @@ func (app *appContext) GetEmail(gc *gin.Context) {
|
|||||||
if content == "" {
|
if content == "" {
|
||||||
newEmail = true
|
newEmail = true
|
||||||
msg, err = app.email.constructInvite("", Invite{}, app, true)
|
msg, err = app.email.constructInvite("", Invite{}, app, true)
|
||||||
content = msg.text
|
content = msg.Text
|
||||||
} else {
|
} else {
|
||||||
variables = app.storage.customEmails.InviteEmail.Variables
|
variables = app.storage.customEmails.InviteEmail.Variables
|
||||||
}
|
}
|
||||||
@ -1450,7 +1528,7 @@ func (app *appContext) GetEmail(gc *gin.Context) {
|
|||||||
if content == "" {
|
if content == "" {
|
||||||
newEmail = true
|
newEmail = true
|
||||||
msg, err = app.email.constructWelcome("", app, true)
|
msg, err = app.email.constructWelcome("", app, true)
|
||||||
content = msg.text
|
content = msg.Text
|
||||||
} else {
|
} else {
|
||||||
variables = app.storage.customEmails.WelcomeEmail.Variables
|
variables = app.storage.customEmails.WelcomeEmail.Variables
|
||||||
}
|
}
|
||||||
@ -1461,7 +1539,7 @@ func (app *appContext) GetEmail(gc *gin.Context) {
|
|||||||
if content == "" {
|
if content == "" {
|
||||||
newEmail = true
|
newEmail = true
|
||||||
msg, err = app.email.constructConfirmation("", "", "", app, true)
|
msg, err = app.email.constructConfirmation("", "", "", app, true)
|
||||||
content = msg.text
|
content = msg.Text
|
||||||
} else {
|
} else {
|
||||||
variables = app.storage.customEmails.EmailConfirmation.Variables
|
variables = app.storage.customEmails.EmailConfirmation.Variables
|
||||||
}
|
}
|
||||||
|
13
css/base.css
13
css/base.css
@ -88,6 +88,10 @@ div.card:contains(section.banner.footer) {
|
|||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mr-half {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mr-1 {
|
.mr-1 {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
@ -149,6 +153,11 @@ div.card:contains(section.banner.footer) {
|
|||||||
margin: 0.5rem;
|
margin: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 400px) {
|
@media screen and (max-width: 400px) {
|
||||||
.row {
|
.row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -269,6 +278,10 @@ sup.\~critical, .text-critical {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-auto {
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.center {
|
.center {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
63
email.go
63
email.go
@ -33,14 +33,14 @@ type Mailgun struct {
|
|||||||
func (mg *Mailgun) send(fromName, fromAddr string, email *Email, address ...string) error {
|
func (mg *Mailgun) send(fromName, fromAddr string, email *Email, address ...string) error {
|
||||||
message := mg.client.NewMessage(
|
message := mg.client.NewMessage(
|
||||||
fmt.Sprintf("%s <%s>", fromName, fromAddr),
|
fmt.Sprintf("%s <%s>", fromName, fromAddr),
|
||||||
email.subject,
|
email.Subject,
|
||||||
email.text,
|
email.Text,
|
||||||
)
|
)
|
||||||
for _, a := range address {
|
for _, a := range address {
|
||||||
// Adding variable tells mailgun to do a batch send, so users don't see other recipients.
|
// Adding variable tells mailgun to do a batch send, so users don't see other recipients.
|
||||||
message.AddRecipientAndVariables(a, map[string]interface{}{"unique_id": a})
|
message.AddRecipientAndVariables(a, map[string]interface{}{"unique_id": a})
|
||||||
}
|
}
|
||||||
message.SetHtml(email.html)
|
message.SetHtml(email.HTML)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
_, _, err := mg.client.Send(ctx, message)
|
_, _, err := mg.client.Send(ctx, message)
|
||||||
@ -69,10 +69,10 @@ func (sm *SMTP) send(fromName, fromAddr string, email *Email, address ...string)
|
|||||||
go func(addr string) {
|
go func(addr string) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
e := jEmail.NewEmail()
|
e := jEmail.NewEmail()
|
||||||
e.Subject = email.subject
|
e.Subject = email.Subject
|
||||||
e.From = from
|
e.From = from
|
||||||
e.Text = []byte(email.text)
|
e.Text = []byte(email.Text)
|
||||||
e.HTML = []byte(email.html)
|
e.HTML = []byte(email.HTML)
|
||||||
e.To = []string{addr}
|
e.To = []string{addr}
|
||||||
if sm.sslTLS {
|
if sm.sslTLS {
|
||||||
err = e.SendWithTLS(server, sm.auth, tlsConfig)
|
err = e.SendWithTLS(server, sm.auth, tlsConfig)
|
||||||
@ -94,8 +94,9 @@ type Emailer struct {
|
|||||||
|
|
||||||
// Email stores content.
|
// Email stores content.
|
||||||
type Email struct {
|
type Email struct {
|
||||||
subject string
|
Subject string `json:"subject"`
|
||||||
html, text string
|
HTML string `json:"html"`
|
||||||
|
Text string `json:"text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (emailer *Emailer) formatExpiry(expiry time.Time, tzaware bool, datePattern, timePattern string) (d, t, expiresIn string) {
|
func (emailer *Emailer) formatExpiry(expiry time.Time, tzaware bool, datePattern, timePattern string) (d, t, expiresIn string) {
|
||||||
@ -213,7 +214,7 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
|
|||||||
|
|
||||||
func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext, noSub bool) (*Email, error) {
|
func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext, noSub bool) (*Email, error) {
|
||||||
email := &Email{
|
email := &Email{
|
||||||
subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")),
|
Subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")),
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
template := map[string]interface{}{
|
template := map[string]interface{}{
|
||||||
@ -245,9 +246,9 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a
|
|||||||
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
email, err = emailer.constructTemplate(email.subject, content, app)
|
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||||
} else {
|
} else {
|
||||||
email.html, email.text, err = emailer.construct(app, "email_confirmation", "email_", template)
|
email.HTML, email.Text, err = emailer.construct(app, "email_confirmation", "email_", template)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -256,13 +257,13 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (*Email, error) {
|
func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (*Email, error) {
|
||||||
email := &Email{subject: subject}
|
email := &Email{Subject: subject}
|
||||||
renderer := html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
|
renderer := html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
|
||||||
html := markdown.ToHTML([]byte(md), nil, renderer)
|
html := markdown.ToHTML([]byte(md), nil, renderer)
|
||||||
text := stripMarkdown(md)
|
text := stripMarkdown(md)
|
||||||
message := app.config.Section("email").Key("message").String()
|
message := app.config.Section("email").Key("message").String()
|
||||||
var err error
|
var err error
|
||||||
email.html, email.text, err = emailer.construct(app, "template_email", "email_", map[string]interface{}{
|
email.HTML, email.Text, err = emailer.construct(app, "template_email", "email_", map[string]interface{}{
|
||||||
"text": template.HTML(html),
|
"text": template.HTML(html),
|
||||||
"plaintext": text,
|
"plaintext": text,
|
||||||
"message": message,
|
"message": message,
|
||||||
@ -275,7 +276,7 @@ func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (
|
|||||||
|
|
||||||
func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext, noSub bool) (*Email, error) {
|
func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext, noSub bool) (*Email, error) {
|
||||||
email := &Email{
|
email := &Email{
|
||||||
subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.InviteEmail.get("title")),
|
Subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.InviteEmail.get("title")),
|
||||||
}
|
}
|
||||||
expiry := invite.ValidTill
|
expiry := invite.ValidTill
|
||||||
d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern)
|
d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern)
|
||||||
@ -312,9 +313,9 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
|
|||||||
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
email, err = emailer.constructTemplate(email.subject, content, app)
|
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||||
} else {
|
} else {
|
||||||
email.html, email.text, err = emailer.construct(app, "invite_emails", "email_", template)
|
email.HTML, email.Text, err = emailer.construct(app, "invite_emails", "email_", template)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -324,7 +325,7 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
|
|||||||
|
|
||||||
func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext, noSub bool) (*Email, error) {
|
func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext, noSub bool) (*Email, error) {
|
||||||
email := &Email{
|
email := &Email{
|
||||||
subject: emailer.lang.InviteExpiry.get("title"),
|
Subject: emailer.lang.InviteExpiry.get("title"),
|
||||||
}
|
}
|
||||||
expiry := app.formatDatetime(invite.ValidTill)
|
expiry := app.formatDatetime(invite.ValidTill)
|
||||||
template := map[string]interface{}{
|
template := map[string]interface{}{
|
||||||
@ -347,9 +348,9 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
|
|||||||
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
email, err = emailer.constructTemplate(email.subject, content, app)
|
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||||
} else {
|
} else {
|
||||||
email.html, email.text, err = emailer.construct(app, "notifications", "expiry_", template)
|
email.HTML, email.Text, err = emailer.construct(app, "notifications", "expiry_", template)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -359,7 +360,7 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
|
|||||||
|
|
||||||
func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext, noSub bool) (*Email, error) {
|
func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext, noSub bool) (*Email, error) {
|
||||||
email := &Email{
|
email := &Email{
|
||||||
subject: emailer.lang.UserCreated.get("title"),
|
Subject: emailer.lang.UserCreated.get("title"),
|
||||||
}
|
}
|
||||||
template := map[string]interface{}{
|
template := map[string]interface{}{
|
||||||
"nameString": emailer.lang.Strings.get("name"),
|
"nameString": emailer.lang.Strings.get("name"),
|
||||||
@ -397,9 +398,9 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
|
|||||||
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
email, err = emailer.constructTemplate(email.subject, content, app)
|
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||||
} else {
|
} else {
|
||||||
email.html, email.text, err = emailer.construct(app, "notifications", "created_", template)
|
email.HTML, email.Text, err = emailer.construct(app, "notifications", "created_", template)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -409,7 +410,7 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
|
|||||||
|
|
||||||
func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub bool) (*Email, error) {
|
func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub bool) (*Email, error) {
|
||||||
email := &Email{
|
email := &Email{
|
||||||
subject: app.config.Section("password_resets").Key("subject").MustString(emailer.lang.PasswordReset.get("title")),
|
Subject: app.config.Section("password_resets").Key("subject").MustString(emailer.lang.PasswordReset.get("title")),
|
||||||
}
|
}
|
||||||
d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
|
d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
|
||||||
message := app.config.Section("email").Key("message").String()
|
message := app.config.Section("email").Key("message").String()
|
||||||
@ -446,9 +447,9 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub
|
|||||||
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
email, err = emailer.constructTemplate(email.subject, content, app)
|
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||||
} else {
|
} else {
|
||||||
email.html, email.text, err = emailer.construct(app, "password_resets", "email_", template)
|
email.HTML, email.Text, err = emailer.construct(app, "password_resets", "email_", template)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -458,7 +459,7 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub
|
|||||||
|
|
||||||
func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub bool) (*Email, error) {
|
func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub bool) (*Email, error) {
|
||||||
email := &Email{
|
email := &Email{
|
||||||
subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")),
|
Subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")),
|
||||||
}
|
}
|
||||||
template := map[string]interface{}{
|
template := map[string]interface{}{
|
||||||
"yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
|
"yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
|
||||||
@ -483,9 +484,9 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub b
|
|||||||
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
email, err = emailer.constructTemplate(email.subject, content, app)
|
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||||
} else {
|
} else {
|
||||||
email.html, email.text, err = emailer.construct(app, "deletion", "email_", template)
|
email.HTML, email.Text, err = emailer.construct(app, "deletion", "email_", template)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -495,7 +496,7 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub b
|
|||||||
|
|
||||||
func (emailer *Emailer) constructWelcome(username string, app *appContext, noSub bool) (*Email, error) {
|
func (emailer *Emailer) constructWelcome(username string, app *appContext, noSub bool) (*Email, error) {
|
||||||
email := &Email{
|
email := &Email{
|
||||||
subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")),
|
Subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")),
|
||||||
}
|
}
|
||||||
template := map[string]interface{}{
|
template := map[string]interface{}{
|
||||||
"welcome": emailer.lang.WelcomeEmail.get("welcome"),
|
"welcome": emailer.lang.WelcomeEmail.get("welcome"),
|
||||||
@ -523,9 +524,9 @@ func (emailer *Emailer) constructWelcome(username string, app *appContext, noSub
|
|||||||
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
email, err = emailer.constructTemplate(email.subject, content, app)
|
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||||
} else {
|
} else {
|
||||||
email.html, email.text, err = emailer.construct(app, "welcome_email", "email_", template)
|
email.HTML, email.Text, err = emailer.construct(app, "welcome_email", "email_", template)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -109,6 +109,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="modal-customize" class="modal">
|
||||||
|
<div class="modal-content card">
|
||||||
|
<span class="heading">{{ .strings.customizeEmails }} <span class="modal-close">×</span></span>
|
||||||
|
<p class="content">{{ .strings.customizeEmailsDescription }}</p>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ .strings.name }}</th>
|
||||||
|
<th>{{ .strings.reset }}</th>
|
||||||
|
<th>{{ .strings.edit }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="customize-list"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="modal-editor" class="modal">
|
||||||
|
<form class="modal-content wide card" id="form-editor" href="">
|
||||||
|
<span class="heading"><span id="header-editor"></span> <span class="modal-close">×</span></span>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col flex-col content mt-half">
|
||||||
|
<span class="label supra" for="editor-variables">{{ .strings.variables }}</span>
|
||||||
|
<div id="editor-variables"></div>
|
||||||
|
<label class="label supra" for="textarea-editor">{{ .strings.message }}</label>
|
||||||
|
<textarea id="textarea-editor" class="textarea full-width flex-auto ~neutral !normal mt-half monospace"></textarea>
|
||||||
|
<p class="support mt-half mb-1">{{ .strings.markdownSupported }}</p>
|
||||||
|
<div class="flex-row">
|
||||||
|
<label class="full-width ml-half">
|
||||||
|
<input type="submit" class="unfocused">
|
||||||
|
<span class="button ~urge !normal full-width center supra submit">{{ .strings.submit }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col card ~neutral !low">
|
||||||
|
<span class="subheading supra">{{ .strings.preview }}</span>
|
||||||
|
<div class="mt-half" id="editor-preview"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<div id="modal-restart" class="modal">
|
<div id="modal-restart" class="modal">
|
||||||
<div class="modal-content card ~critical !low">
|
<div class="modal-content card ~critical !low">
|
||||||
<span class="heading">{{ .strings.settingsRestartRequired }} <span class="modal-close">×</span></span>
|
<span class="heading">{{ .strings.settingsRestartRequired }} <span class="modal-close">×</span></span>
|
||||||
|
@ -33,6 +33,12 @@
|
|||||||
"announce": "Announce",
|
"announce": "Announce",
|
||||||
"subject": "Email Subject",
|
"subject": "Email Subject",
|
||||||
"message": "Message",
|
"message": "Message",
|
||||||
|
"variables": "Variables",
|
||||||
|
"preview": "Preview",
|
||||||
|
"reset": "Reset",
|
||||||
|
"edit": "Edit",
|
||||||
|
"customizeEmails": "Customize Emails",
|
||||||
|
"customizeEmailsDescription": "If you don't want to use jfa-go's email templates, you can create your own using Markdown.",
|
||||||
"markdownSupported": "Markdown is supported.",
|
"markdownSupported": "Markdown is supported.",
|
||||||
"modifySettings": "Modify Settings",
|
"modifySettings": "Modify Settings",
|
||||||
"modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.",
|
"modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.",
|
||||||
@ -76,6 +82,7 @@
|
|||||||
"userCreated": "User {n} created.",
|
"userCreated": "User {n} created.",
|
||||||
"createProfile": "Created profile {n}.",
|
"createProfile": "Created profile {n}.",
|
||||||
"saveSettings": "Settings were saved",
|
"saveSettings": "Settings were saved",
|
||||||
|
"saveEmail": "Email saved.",
|
||||||
"sentAnnouncement": "Announcement sent.",
|
"sentAnnouncement": "Announcement sent.",
|
||||||
"setOmbiDefaults": "Stored ombi defaults.",
|
"setOmbiDefaults": "Stored ombi defaults.",
|
||||||
"errorConnection": "Couldn't connect to jfa-go.",
|
"errorConnection": "Couldn't connect to jfa-go.",
|
||||||
@ -85,6 +92,7 @@
|
|||||||
"errorSettingsFailed": "Application failed.",
|
"errorSettingsFailed": "Application failed.",
|
||||||
"errorLoginBlank": "The username and/or password were left blank.",
|
"errorLoginBlank": "The username and/or password were left blank.",
|
||||||
"errorUnknown": "Unknown error.",
|
"errorUnknown": "Unknown error.",
|
||||||
|
"errorSaveEmail": "Failed to save email.",
|
||||||
"errorBlankFields": "Fields were left blank",
|
"errorBlankFields": "Fields were left blank",
|
||||||
"errorDeleteProfile": "Failed to delete profile {n}",
|
"errorDeleteProfile": "Failed to delete profile {n}",
|
||||||
"errorLoadProfiles": "Failed to load profiles.",
|
"errorLoadProfiles": "Failed to load profiles.",
|
||||||
|
@ -65,7 +65,11 @@
|
|||||||
"notifyInviteExpiry": "No vencimento",
|
"notifyInviteExpiry": "No vencimento",
|
||||||
"notifyUserCreation": "Na criação do usuário",
|
"notifyUserCreation": "Na criação do usuário",
|
||||||
"settingsRestart": "Reiniciar",
|
"settingsRestart": "Reiniciar",
|
||||||
"settingsRestarting": "Reiniciando…"
|
"settingsRestarting": "Reiniciando…",
|
||||||
|
"announce": "Anunciar",
|
||||||
|
"subject": "Assunto do email",
|
||||||
|
"message": "Mensagem",
|
||||||
|
"markdownSupported": "Suporte a Markdown."
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"changedEmailAddress": "Endereço de e-mail alterado de {n}.",
|
"changedEmailAddress": "Endereço de e-mail alterado de {n}.",
|
||||||
@ -94,7 +98,8 @@
|
|||||||
"errorFailureCheckLogs": "Falha (verificar console/logs)",
|
"errorFailureCheckLogs": "Falha (verificar console/logs)",
|
||||||
"errorPartialFailureCheckLogs": "Falha parcial (verificar console/logs)",
|
"errorPartialFailureCheckLogs": "Falha parcial (verificar console/logs)",
|
||||||
"errorUserCreated": "Falha ao criar o usuário {n}.",
|
"errorUserCreated": "Falha ao criar o usuário {n}.",
|
||||||
"errorSendWelcomeEmail": "Falha ao enviar e-mail de boas-vindas (verifique console/logs)"
|
"errorSendWelcomeEmail": "Falha ao enviar e-mail de boas-vindas (verifique console/logs)",
|
||||||
|
"sentAnnouncement": "Comunicado enviado."
|
||||||
},
|
},
|
||||||
"quantityStrings": {
|
"quantityStrings": {
|
||||||
"modifySettingsFor": {
|
"modifySettingsFor": {
|
||||||
@ -120,6 +125,10 @@
|
|||||||
"appliedSettings": {
|
"appliedSettings": {
|
||||||
"singular": "Configurações aplicada ao usuário {n}.",
|
"singular": "Configurações aplicada ao usuário {n}.",
|
||||||
"plural": "Configurações aplicada ao usuários {n}."
|
"plural": "Configurações aplicada ao usuários {n}."
|
||||||
|
},
|
||||||
|
"announceTo": {
|
||||||
|
"singular": "Comunicar o usuário {n}",
|
||||||
|
"plural": "Comunicar os usuários {n}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,25 +10,29 @@
|
|||||||
"title": "Melding: Gebruiker aangemaakt",
|
"title": "Melding: Gebruiker aangemaakt",
|
||||||
"aUserWasCreated": "Er is een gebruiker aangemaakt door gebruik te maken van code {code}.",
|
"aUserWasCreated": "Er is een gebruiker aangemaakt door gebruik te maken van code {code}.",
|
||||||
"time": "Tijdstip",
|
"time": "Tijdstip",
|
||||||
"notificationNotice": "Opmerking: Meldingse-mails kunnen worden aan- of uitgezet via het beheerdersdashboard."
|
"notificationNotice": "Opmerking: Meldingse-mails kunnen worden aan- of uitgezet via het beheerdersdashboard.",
|
||||||
|
"name": "Gebruiker aangemaakt"
|
||||||
},
|
},
|
||||||
"inviteExpiry": {
|
"inviteExpiry": {
|
||||||
"title": "Melding: Uitnodiging verlopen",
|
"title": "Melding: Uitnodiging verlopen",
|
||||||
"inviteExpired": "Uitnodiging verlopen.",
|
"inviteExpired": "Uitnodiging verlopen.",
|
||||||
"expiredAt": "Code {code} is verlopen op {time}.",
|
"expiredAt": "Code {code} is verlopen op {time}.",
|
||||||
"notificationNotice": "Opmerking: Meldingse-mails kunnen worden aan- of uitgezet via het beheerdersdashboard."
|
"notificationNotice": "Opmerking: Meldingse-mails kunnen worden aan- of uitgezet via het beheerdersdashboard.",
|
||||||
|
"name": "Uitnodiging verlopen"
|
||||||
},
|
},
|
||||||
"passwordReset": {
|
"passwordReset": {
|
||||||
"title": "Wachtwoordreset aangevraagd - Jellyfin",
|
"title": "Wachtwoordreset aangevraagd - Jellyfin",
|
||||||
"someoneHasRequestedReset": "Iemand heeft recentelijk een wachtwoordreset aangevraagd in Jellyfin.",
|
"someoneHasRequestedReset": "Iemand heeft recentelijk een wachtwoordreset aangevraagd in Jellyfin.",
|
||||||
"ifItWasYou": "Als jij dit was, voor dan onderstaande PIN in.",
|
"ifItWasYou": "Als jij dit was, voor dan onderstaande PIN in.",
|
||||||
"codeExpiry": "De code verloopt op {date}, op {time} UTC, dat is over {expiresInMinutes}.",
|
"codeExpiry": "De code verloopt op {date}, op {time} UTC, dat is over {expiresInMinutes}.",
|
||||||
"pin": "PIN"
|
"pin": "PIN",
|
||||||
|
"name": "Wachtwoordreset"
|
||||||
},
|
},
|
||||||
"userDeleted": {
|
"userDeleted": {
|
||||||
"title": "Je account is verwijderd - Jellyfin",
|
"title": "Je account is verwijderd - Jellyfin",
|
||||||
"yourAccountWasDeleted": "Je Jellyfin account is verwijderd.",
|
"yourAccountWasDeleted": "Je Jellyfin account is verwijderd.",
|
||||||
"reason": "Reden"
|
"reason": "Reden",
|
||||||
|
"name": "Gebruiker verwijderd"
|
||||||
},
|
},
|
||||||
"inviteEmail": {
|
"inviteEmail": {
|
||||||
"title": "Uitnodiging - Jellyfin",
|
"title": "Uitnodiging - Jellyfin",
|
||||||
@ -36,17 +40,20 @@
|
|||||||
"youHaveBeenInvited": "Je bent uitgenodigd voor Jellyfin.",
|
"youHaveBeenInvited": "Je bent uitgenodigd voor Jellyfin.",
|
||||||
"toJoin": "Volg onderstaande link om door te gaan.",
|
"toJoin": "Volg onderstaande link om door te gaan.",
|
||||||
"inviteExpiry": "Deze uitnodiging verloopt op {date}, om {time}, dat is over {expiresInMinutes}, dus wees er snel bij.",
|
"inviteExpiry": "Deze uitnodiging verloopt op {date}, om {time}, dat is over {expiresInMinutes}, dus wees er snel bij.",
|
||||||
"linkButton": "Maak account aan"
|
"linkButton": "Maak account aan",
|
||||||
|
"name": "Uitnodigingse-mail"
|
||||||
},
|
},
|
||||||
"welcomeEmail": {
|
"welcomeEmail": {
|
||||||
"title": "Welkom bij Jellyfin",
|
"title": "Welkom bij Jellyfin",
|
||||||
"welcome": "Welkom bij Jellyfin!",
|
"welcome": "Welkom bij Jellyfin!",
|
||||||
"youCanLoginWith": "Je kunt inloggen met onderstaande gegevens",
|
"youCanLoginWith": "Je kunt inloggen met onderstaande gegevens",
|
||||||
"jellyfinURL": "URL"
|
"jellyfinURL": "URL",
|
||||||
|
"name": "Welkomste-mail"
|
||||||
},
|
},
|
||||||
"emailConfirmation": {
|
"emailConfirmation": {
|
||||||
"title": "Bevestig je e-mailadres - Jellyfin",
|
"title": "Bevestig je e-mailadres - Jellyfin",
|
||||||
"clickBelow": "Klik op onderstaande link om je e-mailadres te bevestigen en te beginnen met Jellyfin.",
|
"clickBelow": "Klik op onderstaande link om je e-mailadres te bevestigen en te beginnen met Jellyfin.",
|
||||||
"confirmEmail": "Bevestig e-mailadres"
|
"confirmEmail": "Bevestig e-mailadres",
|
||||||
|
"name": "Bevestingingse-mail"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,25 +10,29 @@
|
|||||||
"title": "Aviso: Usuário criado",
|
"title": "Aviso: Usuário criado",
|
||||||
"aUserWasCreated": "Um usuário foi criado usando o código {code}.",
|
"aUserWasCreated": "Um usuário foi criado usando o código {code}.",
|
||||||
"time": "Tempo",
|
"time": "Tempo",
|
||||||
"notificationNotice": "Nota: Os emails de notificação podem ser alternados no painel do administrador."
|
"notificationNotice": "Nota: Os emails de notificação podem ser alternados no painel do administrador.",
|
||||||
|
"name": "Criação de usuário"
|
||||||
},
|
},
|
||||||
"inviteExpiry": {
|
"inviteExpiry": {
|
||||||
"title": "Aviso: Convite expirado",
|
"title": "Aviso: Convite expirado",
|
||||||
"inviteExpired": "Convite expirado.",
|
"inviteExpired": "Convite expirado.",
|
||||||
"expiredAt": "O código {code} expirou em {time}.",
|
"expiredAt": "O código {code} expirou em {time}.",
|
||||||
"notificationNotice": "Nota: Os emails de notificação podem ser alternados no painel do administrador."
|
"notificationNotice": "Nota: Os emails de notificação podem ser alternados no painel do administrador.",
|
||||||
|
"name": "Convite Expirado"
|
||||||
},
|
},
|
||||||
"passwordReset": {
|
"passwordReset": {
|
||||||
"title": "Redefinir senha foi solicitada - Jellyfin",
|
"title": "Redefinir senha foi solicitada - Jellyfin",
|
||||||
"someoneHasRequestedReset": "Alguém recentemente solicitou uma redefinição de senha no Jellyfin.",
|
"someoneHasRequestedReset": "Alguém recentemente solicitou uma redefinição de senha no Jellyfin.",
|
||||||
"ifItWasYou": "Se foi você, insira o PIN abaixo.",
|
"ifItWasYou": "Se foi você, insira o PIN abaixo.",
|
||||||
"codeExpiry": "O código irá expirar em {date}, ás {time}, que está em {expiresInMinutes}.",
|
"codeExpiry": "O código irá expirar em {date}, ás {time}, que está em {expiresInMinutes}.",
|
||||||
"pin": "PIN"
|
"pin": "PIN",
|
||||||
|
"name": "Redefinir senha"
|
||||||
},
|
},
|
||||||
"userDeleted": {
|
"userDeleted": {
|
||||||
"title": "Sua conta foi excluída - Jellyfin",
|
"title": "Sua conta foi excluída - Jellyfin",
|
||||||
"yourAccountWasDeleted": "Sua conta Jellyfin foi excluída.",
|
"yourAccountWasDeleted": "Sua conta Jellyfin foi excluída.",
|
||||||
"reason": "Razão"
|
"reason": "Razão",
|
||||||
|
"name": "Exclusão do usuário"
|
||||||
},
|
},
|
||||||
"inviteEmail": {
|
"inviteEmail": {
|
||||||
"title": "Convite - Jellyfin",
|
"title": "Convite - Jellyfin",
|
||||||
@ -36,17 +40,20 @@
|
|||||||
"youHaveBeenInvited": "Você recebeu um convite para o Jellyfin.",
|
"youHaveBeenInvited": "Você recebeu um convite para o Jellyfin.",
|
||||||
"toJoin": "Para participar, clique no link abaixo.",
|
"toJoin": "Para participar, clique no link abaixo.",
|
||||||
"inviteExpiry": "Este convite expira em {date} às {time}, que é em {expiresInMinutes}, então seja rápido.",
|
"inviteExpiry": "Este convite expira em {date} às {time}, que é em {expiresInMinutes}, então seja rápido.",
|
||||||
"linkButton": "Crie sua conta"
|
"linkButton": "Crie sua conta",
|
||||||
|
"name": "Convide por email"
|
||||||
},
|
},
|
||||||
"welcomeEmail": {
|
"welcomeEmail": {
|
||||||
"title": "Bem vindo ao Jellyfin",
|
"title": "Bem vindo ao Jellyfin",
|
||||||
"welcome": "Bem vindo ao Jellyfin!",
|
"welcome": "Bem vindo ao Jellyfin!",
|
||||||
"youCanLoginWith": "Você pode fazer o login com os detalhes abaixo",
|
"youCanLoginWith": "Abaixo está os detalhes para fazer o login",
|
||||||
"jellyfinURL": "URL"
|
"jellyfinURL": "URL",
|
||||||
|
"name": "Email de Boas vindas"
|
||||||
},
|
},
|
||||||
"emailConfirmation": {
|
"emailConfirmation": {
|
||||||
"title": "Confirme seu email - Jellyfin",
|
"title": "Confirme seu email - Jellyfin",
|
||||||
"clickBelow": "Clique no link abaixo para confirmar seu endereço de e-mail e começar a usar o Jellyfin.",
|
"clickBelow": "Clique no link abaixo para confirmar seu endereço de e-mail e começar a usar o Jellyfin.",
|
||||||
"confirmEmail": "Confirmar Email"
|
"confirmEmail": "Confirmar Email",
|
||||||
|
"name": "Email de Confirmação"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
"successHeader": "Sucesso!",
|
"successHeader": "Sucesso!",
|
||||||
"successContinueButton": "Continuar",
|
"successContinueButton": "Continuar",
|
||||||
"confirmationRequired": "Necessária confirmação de e-mail",
|
"confirmationRequired": "Necessária confirmação de e-mail",
|
||||||
"confirmationRequiredMessage": "Verifique sua caixa de entrada no e-mail para verificar seu endereço."
|
"confirmationRequiredMessage": "Verifique sua caixa de email para finalizar o cadastro."
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorUserExists": "Esse usuário já existe.",
|
"errorUserExists": "Esse usuário já existe.",
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
{{ .aUserWasCreated }}
|
{{ .aUserWasCreated }}
|
||||||
|
|
||||||
{{ .nameString }}: {{ .name }}
|
{{ .nameString }}: {{ .name }}
|
||||||
|
|
||||||
{{ .addressString }}: {{ .address }}
|
{{ .addressString }}: {{ .address }}
|
||||||
|
|
||||||
{{ .timeString }}: {{ .time }}
|
{{ .timeString }}: {{ .time }}
|
||||||
|
|
||||||
{{ .notificationNotice }}
|
{{ .notificationNotice }}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{{ .yourAccountWasDeleted }}
|
{{ .yourAccountWasDeleted }}
|
||||||
|
|
||||||
{{ .reasonString }}: {{ .reason }}
|
{{ .reasonString }}: {{ .reason }}
|
||||||
|
|
||||||
{{ .message }}
|
{{ .message }}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
{{ .helloUser }}
|
{{ .helloUser }}
|
||||||
|
|
||||||
{{ .someoneHasRequestedReset }}
|
{{ .someoneHasRequestedReset }}
|
||||||
|
|
||||||
{{ .ifItWasYou }}
|
{{ .ifItWasYou }}
|
||||||
|
|
||||||
{{ .codeExpiry }}
|
{{ .codeExpiry }}
|
||||||
|
|
||||||
{{ .ifItWasNotYou }}
|
{{ .ifItWasNotYou }}
|
||||||
|
|
||||||
{{ .pinString }}: {{ .pin }}
|
{{ .pinString }}: {{ .pin }}
|
||||||
|
11
models.go
11
models.go
@ -175,8 +175,17 @@ type settings struct {
|
|||||||
|
|
||||||
type langDTO map[string]string
|
type langDTO map[string]string
|
||||||
|
|
||||||
type emailListDTO map[string]string
|
type emailListDTO map[string]emailListEl
|
||||||
|
|
||||||
|
type emailListEl struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
type emailSetDTO struct {
|
type emailSetDTO struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type emailTestDTO struct {
|
||||||
|
Address string `json:"address"`
|
||||||
|
}
|
||||||
|
@ -142,7 +142,8 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
api.GET(p+"/config/emails", app.GetEmails)
|
api.GET(p+"/config/emails", app.GetEmails)
|
||||||
api.GET(p+"/config/emails/:id", app.GetEmail)
|
api.GET(p+"/config/emails/:id", app.GetEmail)
|
||||||
api.POST(p+"/config/emails/:id", app.SetEmail)
|
api.POST(p+"/config/emails/:id", app.SetEmail)
|
||||||
api.POST(p+"/config/emails/:id/:state", app.SetEmailState)
|
api.POST(p+"/config/emails/:id/test", app.GetTestEmail)
|
||||||
|
api.POST(p+"/config/emails/:id/state/:state", app.SetEmailState)
|
||||||
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)
|
||||||
|
@ -44,7 +44,9 @@ func stripMarkdown(md string) string {
|
|||||||
}
|
}
|
||||||
fullLinks := make([]string, len(openSquares))
|
fullLinks := make([]string, len(openSquares))
|
||||||
for i := range openSquares {
|
for i := range openSquares {
|
||||||
fullLinks[i] = md[openSquares[i] : closeBrackets[i]+1]
|
if openSquares[i] != -1 && closeBrackets[i] != -1 {
|
||||||
|
fullLinks[i] = md[openSquares[i] : closeBrackets[i]+1]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for i, _ := range openSquares {
|
for i, _ := range openSquares {
|
||||||
md = strings.Replace(md, fullLinks[i], links[i], 1)
|
md = strings.Replace(md, fullLinks[i], links[i], 1)
|
||||||
|
@ -53,6 +53,10 @@ window.availableProfiles = window.availableProfiles || [];
|
|||||||
window.modals.addProfile = new Modal(document.getElementById("modal-add-profile"));
|
window.modals.addProfile = new Modal(document.getElementById("modal-add-profile"));
|
||||||
|
|
||||||
window.modals.announce = new Modal(document.getElementById("modal-announce"));
|
window.modals.announce = new Modal(document.getElementById("modal-announce"));
|
||||||
|
|
||||||
|
window.modals.editor = new Modal(document.getElementById("modal-editor"));
|
||||||
|
|
||||||
|
window.modals.customizeEmails = new Modal(document.getElementById("modal-customize"));
|
||||||
})();
|
})();
|
||||||
|
|
||||||
var inviteCreator = new createInvite();
|
var inviteCreator = new createInvite();
|
||||||
|
@ -479,14 +479,16 @@ 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();
|
||||||
|
|
||||||
addSection = (name: string, s: Section) => {
|
addSection = (name: string, s: Section, subButton?: HTMLElement) => {
|
||||||
const section = new sectionPanel(s, name);
|
const section = new sectionPanel(s, name);
|
||||||
this._sections[name] = section;
|
this._sections[name] = section;
|
||||||
this._panel.appendChild(this._sections[name].asElement());
|
this._panel.appendChild(this._sections[name].asElement());
|
||||||
const button = document.createElement("span") as HTMLSpanElement;
|
const button = document.createElement("span") as HTMLSpanElement;
|
||||||
button.classList.add("button", "~neutral", "!low", "settings-section-button", "mb-half");
|
button.classList.add("button", "~neutral", "!low", "settings-section-button", "mb-half");
|
||||||
button.textContent = s.meta.name;
|
button.textContent = s.meta.name;
|
||||||
|
if (subButton) { button.appendChild(subButton); }
|
||||||
button.onclick = () => { this._showPanel(name); };
|
button.onclick = () => { this._showPanel(name); };
|
||||||
if (s.meta.depends_true || s.meta.depends_false) {
|
if (s.meta.depends_true || s.meta.depends_false) {
|
||||||
let dependant = splitDependant(name, s.meta.depends_true || s.meta.depends_false);
|
let dependant = splitDependant(name, s.meta.depends_true || s.meta.depends_false);
|
||||||
@ -581,7 +583,22 @@ 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 {
|
||||||
this.addSection(name, settings.sections[name]);
|
if (name == "email") {
|
||||||
|
const editButton = document.createElement("div");
|
||||||
|
editButton.classList.add("tooltip", "left");
|
||||||
|
editButton.innerHTML = `
|
||||||
|
<span class="button ~neutral !normal">
|
||||||
|
<i class="icon ri-edit-line"></i>
|
||||||
|
</span>
|
||||||
|
<span class="content sm">
|
||||||
|
${window.lang.get("strings", "customizeEmails")}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
(editButton.querySelector("span.button") as HTMLSpanElement).onclick = this._emailEditor.showList;
|
||||||
|
this.addSection(name, settings.sections[name], editButton);
|
||||||
|
} else {
|
||||||
|
this.addSection(name, settings.sections[name]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._showPanel(settings.order[0]);
|
this._showPanel(settings.order[0]);
|
||||||
@ -653,3 +670,168 @@ class ombiDefaults {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Email {
|
||||||
|
subject: string;
|
||||||
|
html: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface templateEmail {
|
||||||
|
content: string;
|
||||||
|
variables: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface emailListEl {
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmailEditor {
|
||||||
|
private _currentID: string;
|
||||||
|
private _names: { [id: string]: emailListEl };
|
||||||
|
private _content: string;
|
||||||
|
private _form = document.getElementById("form-editor") as HTMLFormElement;
|
||||||
|
private _header = document.getElementById("header-editor") as HTMLSpanElement;
|
||||||
|
private _variables = document.getElementById("editor-variables") as HTMLDivElement;
|
||||||
|
private _textArea = document.getElementById("textarea-editor") as HTMLTextAreaElement;
|
||||||
|
private _preview = document.getElementById("editor-preview") as HTMLDivElement;
|
||||||
|
private _timeout: number;
|
||||||
|
private _finishInterval = 1000;
|
||||||
|
|
||||||
|
insert = (textarea: HTMLTextAreaElement, text: string) => { // https://kubyshkin.name/posts/insert-text-into-textarea-at-cursor-position <3
|
||||||
|
const isSuccess = document.execCommand("insertText", false, text);
|
||||||
|
|
||||||
|
// Firefox (non-standard method)
|
||||||
|
if (!isSuccess && typeof textarea.setRangeText === "function") {
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
textarea.setRangeText(text);
|
||||||
|
// update cursor to be at the end of insertion
|
||||||
|
textarea.selectionStart = textarea.selectionEnd = start + text.length;
|
||||||
|
|
||||||
|
// Notify any possible listeners of the change
|
||||||
|
const e = document.createEvent("UIEvent");
|
||||||
|
e.initEvent("input", true, false);
|
||||||
|
textarea.dispatchEvent(e);
|
||||||
|
textarea.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadEditor = (id: string) => {
|
||||||
|
this._currentID = id;
|
||||||
|
_get("/config/emails/" + id, null, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
if (req.status != 200) {
|
||||||
|
window.notifications.customError("loadTemplateError", window.lang.notif("errorFailureCheckLogs"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._names[id] !== undefined) {
|
||||||
|
this._header.textContent = this._names[id].name;
|
||||||
|
}
|
||||||
|
const templ = req.response as templateEmail;
|
||||||
|
this._textArea.value = templ.content;
|
||||||
|
this.loadPreview();
|
||||||
|
this._content = templ.content;
|
||||||
|
const colors = ["info", "urge", "positive", "neutral"];
|
||||||
|
let innerHTML = '';
|
||||||
|
for (let i = 0; i < templ.variables.length; i++) {
|
||||||
|
let ci = i % colors.length;
|
||||||
|
innerHTML += '<span class="button ~' + colors[ci] +' !normal mb-1" style="margin-left: 0.25rem; margin-right: 0.25rem;"></span>'
|
||||||
|
}
|
||||||
|
this._variables.innerHTML = innerHTML
|
||||||
|
const buttons = this._variables.querySelectorAll("span.button") as NodeListOf<HTMLSpanElement>;
|
||||||
|
for (let i = 0; i < templ.variables.length; i++) {
|
||||||
|
buttons[i].innerHTML = `<span class="monospace">` + templ.variables[i] + `</span>`;
|
||||||
|
buttons[i].onclick = () => {
|
||||||
|
this.insert(this._textArea, templ.variables[i]);
|
||||||
|
this._timeout = setTimeout(this.loadPreview, this._finishInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.modals.editor.show();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
loadPreview = () => {
|
||||||
|
_post("/config/emails/" + this._currentID + "/test", { "content": this._textArea.value }, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
if (req.status != 200) {
|
||||||
|
window.notifications.customError("loadTemplateError", window.lang.notif("errorFailureCheckLogs"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._preview.innerHTML = (req.response as Email).html;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
showList = () => {
|
||||||
|
_get("/config/emails", null, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
if (req.status != 200) {
|
||||||
|
window.notifications.customError("loadTemplateError", window.lang.notif("errorFailureCheckLogs"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._names = req.response;
|
||||||
|
const list = document.getElementById("customize-list") as HTMLDivElement;
|
||||||
|
list.textContent = '';
|
||||||
|
for (let id in this._names) {
|
||||||
|
const tr = document.createElement("tr") as HTMLTableRowElement;
|
||||||
|
let resetButton = ``;
|
||||||
|
if (this._names[id].enabled) {
|
||||||
|
resetButton = `<i class="icon ri-restart-line" title="${window.lang.get("strings", "reset")}"></i>`;
|
||||||
|
}
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${this._names[id].name}</td>
|
||||||
|
<td>${resetButton}</td>
|
||||||
|
<td><span class="button ~info !normal" title="${window.lang.get("strings", "edit")}"><i class="icon ri-edit-line"></i></span></td>
|
||||||
|
`;
|
||||||
|
(tr.querySelector("span.button") as HTMLSpanElement).onclick = () => {
|
||||||
|
window.modals.customizeEmails.close()
|
||||||
|
this.loadEditor(id);
|
||||||
|
};
|
||||||
|
if (this._names[id].enabled) {
|
||||||
|
const rb = tr.querySelector("i.ri-restart-line") as HTMLElement;
|
||||||
|
rb.onclick = () => _post("/config/emails/" + id + "/state/disable", null, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
if (req.status != 200 && req.status != 204) {
|
||||||
|
window.notifications.customError("setEmailStateError", window.lang.notif("errorFailureCheckLogs"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rb.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
list.appendChild(tr);
|
||||||
|
}
|
||||||
|
window.modals.customizeEmails.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._textArea.onkeyup = () => {
|
||||||
|
clearTimeout(this._timeout);
|
||||||
|
this._timeout = setTimeout(this.loadPreview, this._finishInterval);
|
||||||
|
};
|
||||||
|
this._textArea.onkeydown = () => {
|
||||||
|
clearTimeout(this._timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
this._form.onsubmit = (event: Event) => {
|
||||||
|
if (this._textArea.value == this._content) {
|
||||||
|
window.modals.editor.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
_post("/config/emails/" + this._currentID, { "content": this._textArea.value }, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
window.modals.editor.close();
|
||||||
|
if (req.status != 200) {
|
||||||
|
window.notifications.customError("saveEmailError", window.lang.notif("errorSaveEmail"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.notifications.customSuccess("saveEmail", window.lang.notif("saveEmail"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -75,6 +75,8 @@ declare interface Modals {
|
|||||||
profiles: Modal;
|
profiles: Modal;
|
||||||
addProfile: Modal;
|
addProfile: Modal;
|
||||||
announce: Modal;
|
announce: Modal;
|
||||||
|
editor: Modal;
|
||||||
|
customizeEmails: Modal;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Invite {
|
interface Invite {
|
||||||
|
Loading…
Reference in New Issue
Block a user