1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-10-18 17:10:11 +00:00

Compare commits

..

No commits in common. "9799665951a5a2cc108d2f1ebcd4a20b84c89293" and "f40fb9d3f7a62fdeb7d5308ad02ba965e069f8a7" have entirely different histories.

15 changed files with 156 additions and 167 deletions

View File

@ -20,6 +20,7 @@ before:
- python3 scripts/enumerate_config.py -i config/config-base.json -o data/config-base.json
- python3 scripts/generate_ini.py -i config/config-base.json -o data/config-default.ini
- python3 scripts/compile_mjml.py -o data/
- python3 scripts/version.py {{.Version}}
- npx esbuild --bundle ts/admin.ts --outfile=./data/web/js/admin.js --minify
- npx esbuild --bundle ts/form.ts --outfile=./data/web/js/form.js --minify
- npx esbuild --bundle ts/setup.ts --outfile=./data/web/js/setup.js --minify

View File

@ -6,7 +6,7 @@ RUN apt-get update -y \
&& apt-get install build-essential python3-pip curl software-properties-common sed -y \
&& (curl -sL https://deb.nodesource.com/setup_14.x | bash -) \
&& apt-get install nodejs \
&& (cd /opt/build; make configuration npm email typescript bundle-css swagger copy external-files GOESBUILD=on) \
&& (cd /opt/build; make configuration npm email version typescript bundle-css swagger copy external-files GOESBUILD=on) \
&& sed -i 's#id="password_resets-watch_directory" placeholder="/config/jellyfin"#id="password_resets-watch_directory" value="/jf" disabled#g' /opt/build/build/data/html/setup.html

View File

@ -6,10 +6,6 @@ else
endif
GOBINARY ?= go
VERSION ?= $(shell git describe --exact-match HEAD 2> /dev/null || echo vgit)
VERSION := $(shell echo $(VERSION) | sed 's/v//g')
COMMIT ?= $(shell git rev-parse --short HEAD || echo unknown)
npm:
$(info installing npm dependencies)
npm install
@ -51,19 +47,22 @@ swagger:
$(GOBINARY) get github.com/swaggo/swag/cmd/swag
swag init -g main.go
version:
python3 scripts/version.py auto
compile:
$(info Downloading deps)
$(GOBINARY) mod download
$(info Building)
mkdir -p build
cd build && CGO_ENABLED=0 $(GOBINARY) build -ldflags="-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT)" -o ./jfa-go ../*.go
cd build && CGO_ENABLED=0 $(GOBINARY) build -ldflags="-s -w" -o ./jfa-go ../*.go
compile-debug:
$(info Downloading deps)
$(GOBINARY) mod download
$(info Building)
mkdir -p build
cd build && CGO_ENABLED=0 $(GOBINARY) build -o ./jfa-go ../*.go -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT)"
cd build && CGO_ENABLED=0 $(GOBINARY) build -o ./jfa-go ../*.go
compress:
upx --lzma build/jfa-go
@ -96,6 +95,6 @@ external-files:
install:
cp -r build $(DESTDIR)/jfa-go
all: configuration npm email typescript bundle-css swagger copy internal-files compile
all-external: configuration npm email typescript bundle-css swagger copy external-files compile
debug: configuration npm email ts-debug bundle-css swagger copy external-files compile-debug
all: configuration npm email version typescript bundle-css swagger copy internal-files compile
all-external: configuration npm email version typescript bundle-css swagger copy external-files compile
debug: configuration npm email version ts-debug bundle-css swagger copy external-files compile-debug

View File

@ -19,7 +19,7 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
* Email addresses can optionally be used instead of usernames
* 🔑 Password resets: When user's forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to the user via email.
* Notifications: Get notified when someone creates an account, or an invite expires.
* 📣 Announcements: Bulk email your users with announcements about your server.
* 📣 Announcements: Bulk email you users with announcements about your server.
* Authentication via Jellyfin: Instead of using separate credentials for jfa-go and Jellyfin, jfa-go can use it as the authentication provider.
* Enables the usage of jfa-go by multiple people
* 🌓 Customizable look

99
api.go
View File

@ -1375,9 +1375,87 @@ func (app *appContext) SetEmailState(gc *gin.Context) {
respondBool(200, true, gc)
}
// @Summary Returns the custom email (generating it if not set) and list of used variables in it.
// @Summary Render and return an email for testing purposes.
// @Produce json
// @Success 200 {object} customEmailDTO
// @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.
// @Produce json
// @Success 200 {object} customEmail
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /config/emails/{id} [get]
@ -1388,11 +1466,8 @@ func (app *appContext) GetEmail(gc *gin.Context) {
var err error
var msg *Email
var variables []string
var values map[string]interface{}
var writeVars func(variables []string)
newEmail := false
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" {
content = app.storage.customEmails.UserCreated.Content
if content == "" {
@ -1403,7 +1478,6 @@ func (app *appContext) GetEmail(gc *gin.Context) {
variables = app.storage.customEmails.UserCreated.Variables
}
writeVars = func(variables []string) { app.storage.customEmails.UserCreated.Variables = variables }
values = app.email.createdValues("xxxxxx", username, emailAddress, Invite{}, app, false)
// app.storage.customEmails.UserCreated = content
} else if id == "InviteExpiry" {
content = app.storage.customEmails.InviteExpiry.Content
@ -1415,7 +1489,6 @@ func (app *appContext) GetEmail(gc *gin.Context) {
variables = app.storage.customEmails.InviteExpiry.Variables
}
writeVars = func(variables []string) { app.storage.customEmails.InviteExpiry.Variables = variables }
values = app.email.expiryValues("xxxxxx", Invite{}, app, false)
// app.storage.customEmails.InviteExpiry = content
} else if id == "PasswordReset" {
content = app.storage.customEmails.PasswordReset.Content
@ -1427,7 +1500,6 @@ func (app *appContext) GetEmail(gc *gin.Context) {
variables = app.storage.customEmails.PasswordReset.Variables
}
writeVars = func(variables []string) { app.storage.customEmails.PasswordReset.Variables = variables }
values = app.email.resetValues(PasswordReset{Pin: "12-34-56", Username: username}, app, false)
// app.storage.customEmails.PasswordReset = content
} else if id == "UserDeleted" {
content = app.storage.customEmails.UserDeleted.Content
@ -1439,7 +1511,6 @@ func (app *appContext) GetEmail(gc *gin.Context) {
variables = app.storage.customEmails.UserDeleted.Variables
}
writeVars = func(variables []string) { app.storage.customEmails.UserDeleted.Variables = variables }
values = app.email.deletedValues(app.storage.lang.Email[app.storage.lang.chosenEmailLang].UserDeleted.get("reason"), app, false)
// app.storage.customEmails.UserDeleted = content
} else if id == "InviteEmail" {
content = app.storage.customEmails.InviteEmail.Content
@ -1451,7 +1522,6 @@ func (app *appContext) GetEmail(gc *gin.Context) {
variables = app.storage.customEmails.InviteEmail.Variables
}
writeVars = func(variables []string) { app.storage.customEmails.InviteEmail.Variables = variables }
values = app.email.inviteValues("xxxxxx", Invite{}, app, false)
// app.storage.customEmails.InviteEmail = content
} else if id == "WelcomeEmail" {
content = app.storage.customEmails.WelcomeEmail.Content
@ -1464,7 +1534,6 @@ func (app *appContext) GetEmail(gc *gin.Context) {
}
writeVars = func(variables []string) { app.storage.customEmails.WelcomeEmail.Variables = variables }
// app.storage.customEmails.WelcomeEmail = content
values = app.email.welcomeValues(username, app, false)
} else if id == "EmailConfirmation" {
content = app.storage.customEmails.EmailConfirmation.Content
if content == "" {
@ -1475,7 +1544,6 @@ func (app *appContext) GetEmail(gc *gin.Context) {
variables = app.storage.customEmails.EmailConfirmation.Variables
}
writeVars = func(variables []string) { app.storage.customEmails.EmailConfirmation.Variables = variables }
values = app.email.confirmationValues("xxxxxx", username, "xxxxxx", app, false)
// app.storage.customEmails.EmailConfirmation = content
} else {
respondBool(400, false, gc)
@ -1509,12 +1577,7 @@ func (app *appContext) GetEmail(gc *gin.Context) {
respondBool(500, false, gc)
return
}
email, err := app.email.constructTemplate("", "<div id=\"preview-content\"></div>", app)
if err != nil {
respondBool(500, false, gc)
return
}
gc.JSON(200, customEmailDTO{Content: content, Variables: variables, Values: values, HTML: email.HTML})
gc.JSON(200, customEmail{Content: content, Variables: variables})
}
// @Summary Logout by deleting refresh token from cookies.

View File

@ -67,9 +67,9 @@ func (app *appContext) loadConfig() error {
app.config.Section("template_email").Key("email_html").SetValue(app.config.Section("template_email").Key("email_html").MustString("jfa-go:" + "template.html"))
app.config.Section("template_email").Key("email_text").SetValue(app.config.Section("template_email").Key("email_text").MustString("jfa-go:" + "template.txt"))
app.config.Section("jellyfin").Key("version").SetValue(version)
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))
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", VERSION, COMMIT))
if app.config.Section("email").Key("method").MustString("") == "" {
emailEnabled = false

View File

@ -212,7 +212,11 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
return
}
func (emailer *Emailer) confirmationValues(code, username, key string, app *appContext, noSub bool) map[string]interface{} {
func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext, noSub bool) (*Email, error) {
email := &Email{
Subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")),
}
var err error
template := map[string]interface{}{
"clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"),
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
@ -234,15 +238,6 @@ func (emailer *Emailer) confirmationValues(code, username, key string, app *appC
template["confirmationURL"] = inviteLink
template["message"] = message
}
return template
}
func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext, noSub bool) (*Email, error) {
email := &Email{
Subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")),
}
var err error
template := emailer.confirmationValues(code, username, key, app, noSub)
if app.storage.customEmails.EmailConfirmation.Enabled {
content := app.storage.customEmails.EmailConfirmation.Content
for _, v := range app.storage.customEmails.EmailConfirmation.Variables {
@ -279,7 +274,10 @@ func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (
return email, nil
}
func (emailer *Emailer) inviteValues(code string, invite Invite, app *appContext, noSub bool) map[string]interface{} {
func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext, noSub bool) (*Email, error) {
email := &Email{
Subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.InviteEmail.get("title")),
}
expiry := invite.ValidTill
d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern)
message := app.config.Section("email").Key("message").String()
@ -306,14 +304,6 @@ func (emailer *Emailer) inviteValues(code string, invite Invite, app *appContext
template["inviteURL"] = inviteLink
template["message"] = message
}
return template
}
func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext, noSub bool) (*Email, error) {
email := &Email{
Subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.InviteEmail.get("title")),
}
template := emailer.inviteValues(code, invite, app, noSub)
var err error
if app.storage.customEmails.InviteEmail.Enabled {
content := app.storage.customEmails.InviteEmail.Content
@ -333,7 +323,10 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
return email, nil
}
func (emailer *Emailer) expiryValues(code string, invite Invite, app *appContext, noSub bool) map[string]interface{} {
func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext, noSub bool) (*Email, error) {
email := &Email{
Subject: emailer.lang.InviteExpiry.get("title"),
}
expiry := app.formatDatetime(invite.ValidTill)
template := map[string]interface{}{
"inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"),
@ -346,15 +339,7 @@ func (emailer *Emailer) expiryValues(code string, invite Invite, app *appContext
} else {
template["expiredAt"] = emailer.lang.InviteExpiry.template("expiredAt", tmpl{"code": template["code"].(string), "time": template["time"].(string)})
}
return template
}
func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext, noSub bool) (*Email, error) {
email := &Email{
Subject: emailer.lang.InviteExpiry.get("title"),
}
var err error
template := emailer.expiryValues(code, invite, app, noSub)
if app.storage.customEmails.InviteExpiry.Enabled {
content := app.storage.customEmails.InviteExpiry.Content
for _, v := range app.storage.customEmails.InviteExpiry.Variables {
@ -373,7 +358,10 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
return email, nil
}
func (emailer *Emailer) createdValues(code, username, address string, invite Invite, app *appContext, noSub bool) map[string]interface{} {
func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext, noSub bool) (*Email, error) {
email := &Email{
Subject: emailer.lang.UserCreated.get("title"),
}
template := map[string]interface{}{
"nameString": emailer.lang.Strings.get("name"),
"addressString": emailer.lang.Strings.get("emailAddress"),
@ -401,14 +389,6 @@ func (emailer *Emailer) createdValues(code, username, address string, invite Inv
template["time"] = created
template["notificationNotice"] = emailer.lang.UserCreated.get("notificationNotice")
}
return template
}
func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext, noSub bool) (*Email, error) {
email := &Email{
Subject: emailer.lang.UserCreated.get("title"),
}
template := emailer.createdValues(code, username, address, invite, app, noSub)
var err error
if app.storage.customEmails.UserCreated.Enabled {
content := app.storage.customEmails.UserCreated.Content
@ -428,7 +408,10 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
return email, nil
}
func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bool) map[string]interface{} {
func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub bool) (*Email, error) {
email := &Email{
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)
message := app.config.Section("email").Key("message").String()
template := map[string]interface{}{
@ -455,14 +438,6 @@ func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bo
template["pin"] = pwr.Pin
template["message"] = message
}
return template
}
func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub bool) (*Email, error) {
email := &Email{
Subject: app.config.Section("password_resets").Key("subject").MustString(emailer.lang.PasswordReset.get("title")),
}
template := emailer.resetValues(pwr, app, noSub)
var err error
if app.storage.customEmails.PasswordReset.Enabled {
content := app.storage.customEmails.PasswordReset.Content
@ -482,7 +457,10 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub
return email, nil
}
func (emailer *Emailer) deletedValues(reason string, app *appContext, noSub bool) map[string]interface{} {
func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub bool) (*Email, error) {
email := &Email{
Subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")),
}
template := map[string]interface{}{
"yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
"reasonString": emailer.lang.UserDeleted.get("reason"),
@ -497,15 +475,7 @@ func (emailer *Emailer) deletedValues(reason string, app *appContext, noSub bool
template["reason"] = reason
template["message"] = app.config.Section("email").Key("message").String()
}
return template
}
func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub bool) (*Email, error) {
email := &Email{
Subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")),
}
var err error
template := emailer.deletedValues(reason, app, noSub)
if app.storage.customEmails.UserDeleted.Enabled {
content := app.storage.customEmails.UserDeleted.Content
for _, v := range app.storage.customEmails.UserDeleted.Variables {
@ -524,7 +494,10 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub b
return email, nil
}
func (emailer *Emailer) welcomeValues(username string, app *appContext, noSub bool) map[string]interface{} {
func (emailer *Emailer) constructWelcome(username string, app *appContext, noSub bool) (*Email, error) {
email := &Email{
Subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")),
}
template := map[string]interface{}{
"welcome": emailer.lang.WelcomeEmail.get("welcome"),
"youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"),
@ -542,15 +515,7 @@ func (emailer *Emailer) welcomeValues(username string, app *appContext, noSub bo
template["username"] = username
template["message"] = app.config.Section("email").Key("message").String()
}
return template
}
func (emailer *Emailer) constructWelcome(username string, app *appContext, noSub bool) (*Email, error) {
email := &Email{
Subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")),
}
var err error
template := emailer.welcomeValues(username, app, noSub)
if app.storage.customEmails.WelcomeEmail.Enabled {
content := app.storage.customEmails.WelcomeEmail.Content
for _, v := range app.storage.customEmails.WelcomeEmail.Variables {

View File

@ -44,8 +44,6 @@ var (
info = color.New(color.FgMagenta).SprintfFunc()
hiwhite = color.New(color.FgHiWhite).SprintfFunc()
white = color.New(color.FgWhite).SprintfFunc()
version string
commit string
)
var serverTypes = map[string]string{
@ -626,7 +624,7 @@ func flagPassed(name string) (found bool) {
// @tag.description Things that dont fit elsewhere.
func printVersion() {
fmt.Println(info("jfa-go version: %s (%s)\n", hiwhite(version), white(commit)))
fmt.Println(info("jfa-go version: %s (%s)\n", hiwhite(VERSION), white(COMMIT)))
}
func main() {

View File

@ -189,10 +189,3 @@ type emailSetDTO struct {
type emailTestDTO struct {
Address string `json:"address"`
}
type customEmailDTO struct {
Content string `json:"content"`
Variables []string `json:"variables"`
Values map[string]interface{} `json:"values"`
HTML string `json:"html"`
}

19
package-lock.json generated
View File

@ -12,14 +12,6 @@
"regenerator-runtime": "^0.13.4"
}
},
"@ts-stack/markdown": {
"version": "1.3.0",
"resolved": "https://registry.npm.taobao.org/@ts-stack/markdown/download/@ts-stack/markdown-1.3.0.tgz",
"integrity": "sha1-OdkuDifo9w6Ba3L/EzaO6HWWe+o=",
"requires": {
"tslib": "^2.0.0"
}
},
"@types/node": {
"version": "14.14.16",
"resolved": "https://registry.npm.taobao.org/@types/node/download/@types/node-14.14.16.tgz?cache=0&sync_timestamp=1608756036972&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-14.14.16.tgz",
@ -236,9 +228,9 @@
"integrity": "sha1-vfpzUplmTfr9NFKe1PhSKidf6lY="
},
"esbuild": {
"version": "0.8.50",
"resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.8.50.tgz",
"integrity": "sha1-6/JP3gza0aNpeJ3W/XqCCwoB5Gw="
"version": "0.8.49",
"resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.8.49.tgz",
"integrity": "sha1-PTP3GzlmYR+CLPTIOBFfP70W3vI="
},
"escalade": {
"version": "3.1.1",
@ -1471,11 +1463,6 @@
"safe-buffer": "~5.1.0"
}
},
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npm.taobao.org/tslib/download/tslib-2.1.0.tgz?cache=0&sync_timestamp=1609887446200&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftslib%2Fdownload%2Ftslib-2.1.0.tgz",
"integrity": "sha1-2mCGDxwuyqVwOrfTm8Bba/mIuXo="
},
"typescript": {
"version": "4.1.3",
"resolved": "https://registry.npm.taobao.org/typescript/download/typescript-4.1.3.tgz?cache=0&sync_timestamp=1609830171931&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftypescript%2Fdownload%2Ftypescript-4.1.3.tgz",

View File

@ -17,9 +17,8 @@
},
"homepage": "https://github.com/hrfee/jfa-go#readme",
"dependencies": {
"@ts-stack/markdown": "^1.3.0",
"a17t": "^0.4.0",
"esbuild": "^0.8.50",
"esbuild": "^0.8.49",
"lodash": "^4.17.19",
"mjml": "^4.8.0",
"remixicon": "^2.5.0",

View File

@ -142,6 +142,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/config/emails", app.GetEmails)
api.GET(p+"/config/emails/:id", app.GetEmail)
api.POST(p+"/config/emails/:id", app.SetEmail)
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.POST(p+"/config", app.ModifyConfig)

View File

@ -1,5 +1,4 @@
import { _get, _post, toggleLoader } from "../modules/common.js";
import { Marked } from "@ts-stack/markdown";
interface settingsBoolEvent extends Event {
detail: boolean;
@ -681,8 +680,6 @@ interface Email {
interface templateEmail {
content: string;
variables: string[];
values: { [key: string]: string };
html: string;
}
interface emailListEl {
@ -694,14 +691,13 @@ class EmailEditor {
private _currentID: string;
private _names: { [id: string]: emailListEl };
private _content: string;
private _templ: templateEmail;
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 = 200;
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);
@ -732,25 +728,23 @@ class EmailEditor {
if (this._names[id] !== undefined) {
this._header.textContent = this._names[id].name;
}
this._templ = req.response as templateEmail;
this._textArea.value = this._templ.content;
this._preview.innerHTML = this._templ.html;
const templ = req.response as templateEmail;
this._textArea.value = templ.content;
this.loadPreview();
this._content = this._templ.content;
this._content = templ.content;
const colors = ["info", "urge", "positive", "neutral"];
let innerHTML = '';
for (let i = 0; i < this._templ.variables.length; i++) {
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 < this._templ.variables.length; i++) {
buttons[i].innerHTML = `<span class="monospace">` + this._templ.variables[i] + `</span>`;
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, this._templ.variables[i]);
this.loadPreview();
// this._timeout = setTimeout(this.loadPreview, this._finishInterval);
this.insert(this._textArea, templ.variables[i]);
this._timeout = setTimeout(this.loadPreview, this._finishInterval);
}
}
window.modals.editor.show();
@ -758,23 +752,15 @@ class EmailEditor {
})
}
loadPreview = () => {
let content = this._textArea.value;
for (let variable of this._templ.variables) {
let value = this._templ.values[variable.slice(1, -1)];
if (value === undefined) { value = variable; }
content = content.replace(new RegExp(variable, "g"), value);
}
content = Marked.parse(content)
document.getElementById("preview-content").innerHTML = content;
// _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);
_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 = () => {
@ -823,13 +809,12 @@ class EmailEditor {
constructor() {
this._textArea.onkeyup = () => {
// clearTimeout(this._timeout);
// this._timeout = setTimeout(this.loadPreview, this._finishInterval);
this.loadPreview();
clearTimeout(this._timeout);
this._timeout = setTimeout(this.loadPreview, this._finishInterval);
};
this._textArea.onkeydown = () => {
clearTimeout(this._timeout);
};
// this._textArea.onkeydown = () => {
// clearTimeout(this._timeout);
// };
this._form.onsubmit = (event: Event) => {
if (this._textArea.value == this._content) {

View File

@ -3,8 +3,6 @@
"outDir": "../js",
"target": "es6",
"lib": ["dom", "es2017"],
"typeRoots": ["./typings", "./node_modules/@types"],
"moduleResolution": "node",
"esModuleInterop": true
"typeRoots": ["./node_modules/@types", "./typings"]
}
}

View File

@ -74,8 +74,8 @@ func (app *appContext) AdminPage(gc *gin.Context) {
"contactMessage": "",
"email_enabled": emailEnabled,
"notifications": notificationsEnabled,
"version": version,
"commit": commit,
"version": VERSION,
"commit": COMMIT,
"ombiEnabled": ombiEnabled,
"username": !app.config.Section("email").Key("no_username").MustBool(false),
"strings": app.storage.lang.Admin[lang].Strings,