mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-29 12:30:11 +00:00
Compare commits
6 Commits
cdc837e781
...
ce30537ebd
Author | SHA1 | Date | |
---|---|---|---|
|
ce30537ebd | ||
93b5b483cc | |||
27ef931670 | |||
fb727e75ec | |||
fa433c88a8 | |||
adbb5b9d38 |
56
api.go
56
api.go
@ -130,7 +130,7 @@ func (app *appContext) checkInvites() {
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct expiry notification", code)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
} else if err := app.email.send(addr, msg); err != nil {
|
||||
} else if err := app.email.send(msg, addr); err != nil {
|
||||
app.err.Printf("%s: Failed to send expiry notification", code)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
} else {
|
||||
@ -169,7 +169,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct expiry notification", code)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
} else if err := app.email.send(address, msg); err != nil {
|
||||
} else if err := app.email.send(msg, address); err != nil {
|
||||
app.err.Printf("%s: Failed to send expiry notification", code)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
} else {
|
||||
@ -308,7 +308,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
|
||||
app.err.Printf("%s: Failed to construct welcome email: %s", req.Username, err)
|
||||
respondUser(500, true, false, err.Error(), gc)
|
||||
return
|
||||
} else if err := app.email.send(req.Email, msg); err != nil {
|
||||
} else if err := app.email.send(msg, req.Email); err != nil {
|
||||
app.err.Printf("%s: Failed to send welcome email: %s", req.Username, err)
|
||||
respondUser(500, true, false, err.Error(), gc)
|
||||
return
|
||||
@ -363,7 +363,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct confirmation email", req.Code)
|
||||
app.debug.Printf("%s: Error: %s", req.Code, err)
|
||||
} else if err := app.email.send(req.Email, msg); err != nil {
|
||||
} else if err := app.email.send(msg, req.Email); err != nil {
|
||||
app.err.Printf("%s: Failed to send user confirmation email: %s", req.Code, err)
|
||||
} else {
|
||||
app.info.Printf("%s: Sent user confirmation email to %s", req.Code, req.Email)
|
||||
@ -393,7 +393,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct user creation notification", req.Code)
|
||||
app.debug.Printf("%s: Error: %s", req.Code, err)
|
||||
} else if err := app.email.send(address, msg); err != nil {
|
||||
} else if err := app.email.send(msg, address); err != nil {
|
||||
app.err.Printf("%s: Failed to send user creation notification", req.Code)
|
||||
app.debug.Printf("%s: Error: %s", req.Code, err)
|
||||
} else {
|
||||
@ -455,7 +455,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
msg, err := app.email.constructWelcome(req.Username, app)
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct welcome email: %s", req.Username, err)
|
||||
} else if err := app.email.send(req.Email, msg); err != nil {
|
||||
} else if err := app.email.send(msg, req.Email); err != nil {
|
||||
app.err.Printf("%s: Failed to send welcome email: %s", req.Username, err)
|
||||
} else {
|
||||
app.info.Printf("%s: Sent welcome email to %s", req.Username, req.Email)
|
||||
@ -508,6 +508,46 @@ func (app *appContext) NewUser(gc *gin.Context) {
|
||||
gc.JSON(code, validation)
|
||||
}
|
||||
|
||||
// @Summary Send an announcement via email to a given list of users.
|
||||
// @Produce json
|
||||
// @Param announcementDTO body announcementDTO true "Announcement request object"
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Router /users/announce [post]
|
||||
// @Security Bearer
|
||||
// @tags Users
|
||||
func (app *appContext) Announce(gc *gin.Context) {
|
||||
var req announcementDTO
|
||||
gc.BindJSON(&req)
|
||||
if !emailEnabled {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
addresses := []string{}
|
||||
for _, userID := range req.Users {
|
||||
addr, ok := app.storage.emails[userID]
|
||||
if !ok || addr == "" {
|
||||
continue
|
||||
}
|
||||
addresses = append(addresses, addr.(string))
|
||||
}
|
||||
msg, err := app.email.constructAnnouncement(req.Subject, req.Message, app)
|
||||
if err != nil {
|
||||
app.err.Println("Failed to construct announcement email")
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
} else if err := app.email.send(msg, addresses...); err != nil {
|
||||
app.err.Println("Failed to send announcement email")
|
||||
app.debug.Printf("Error: %s", err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
app.info.Println("Sent announcement email")
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Delete a list of users, optionally notifying them why.
|
||||
// @Produce json
|
||||
// @Param deleteUserDTO body deleteUserDTO true "User deletion request object"
|
||||
@ -552,7 +592,7 @@ func (app *appContext) DeleteUser(gc *gin.Context) {
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct account deletion email", userID)
|
||||
app.debug.Printf("%s: Error: %s", userID, err)
|
||||
} else if err := app.email.send(address, msg); err != nil {
|
||||
} else if err := app.email.send(msg, address); err != nil {
|
||||
app.err.Printf("%s: Failed to send to %s", userID, address)
|
||||
app.debug.Printf("%s: Error: %s", userID, err)
|
||||
} else {
|
||||
@ -619,7 +659,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
|
||||
app.err.Printf("%s: Failed to construct invite email", inviteCode)
|
||||
app.debug.Printf("%s: Error: %s", inviteCode, err)
|
||||
} else if err := app.email.send(req.Email, msg); err != nil {
|
||||
} else if err := app.email.send(msg, req.Email); err != nil {
|
||||
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
|
||||
app.err.Printf("%s: %s", inviteCode, invite.Email)
|
||||
app.debug.Printf("%s: Error: %s", inviteCode, err)
|
||||
|
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -17,7 +18,8 @@ func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
|
||||
if strings.HasPrefix(val, "jfa-go:") {
|
||||
return localFS, strings.TrimPrefix(val, "jfa-go:")
|
||||
}
|
||||
return app.systemFS, strings.TrimPrefix(val, "/")
|
||||
dir, file := filepath.Split(val)
|
||||
return os.DirFS(dir), file
|
||||
}
|
||||
|
||||
func (app *appContext) loadConfig() error {
|
||||
@ -61,6 +63,9 @@ func (app *appContext) loadConfig() error {
|
||||
app.config.Section("welcome_email").Key("email_html").SetValue(app.config.Section("welcome_email").Key("email_html").MustString("jfa-go:" + "welcome.html"))
|
||||
app.config.Section("welcome_email").Key("email_text").SetValue(app.config.Section("welcome_email").Key("email_text").MustString("jfa-go:" + "welcome.txt"))
|
||||
|
||||
app.config.Section("announcement_email").Key("email_html").SetValue(app.config.Section("announcement_email").Key("email_html").MustString("jfa-go:" + "announcement.html"))
|
||||
app.config.Section("announcement_email").Key("email_text").SetValue(app.config.Section("announcement_email").Key("email_text").MustString("jfa-go:" + "announcement.txt"))
|
||||
|
||||
app.config.Section("jellyfin").Key("version").SetValue(VERSION)
|
||||
app.config.Section("jellyfin").Key("device").SetValue("jfa-go")
|
||||
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", VERSION, COMMIT))
|
||||
|
@ -73,7 +73,7 @@
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Optionally substitute occurrences of \"Jellyfin\" in the account creation form with this. May result in bad grammar."
|
||||
"description": "Optionally substitute occurrences of \"Jellyfin\" in the account creation form and emails with this. May result in bad grammar."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -398,6 +398,15 @@
|
||||
"type": "text",
|
||||
"value": "Jellyfin",
|
||||
"description": "The name of the sender"
|
||||
},
|
||||
"plaintext": {
|
||||
"name": "Send emails as plain text",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Send emails as plain text instead of HTML."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -164,6 +164,7 @@ div.card:contains(section.banner.footer) {
|
||||
|
||||
.monospace {
|
||||
background-color: inherit; /* so we can use a17t code blocks */
|
||||
font-family: Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;
|
||||
}
|
||||
|
||||
sup.\~critical, .text-critical {
|
||||
|
95
email.go
95
email.go
@ -6,18 +6,24 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"sync"
|
||||
textTemplate "text/template"
|
||||
"time"
|
||||
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
jEmail "github.com/jordan-wright/email"
|
||||
"github.com/knz/strtime"
|
||||
"github.com/mailgun/mailgun-go/v4"
|
||||
stripmd "github.com/writeas/go-strip-markdown"
|
||||
)
|
||||
|
||||
// implements email sending, right now via smtp or mailgun.
|
||||
type emailClient interface {
|
||||
send(address, fromName, fromAddr string, email *Email) error
|
||||
send(fromName, fromAddr string, email *Email, address ...string) error
|
||||
}
|
||||
|
||||
// Mailgun client implements emailClient.
|
||||
@ -25,13 +31,16 @@ type Mailgun struct {
|
||||
client *mailgun.MailgunImpl
|
||||
}
|
||||
|
||||
func (mg *Mailgun) send(address, fromName, fromAddr string, email *Email) error {
|
||||
func (mg *Mailgun) send(fromName, fromAddr string, email *Email, address ...string) error {
|
||||
message := mg.client.NewMessage(
|
||||
fmt.Sprintf("%s <%s>", fromName, fromAddr),
|
||||
email.subject,
|
||||
email.text,
|
||||
address,
|
||||
)
|
||||
for _, a := range address {
|
||||
// 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.SetHtml(email.html)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
@ -47,25 +56,33 @@ type SMTP struct {
|
||||
auth smtp.Auth
|
||||
}
|
||||
|
||||
func (sm *SMTP) send(address, fromName, fromAddr string, email *Email) error {
|
||||
e := jEmail.NewEmail()
|
||||
e.Subject = email.subject
|
||||
e.From = fmt.Sprintf("%s <%s>", fromName, fromAddr)
|
||||
e.To = []string{address}
|
||||
e.Text = []byte(email.text)
|
||||
e.HTML = []byte(email.html)
|
||||
func (sm *SMTP) send(fromName, fromAddr string, email *Email, address ...string) error {
|
||||
server := fmt.Sprintf("%s:%d", sm.server, sm.port)
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
ServerName: sm.server,
|
||||
}
|
||||
from := fmt.Sprintf("%s <%s>", fromName, fromAddr)
|
||||
var wg sync.WaitGroup
|
||||
var err error
|
||||
// err = e.Send(server, sm.auth)
|
||||
if sm.sslTLS {
|
||||
err = e.SendWithTLS(server, sm.auth, tlsConfig)
|
||||
} else {
|
||||
err = e.SendWithStartTLS(server, sm.auth, tlsConfig)
|
||||
for _, addr := range address {
|
||||
wg.Add(1)
|
||||
go func(addr string) {
|
||||
defer wg.Done()
|
||||
e := jEmail.NewEmail()
|
||||
e.Subject = email.subject
|
||||
e.From = from
|
||||
e.Text = []byte(email.text)
|
||||
e.HTML = []byte(email.html)
|
||||
e.To = []string{addr}
|
||||
if sm.sslTLS {
|
||||
err = e.SendWithTLS(server, sm.auth, tlsConfig)
|
||||
} else {
|
||||
err = e.SendWithStartTLS(server, sm.auth, tlsConfig)
|
||||
}
|
||||
}(addr)
|
||||
}
|
||||
wg.Wait()
|
||||
return err
|
||||
}
|
||||
|
||||
@ -153,11 +170,31 @@ func (emailer *Emailer) NewSMTP(server string, port int, username, password stri
|
||||
}
|
||||
}
|
||||
|
||||
type templ interface {
|
||||
Execute(wr io.Writer, data interface{}) error
|
||||
}
|
||||
|
||||
func (emailer *Emailer) construct(app *appContext, section, keyFragment string, data map[string]interface{}) (html, text string, err error) {
|
||||
var tpl *template.Template
|
||||
for _, key := range []string{"html", "text"} {
|
||||
var tpl templ
|
||||
if substituteStrings == "" {
|
||||
data["jellyfin"] = "Jellyfin"
|
||||
} else {
|
||||
data["jellyfin"] = substituteStrings
|
||||
}
|
||||
var keys []string
|
||||
if app.config.Section("email").Key("plaintext").MustBool(false) {
|
||||
keys = []string{"text"}
|
||||
text = ""
|
||||
} else {
|
||||
keys = []string{"html", "text"}
|
||||
}
|
||||
for _, key := range keys {
|
||||
filesystem, fpath := app.GetPath(section, keyFragment+key)
|
||||
tpl, err = template.ParseFS(filesystem, fpath)
|
||||
if key == "html" {
|
||||
tpl, err = template.ParseFS(filesystem, fpath)
|
||||
} else {
|
||||
tpl, err = textTemplate.ParseFS(filesystem, fpath)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -197,6 +234,24 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a
|
||||
return email, nil
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructAnnouncement(subject, md string, app *appContext) (*Email, error) {
|
||||
email := &Email{subject: subject}
|
||||
renderer := html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
|
||||
html := markdown.ToHTML([]byte(md), nil, renderer)
|
||||
text := strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(md), "</p>"), "<p>")
|
||||
message := app.config.Section("email").Key("message").String()
|
||||
var err error
|
||||
email.html, email.text, err = emailer.construct(app, "announcement_email", "email_", map[string]interface{}{
|
||||
"text": template.HTML(html),
|
||||
"plaintext": text,
|
||||
"message": message,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return email, nil
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext) (*Email, error) {
|
||||
email := &Email{
|
||||
subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.InviteEmail.get("title")),
|
||||
@ -327,6 +382,6 @@ func (emailer *Emailer) constructWelcome(username string, app *appContext) (*Ema
|
||||
}
|
||||
|
||||
// calls the send method in the underlying emailClient.
|
||||
func (emailer *Emailer) send(address string, email *Email) error {
|
||||
return emailer.sender.send(address, emailer.fromName, emailer.fromAddr, email)
|
||||
func (emailer *Emailer) send(email *Email, address ...string) error {
|
||||
return emailer.sender.send(emailer.fromName, emailer.fromAddr, email, address...)
|
||||
}
|
||||
|
@ -5,12 +5,25 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var localFS fs.FS
|
||||
var langFS fs.FS
|
||||
|
||||
func FSJoin(elem ...string) string { return filepath.Join(elem...) }
|
||||
// When using os.DirFS, even on Windows the separator seems to be '/'.
|
||||
// func FSJoin(elem ...string) string { return filepath.Join(elem...) }
|
||||
func FSJoin(elem ...string) string {
|
||||
sep := "/"
|
||||
if strings.Contains(elem[0], "\\") {
|
||||
sep = "\\"
|
||||
}
|
||||
path := ""
|
||||
for _, el := range elem {
|
||||
path += el + sep
|
||||
}
|
||||
return strings.TrimSuffix(path, sep)
|
||||
}
|
||||
|
||||
func loadFilesystems() {
|
||||
log.Println("Using external storage")
|
||||
|
2
go.mod
2
go.mod
@ -20,6 +20,7 @@ require (
|
||||
github.com/go-openapi/spec v0.20.3 // indirect
|
||||
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
||||
github.com/golang/protobuf v1.4.3 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8 // indirect
|
||||
github.com/google/uuid v1.1.2 // indirect
|
||||
github.com/hrfee/jfa-go/common v0.0.0-20210105184019-fdc97b4e86cc
|
||||
github.com/hrfee/jfa-go/docs v0.0.0-20201112212552-b6f3cd7c1f71
|
||||
@ -37,6 +38,7 @@ require (
|
||||
github.com/swaggo/gin-swagger v1.3.0
|
||||
github.com/swaggo/swag v1.7.0 // indirect
|
||||
github.com/ugorji/go v1.2.0 // indirect
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible // indirect
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
|
||||
golang.org/x/tools v0.1.0 // indirect
|
||||
|
6
go.sum
6
go.sum
@ -109,6 +109,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8 h1:nWU6p08f1VgIalT6iZyqXi4o5cZsz4X6qa87nusfcsc=
|
||||
github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@ -224,8 +226,12 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
|
||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw=
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE=
|
||||
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead h1:jeP6FgaSLNTMP+Yri3qjlACywQLye+huGLmNGhBzm6k=
|
||||
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
@ -93,6 +93,22 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-announce" class="modal">
|
||||
<form class="modal-content card" id="form-announce" href="">
|
||||
<span class="heading"><span id="header-announce"></span> <span class="modal-close">×</span></span>
|
||||
<div class="content mt-half">
|
||||
<label class="label supra" for="announce-subject"> {{ .strings.subject }}</label>
|
||||
<input type="text" id="announce-subject" class="input ~neutral !normal mb-1 mt-half">
|
||||
<label class="label supra" for="textarea-announce">{{ .strings.message }}</label>
|
||||
<textarea id="textarea-announce" class="textarea full-width ~neutral !normal mt-half monospace"></textarea>
|
||||
<p class="support mt-half mb-1">{{ .strings.markdownSupported }}</p>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">{{ .strings.submit }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-restart" class="modal">
|
||||
<div class="modal-content card ~critical !low">
|
||||
<span class="heading">{{ .strings.settingsRestartRequired }} <span class="modal-close">×</span></span>
|
||||
@ -256,6 +272,7 @@
|
||||
<span class="heading">{{ .strings.accounts }}</span>
|
||||
<div class="fr">
|
||||
<span class="button ~neutral !normal" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
|
||||
<span class="button ~info !normal" id="accounts-announce">{{ .strings.announce }}</span>
|
||||
<span class="button ~urge !normal" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
|
||||
<span class="button ~critical !normal" id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
|
||||
</div>
|
||||
|
@ -30,6 +30,10 @@
|
||||
"profile": "Profile",
|
||||
"unknown": "Unknown",
|
||||
"label": "Label",
|
||||
"announce": "Announce",
|
||||
"subject": "Email Subject",
|
||||
"message": "Message",
|
||||
"markdownSupported": "Markdown is supported.",
|
||||
"modifySettings": "Modify Settings",
|
||||
"modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.",
|
||||
"applyHomescreenLayout": "Apply homescreen layout",
|
||||
@ -72,6 +76,7 @@
|
||||
"userCreated": "User {n} created.",
|
||||
"createProfile": "Created profile {n}.",
|
||||
"saveSettings": "Settings were saved",
|
||||
"sentAnnouncement": "Announcement sent.",
|
||||
"setOmbiDefaults": "Stored ombi defaults.",
|
||||
"errorConnection": "Couldn't connect to jfa-go.",
|
||||
"error401Unauthorized": "Unauthorized. Try refreshing the page.",
|
||||
@ -117,6 +122,10 @@
|
||||
"singular": "Deleted {n} user.",
|
||||
"plural": "Deleted {n} users."
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "Announce to {n} user",
|
||||
"plural": "Announce to {n} users"
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "Applied settings to {n} user.",
|
||||
"plural": "Applied settings to {n} users."
|
||||
|
@ -66,7 +66,11 @@
|
||||
"notifyUserCreation": "à la création de l'utilisateur",
|
||||
"label": "Etiquette",
|
||||
"settingsRestarting": "Redémarrage…",
|
||||
"settingsRestart": "Redémarrer"
|
||||
"settingsRestart": "Redémarrer",
|
||||
"announce": "Annoncer",
|
||||
"subject": "Sujet du courriel",
|
||||
"message": "Message",
|
||||
"markdownSupported": "Markdown est pris en charge."
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Adresse e-mail modifiée de {n}.",
|
||||
@ -95,7 +99,8 @@
|
||||
"errorFailureCheckLogs": "Échec (vérifier la console / les journaux)",
|
||||
"errorPartialFailureCheckLogs": "Panne partielle (vérifier la console / les journaux)",
|
||||
"errorUserCreated": "Echec lors de la création de l'utilisateur {n}.",
|
||||
"errorSendWelcomeEmail": "Echec lors de l'envoi du mail de bienvenue (vérifier la console/les journaux)"
|
||||
"errorSendWelcomeEmail": "Echec lors de l'envoi du mail de bienvenue (vérifier la console/les journaux)",
|
||||
"sentAnnouncement": "Annonce envoyée."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
@ -121,6 +126,10 @@
|
||||
"appliedSettings": {
|
||||
"singular": "Appliquer le paramètre {n} utilisteur.",
|
||||
"plural": "Appliquer les paramètres {n} utilisteurs."
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "Annonce à {n} utilisateur",
|
||||
"plural": "Annonce à {n} utilisateurs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
75
mail/announcement.mjml
Normal file
75
mail/announcement.mjml
Normal file
@ -0,0 +1,75 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-raw>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
</mj-raw>
|
||||
<mj-style>
|
||||
:root {
|
||||
Color-scheme: light dark;
|
||||
supported-color-schemes: light dark;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
</mj-style>
|
||||
<mj-attributes>
|
||||
<mj-class name="bg" background-color="#101010" />
|
||||
<mj-class name="bg2" background-color="#242424" />
|
||||
<mj-class name="text" color="#cacaca" />
|
||||
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||
</mj-attributes>
|
||||
<mj-font name="Quicksand" href="https://fonts.googleapis.com/css2?family=Quicksand" />
|
||||
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans" />
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> {{ .jellyfin }} </mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg">
|
||||
<mj-column>
|
||||
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<mj-raw>{{ .text }}</mj-raw>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="secondary" font-style="italic" font-size="14px">
|
||||
{{ .message }}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</body>
|
||||
</mjml>
|
3
mail/announcement.txt
Normal file
3
mail/announcement.txt
Normal file
@ -0,0 +1,3 @@
|
||||
{{ .plaintext }}
|
||||
|
||||
{{ .message }}
|
@ -1,9 +1,49 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-raw>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
</mj-raw>
|
||||
<mj-style>
|
||||
:root {
|
||||
Color-scheme: light dark;
|
||||
supported-color-schemes: light dark;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
</mj-style>
|
||||
<mj-attributes>
|
||||
<mj-class name="bg" background-color="#101010" />
|
||||
<mj-class name="bg2" background-color="#242424" />
|
||||
<mj-class name="text" color="rgba(255,255,255,0.8)" />
|
||||
<mj-class name="text" color="#cacaca" />
|
||||
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||
@ -14,7 +54,7 @@
|
||||
<mj-body>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> Jellyfin </mj-text>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> {{ .jellyfin }} </mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg">
|
||||
|
@ -1,9 +1,49 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-raw>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
</mj-raw>
|
||||
<mj-style>
|
||||
:root {
|
||||
Color-scheme: light dark;
|
||||
supported-color-schemes: light dark;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
</mj-style>
|
||||
<mj-attributes>
|
||||
<mj-class name="bg" background-color="#101010" />
|
||||
<mj-class name="bg2" background-color="#242424" />
|
||||
<mj-class name="text" color="rgba(255,255,255,0.8)" />
|
||||
<mj-class name="text" color="#cacaca" />
|
||||
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||
|
@ -1,9 +1,49 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-raw>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
</mj-raw>
|
||||
<mj-style>
|
||||
:root {
|
||||
Color-scheme: light dark;
|
||||
supported-color-schemes: light dark;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
</mj-style>
|
||||
<mj-attributes>
|
||||
<mj-class name="bg" background-color="#101010" />
|
||||
<mj-class name="bg2" background-color="#242424" />
|
||||
<mj-class name="text" color="rgba(255,255,255,0.8)" />
|
||||
<mj-class name="text" color="#cacaca" />
|
||||
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||
@ -14,7 +54,7 @@
|
||||
<mj-body>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> Jellyfin </mj-text>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> {{ .jellyfin }} </mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg">
|
||||
|
@ -1,9 +1,49 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-raw>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
</mj-raw>
|
||||
<mj-style>
|
||||
:root {
|
||||
Color-scheme: light dark;
|
||||
supported-color-schemes: light dark;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
</mj-style>
|
||||
<mj-attributes>
|
||||
<mj-class name="bg" background-color="#101010" />
|
||||
<mj-class name="bg2" background-color="#242424" />
|
||||
<mj-class name="text" color="rgba(255,255,255,0.8)" />
|
||||
<mj-class name="text" color="#cacaca" />
|
||||
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||
@ -14,7 +54,7 @@
|
||||
<mj-body>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> Jellyfin </mj-text>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> {{ .jellyfin }} </mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg">
|
||||
|
@ -1,9 +1,49 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-raw>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
</mj-raw>
|
||||
<mj-style>
|
||||
:root {
|
||||
Color-scheme: light dark;
|
||||
supported-color-schemes: light dark;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
</mj-style>
|
||||
<mj-attributes>
|
||||
<mj-class name="bg" background-color="#101010" />
|
||||
<mj-class name="bg2" background-color="#242424" />
|
||||
<mj-class name="text" color="rgba(255,255,255,0.8)" />
|
||||
<mj-class name="text" color="#cacaca" />
|
||||
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||
|
@ -1,9 +1,49 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-raw>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
</mj-raw>
|
||||
<mj-style>
|
||||
:root {
|
||||
Color-scheme: light dark;
|
||||
supported-color-schemes: light dark;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
</mj-style>
|
||||
<mj-attributes>
|
||||
<mj-class name="bg" background-color="#101010" />
|
||||
<mj-class name="bg2" background-color="#242424" />
|
||||
<mj-class name="text" color="rgba(255,255,255,0.8)" />
|
||||
<mj-class name="text" color="#cacaca" />
|
||||
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||
@ -14,7 +54,7 @@
|
||||
<mj-body>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> Jellyfin </mj-text>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> {{ .jellyfin }} </mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg">
|
||||
|
@ -1,9 +1,49 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-raw>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
</mj-raw>
|
||||
<mj-style>
|
||||
:root {
|
||||
Color-scheme: light dark;
|
||||
supported-color-schemes: light dark;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
Color-scheme: dark;
|
||||
.body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsc] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
[data-ogsb] .body {
|
||||
background: #242424 !important;
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
}
|
||||
</mj-style>
|
||||
<mj-attributes>
|
||||
<mj-class name="bg" background-color="#101010" />
|
||||
<mj-class name="bg2" background-color="#242424" />
|
||||
<mj-class name="text" color="rgba(255,255,255,0.8)" />
|
||||
<mj-class name="text" color="#cacaca" />
|
||||
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
|
||||
<mj-class name="secondary" color="rgb(153,153,153)" />
|
||||
<mj-class name="blue" background-color="rgb(0,164,220)" />
|
||||
@ -14,7 +54,7 @@
|
||||
<mj-body>
|
||||
<mj-section mj-class="bg2">
|
||||
<mj-column>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> Jellyfin </mj-text>
|
||||
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> {{ .jellyfin }} </mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg">
|
||||
|
2
main.go
2
main.go
@ -65,7 +65,6 @@ type appContext struct {
|
||||
configBasePath string
|
||||
configBase settings
|
||||
dataPath string
|
||||
systemFS fs.FS
|
||||
webFS httpFS
|
||||
cssClass string
|
||||
jellyfinLogin bool
|
||||
@ -141,7 +140,6 @@ func start(asDaemon, firstCall bool) {
|
||||
userConfigDir, _ := os.UserConfigDir()
|
||||
app.dataPath = filepath.Join(userConfigDir, "jfa-go")
|
||||
app.configPath = filepath.Join(app.dataPath, "config.ini")
|
||||
app.systemFS = os.DirFS("/")
|
||||
// gin-static doesn't just take a plain http.FileSystem, so we implement it's ServeFileSystem. See static.go.
|
||||
app.webFS = httpFS{
|
||||
hfs: http.FS(localFS),
|
||||
|
@ -131,6 +131,12 @@ type userSettingsDTO struct {
|
||||
Homescreen bool `json:"homescreen"` // Whether to apply homescreen layout or not
|
||||
}
|
||||
|
||||
type announcementDTO struct {
|
||||
Users []string `json:"users"` // List of User IDs to send announcement to
|
||||
Subject string `json:"subject"` // Email subject
|
||||
Message string `json:"message"` // Email content (markdown supported)
|
||||
}
|
||||
|
||||
type errorListDTO map[string]map[string]string
|
||||
|
||||
type configDTO map[string]interface{}
|
||||
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -228,9 +228,9 @@
|
||||
"integrity": "sha1-vfpzUplmTfr9NFKe1PhSKidf6lY="
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.8.46",
|
||||
"resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.8.46.tgz",
|
||||
"integrity": "sha1-j8cjDOMBmxLiVTOZ8MA4dacpwms="
|
||||
"version": "0.8.48",
|
||||
"resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.8.48.tgz",
|
||||
"integrity": "sha1-pX5N3oTsVtocbsrv7pfp2mxbALU="
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.1.1",
|
||||
|
@ -18,7 +18,7 @@
|
||||
"homepage": "https://github.com/hrfee/jfa-go#readme",
|
||||
"dependencies": {
|
||||
"a17t": "^0.4.0",
|
||||
"esbuild": "^0.8.46",
|
||||
"esbuild": "^0.8.48",
|
||||
"lodash": "^4.17.19",
|
||||
"mjml": "^4.8.0",
|
||||
"remixicon": "^2.5.0",
|
||||
|
@ -86,7 +86,7 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct password reset email for %s", pwr.Username)
|
||||
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||
} else if err := app.email.send(address, msg); err != nil {
|
||||
} else if err := app.email.send(msg, address); err != nil {
|
||||
app.err.Printf("Failed to send password reset email to \"%s\"", address)
|
||||
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||
} else {
|
||||
|
@ -138,6 +138,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
api.POST(p+"/users/emails", app.ModifyEmails)
|
||||
// api.POST(p + "/setDefaults", app.SetDefaults)
|
||||
api.POST(p+"/users/settings", app.ApplySettings)
|
||||
api.POST(p+"/users/announce", app.Announce)
|
||||
api.GET(p+"/config", app.GetConfig)
|
||||
api.POST(p+"/config", app.ModifyConfig)
|
||||
api.POST(p+"/restart", app.restart)
|
||||
|
@ -51,6 +51,8 @@ window.availableProfiles = window.availableProfiles || [];
|
||||
window.modals.profiles = new Modal(document.getElementById("modal-user-profiles"));
|
||||
|
||||
window.modals.addProfile = new Modal(document.getElementById("modal-add-profile"));
|
||||
|
||||
window.modals.announce = new Modal(document.getElementById("modal-announce"));
|
||||
})();
|
||||
|
||||
var inviteCreator = new createInvite();
|
||||
|
@ -148,6 +148,7 @@ export class accountsList {
|
||||
private _table = document.getElementById("accounts-list") as HTMLTableSectionElement;
|
||||
|
||||
private _addUserButton = document.getElementById("accounts-add-user") as HTMLSpanElement;
|
||||
private _announceButton = document.getElementById("accounts-announce") as HTMLSpanElement;
|
||||
private _deleteUser = document.getElementById("accounts-delete-user") as HTMLSpanElement;
|
||||
private _deleteNotify = document.getElementById("delete-user-notify") as HTMLInputElement;
|
||||
private _deleteReason = document.getElementById("textarea-delete-user") as HTMLTextAreaElement;
|
||||
@ -189,6 +190,9 @@ export class accountsList {
|
||||
this._selectAll.checked = false;
|
||||
this._modifySettings.classList.add("unfocused");
|
||||
this._deleteUser.classList.add("unfocused");
|
||||
if (window.emailEnabled) {
|
||||
this._announceButton.classList.add("unfocused");
|
||||
}
|
||||
} else {
|
||||
if (this._checkCount == Object.keys(this._users).length) {
|
||||
this._selectAll.checked = true;
|
||||
@ -200,6 +204,9 @@ export class accountsList {
|
||||
this._modifySettings.classList.remove("unfocused");
|
||||
this._deleteUser.classList.remove("unfocused");
|
||||
this._deleteUser.textContent = window.lang.quantity("deleteUser", this._checkCount);
|
||||
if (window.emailEnabled) {
|
||||
this._announceButton.classList.remove("unfocused");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -247,6 +254,39 @@ export class accountsList {
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
announce = () => {
|
||||
const modalHeader = document.getElementById("header-announce");
|
||||
modalHeader.textContent = window.lang.quantity("announceTo", this._checkCount);
|
||||
const form = document.getElementById("form-announce") as HTMLFormElement;
|
||||
let list = this._collectUsers();
|
||||
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
||||
const subject = document.getElementById("announce-subject") as HTMLInputElement;
|
||||
const message = document.getElementById("textarea-announce") as HTMLTextAreaElement;
|
||||
subject.value = "";
|
||||
message.value = "";
|
||||
form.onsubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
toggleLoader(button);
|
||||
let send = {
|
||||
"users": list,
|
||||
"subject": subject.value,
|
||||
"message": message.value
|
||||
}
|
||||
_post("/users/announce", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(button);
|
||||
window.modals.announce.close();
|
||||
if (req.status != 200 && req.status != 204) {
|
||||
window.notifications.customError("announcementError", window.lang.notif("errorFailureCheckLogs"));
|
||||
} else {
|
||||
window.notifications.customSuccess("announcementSuccess", window.lang.notif("sentAnnouncement"));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
window.modals.announce.show();
|
||||
}
|
||||
|
||||
deleteUsers = () => {
|
||||
const modalHeader = document.getElementById("header-delete-user");
|
||||
@ -397,6 +437,9 @@ export class accountsList {
|
||||
this._deleteUser.onclick = this.deleteUsers;
|
||||
this._deleteUser.classList.add("unfocused");
|
||||
|
||||
this._announceButton.onclick = this.announce;
|
||||
this._announceButton.classList.add("unfocused");
|
||||
|
||||
if (!window.usernameEnabled) {
|
||||
this._addUserName.classList.add("unfocused");
|
||||
this._addUserName = this._addUserEmail;
|
||||
|
@ -74,6 +74,7 @@ declare interface Modals {
|
||||
ombiDefaults?: Modal;
|
||||
profiles: Modal;
|
||||
addProfile: Modal;
|
||||
announce: Modal;
|
||||
}
|
||||
|
||||
interface Invite {
|
||||
|
Loading…
Reference in New Issue
Block a user