mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-11-10 04:10:10 +00:00
Compare commits
No commits in common. "ef5d89f323cdf12449e295e1feffa3527bd2c39c" and "bf981935cbe86a4913873ae530a192b16bf3b1ed" have entirely different histories.
ef5d89f323
...
bf981935cb
3
.gitignore
vendored
3
.gitignore
vendored
@ -21,6 +21,3 @@ cl.md
|
|||||||
mautrix/
|
mautrix/
|
||||||
tempts/
|
tempts/
|
||||||
matacc.txt
|
matacc.txt
|
||||||
scripts/langmover/lang
|
|
||||||
scripts/langmover/lang2
|
|
||||||
scripts/langmover/out
|
|
||||||
|
@ -26,11 +26,11 @@ before:
|
|||||||
- cp -r ts tempts
|
- cp -r ts tempts
|
||||||
- scripts/dark-variant.sh tempts
|
- scripts/dark-variant.sh tempts
|
||||||
- scripts/dark-variant.sh tempts/modules
|
- scripts/dark-variant.sh tempts/modules
|
||||||
- npx esbuild --target=es6 --format=esm --bundle tempts/admin.ts --outfile=./data/web/js/admin.js --minify
|
- npx esbuild --bundle tempts/admin.ts --outfile=./data/web/js/admin.js --minify
|
||||||
- npx esbuild --target=es6 --format=esm --bundle tempts/pwr.ts --outfile=./data/web/js/pwr.js --minify
|
- npx esbuild --bundle tempts/pwr.ts --outfile=./data/web/js/pwr.js --minify
|
||||||
- npx esbuild --target=es6 --format=esm --bundle tempts/form.ts --outfile=./data/web/js/form.js --minify
|
- npx esbuild --bundle tempts/form.ts --outfile=./data/web/js/form.js --minify
|
||||||
- npx esbuild --target=es6 --format=esm --bundle tempts/setup.ts --outfile=./data/web/js/setup.js --minify
|
- npx esbuild --bundle tempts/setup.ts --outfile=./data/web/js/setup.js --minify
|
||||||
- npx esbuild --target=es6 --format=esm --bundle tempts/crash.ts --outfile=./data/crash.js --minify
|
- npx esbuild --bundle tempts/crash.ts --outfile=./data/crash.js --minify
|
||||||
- rm -r tempts
|
- rm -r tempts
|
||||||
- npx esbuild --bundle css/base.css --outfile=./data/web/css/bundle.css --external:remixicon.css --minify
|
- npx esbuild --bundle css/base.css --outfile=./data/web/css/bundle.css --external:remixicon.css --minify
|
||||||
- cp html/crash.html data/
|
- cp html/crash.html data/
|
||||||
|
3
Makefile
3
Makefile
@ -104,7 +104,6 @@ typescript:
|
|||||||
$(info compiling typescript)
|
$(info compiling typescript)
|
||||||
mkdir -p $(DATA)/web/js
|
mkdir -p $(DATA)/web/js
|
||||||
$(ESBUILD) --target=es6 --bundle tempts/admin.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/admin.js --minify
|
$(ESBUILD) --target=es6 --bundle tempts/admin.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/admin.js --minify
|
||||||
$(ESBUILD) --target=es6 --bundle tempts/user.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/user.js --minify
|
|
||||||
$(ESBUILD) --target=es6 --bundle tempts/pwr.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/pwr.js --minify
|
$(ESBUILD) --target=es6 --bundle tempts/pwr.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/pwr.js --minify
|
||||||
$(ESBUILD) --target=es6 --bundle tempts/form.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/form.js --minify
|
$(ESBUILD) --target=es6 --bundle tempts/form.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/form.js --minify
|
||||||
$(ESBUILD) --target=es6 --bundle tempts/setup.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/setup.js --minify
|
$(ESBUILD) --target=es6 --bundle tempts/setup.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/setup.js --minify
|
||||||
@ -178,6 +177,4 @@ clean:
|
|||||||
-rm docs/docs.go docs/swagger.json docs/swagger.yaml
|
-rm docs/docs.go docs/swagger.json docs/swagger.yaml
|
||||||
go clean
|
go clean
|
||||||
|
|
||||||
quick: configuration typescript variants-html bundle-css inline-css copy compile
|
|
||||||
|
|
||||||
all: configuration npm email typescript variants-html bundle-css inline-css swagger copy compile
|
all: configuration npm email typescript variants-html bundle-css inline-css swagger copy compile
|
||||||
|
@ -16,7 +16,7 @@ func (app *appContext) checkInvites() {
|
|||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
app.storage.loadInvites()
|
app.storage.loadInvites()
|
||||||
changed := false
|
changed := false
|
||||||
for code, data := range app.storage.GetInvites() {
|
for code, data := range app.storage.invites {
|
||||||
expiry := data.ValidTill
|
expiry := data.ValidTill
|
||||||
if !currentTime.After(expiry) {
|
if !currentTime.After(expiry) {
|
||||||
continue
|
continue
|
||||||
@ -54,7 +54,7 @@ func (app *appContext) checkInvites() {
|
|||||||
wait.Wait()
|
wait.Wait()
|
||||||
}
|
}
|
||||||
changed = true
|
changed = true
|
||||||
app.storage.DeleteInvitesKey(code)
|
delete(app.storage.invites, code)
|
||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
app.storage.storeInvites()
|
app.storage.storeInvites()
|
||||||
@ -65,7 +65,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
|||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
app.storage.loadInvites()
|
app.storage.loadInvites()
|
||||||
changed := false
|
changed := false
|
||||||
inv, match := app.storage.GetInvitesKey(code)
|
inv, match := app.storage.invites[code]
|
||||||
if !match {
|
if !match {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -105,21 +105,21 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
|||||||
}
|
}
|
||||||
changed = true
|
changed = true
|
||||||
match = false
|
match = false
|
||||||
app.storage.DeleteInvitesKey(code)
|
delete(app.storage.invites, code)
|
||||||
} else if used {
|
} else if used {
|
||||||
changed = true
|
changed = true
|
||||||
del := false
|
del := false
|
||||||
newInv := inv
|
newInv := inv
|
||||||
if newInv.RemainingUses == 1 {
|
if newInv.RemainingUses == 1 {
|
||||||
del = true
|
del = true
|
||||||
app.storage.DeleteInvitesKey(code)
|
delete(app.storage.invites, code)
|
||||||
} else if newInv.RemainingUses != 0 {
|
} else if newInv.RemainingUses != 0 {
|
||||||
// 0 means infinite i guess?
|
// 0 means infinite i guess?
|
||||||
newInv.RemainingUses--
|
newInv.RemainingUses--
|
||||||
}
|
}
|
||||||
newInv.UsedBy = append(newInv.UsedBy, []string{username, strconv.FormatInt(currentTime.Unix(), 10)})
|
newInv.UsedBy = append(newInv.UsedBy, []string{username, strconv.FormatInt(currentTime.Unix(), 10)})
|
||||||
if !del {
|
if !del {
|
||||||
app.storage.SetInvitesKey(code, newInv)
|
app.storage.invites[code] = newInv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
@ -219,7 +219,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
|||||||
invite.Profile = "Default"
|
invite.Profile = "Default"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.storage.SetInvitesKey(inviteCode, invite)
|
app.storage.invites[inviteCode] = invite
|
||||||
app.storage.storeInvites()
|
app.storage.storeInvites()
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
}
|
}
|
||||||
@ -236,7 +236,7 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
|||||||
app.storage.loadInvites()
|
app.storage.loadInvites()
|
||||||
app.checkInvites()
|
app.checkInvites()
|
||||||
var invites []inviteDTO
|
var invites []inviteDTO
|
||||||
for code, inv := range app.storage.GetInvites() {
|
for code, inv := range app.storage.invites {
|
||||||
_, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
|
_, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
|
||||||
invite := inviteDTO{
|
invite := inviteDTO{
|
||||||
Code: code,
|
Code: code,
|
||||||
@ -280,7 +280,7 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
|||||||
var address string
|
var address string
|
||||||
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||||
app.storage.loadEmails()
|
app.storage.loadEmails()
|
||||||
if addr, ok := app.storage.GetEmailsKey(gc.GetString("jfId")); ok && addr.Addr != "" {
|
if addr, ok := app.storage.emails[gc.GetString("jfId")]; ok && addr.Addr != "" {
|
||||||
address = addr.Addr
|
address = addr.Addr
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -335,9 +335,9 @@ func (app *appContext) SetProfile(gc *gin.Context) {
|
|||||||
respond(500, "Profile not found", gc)
|
respond(500, "Profile not found", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
inv, _ := app.storage.GetInvitesKey(req.Invite)
|
inv := app.storage.invites[req.Invite]
|
||||||
inv.Profile = req.Profile
|
inv.Profile = req.Profile
|
||||||
app.storage.SetInvitesKey(req.Invite, inv)
|
app.storage.invites[req.Invite] = inv
|
||||||
app.storage.storeInvites()
|
app.storage.storeInvites()
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
}
|
}
|
||||||
@ -359,7 +359,7 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
|||||||
app.debug.Printf("%s: Notification settings change requested", code)
|
app.debug.Printf("%s: Notification settings change requested", code)
|
||||||
app.storage.loadInvites()
|
app.storage.loadInvites()
|
||||||
app.storage.loadEmails()
|
app.storage.loadEmails()
|
||||||
invite, ok := app.storage.GetInvitesKey(code)
|
invite, ok := app.storage.invites[code]
|
||||||
if !ok {
|
if !ok {
|
||||||
app.err.Printf("%s Notification setting change failed: Invalid code", code)
|
app.err.Printf("%s Notification setting change failed: Invalid code", code)
|
||||||
respond(400, "Invalid invite code", gc)
|
respond(400, "Invalid invite code", gc)
|
||||||
@ -398,7 +398,7 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
|||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
app.storage.SetInvitesKey(code, invite)
|
app.storage.invites[code] = invite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
@ -419,9 +419,9 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
|
|||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
app.debug.Printf("%s: Deletion requested", req.Code)
|
app.debug.Printf("%s: Deletion requested", req.Code)
|
||||||
var ok bool
|
var ok bool
|
||||||
_, ok = app.storage.GetInvitesKey(req.Code)
|
_, ok = app.storage.invites[req.Code]
|
||||||
if ok {
|
if ok {
|
||||||
app.storage.DeleteInvitesKey(req.Code)
|
delete(app.storage.invites, req.Code)
|
||||||
app.storage.storeInvites()
|
app.storage.storeInvites()
|
||||||
app.info.Printf("%s: Invite deleted", req.Code)
|
app.info.Printf("%s: Invite deleted", req.Code)
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
|
226
api-messages.go
226
api-messages.go
@ -15,16 +15,12 @@ import (
|
|||||||
// @Router /config/emails [get]
|
// @Router /config/emails [get]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Configuration
|
// @tags Configuration
|
||||||
func (app *appContext) GetCustomContent(gc *gin.Context) {
|
func (app *appContext) GetCustomEmails(gc *gin.Context) {
|
||||||
lang := gc.Query("lang")
|
lang := gc.Query("lang")
|
||||||
if _, ok := app.storage.lang.Email[lang]; !ok {
|
if _, ok := app.storage.lang.Email[lang]; !ok {
|
||||||
lang = app.storage.lang.chosenEmailLang
|
lang = app.storage.lang.chosenEmailLang
|
||||||
}
|
}
|
||||||
adminLang := lang
|
gc.JSON(200, emailListDTO{
|
||||||
if _, ok := app.storage.lang.Admin[lang]; !ok {
|
|
||||||
adminLang = app.storage.lang.chosenAdminLang
|
|
||||||
}
|
|
||||||
list := emailListDTO{
|
|
||||||
"UserCreated": {Name: app.storage.lang.Email[lang].UserCreated["name"], Enabled: app.storage.customEmails.UserCreated.Enabled},
|
"UserCreated": {Name: app.storage.lang.Email[lang].UserCreated["name"], Enabled: app.storage.customEmails.UserCreated.Enabled},
|
||||||
"InviteExpiry": {Name: app.storage.lang.Email[lang].InviteExpiry["name"], Enabled: app.storage.customEmails.InviteExpiry.Enabled},
|
"InviteExpiry": {Name: app.storage.lang.Email[lang].InviteExpiry["name"], Enabled: app.storage.customEmails.InviteExpiry.Enabled},
|
||||||
"PasswordReset": {Name: app.storage.lang.Email[lang].PasswordReset["name"], Enabled: app.storage.customEmails.PasswordReset.Enabled},
|
"PasswordReset": {Name: app.storage.lang.Email[lang].PasswordReset["name"], Enabled: app.storage.customEmails.PasswordReset.Enabled},
|
||||||
@ -35,25 +31,13 @@ func (app *appContext) GetCustomContent(gc *gin.Context) {
|
|||||||
"WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.customEmails.WelcomeEmail.Enabled},
|
"WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.customEmails.WelcomeEmail.Enabled},
|
||||||
"EmailConfirmation": {Name: app.storage.lang.Email[lang].EmailConfirmation["name"], Enabled: app.storage.customEmails.EmailConfirmation.Enabled},
|
"EmailConfirmation": {Name: app.storage.lang.Email[lang].EmailConfirmation["name"], Enabled: app.storage.customEmails.EmailConfirmation.Enabled},
|
||||||
"UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.customEmails.UserExpired.Enabled},
|
"UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.customEmails.UserExpired.Enabled},
|
||||||
"UserLogin": {Name: app.storage.lang.Admin[adminLang].Strings["userPageLogin"], Enabled: app.storage.userPage.Login.Enabled},
|
})
|
||||||
"UserPage": {Name: app.storage.lang.Admin[adminLang].Strings["userPagePage"], Enabled: app.storage.userPage.Page.Enabled},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
filter := gc.Query("filter")
|
func (app *appContext) getCustomEmail(id string) *customEmail {
|
||||||
if filter == "user" {
|
|
||||||
list = emailListDTO{"UserLogin": list["UserLogin"], "UserPage": list["UserPage"]}
|
|
||||||
} else {
|
|
||||||
delete(list, "UserLogin")
|
|
||||||
delete(list, "UserPage")
|
|
||||||
}
|
|
||||||
|
|
||||||
gc.JSON(200, list)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *appContext) getCustomMessage(id string) *customContent {
|
|
||||||
switch id {
|
switch id {
|
||||||
case "Announcement":
|
case "Announcement":
|
||||||
return &customContent{}
|
return &customEmail{}
|
||||||
case "UserCreated":
|
case "UserCreated":
|
||||||
return &app.storage.customEmails.UserCreated
|
return &app.storage.customEmails.UserCreated
|
||||||
case "InviteExpiry":
|
case "InviteExpiry":
|
||||||
@ -74,17 +58,13 @@ func (app *appContext) getCustomMessage(id string) *customContent {
|
|||||||
return &app.storage.customEmails.EmailConfirmation
|
return &app.storage.customEmails.EmailConfirmation
|
||||||
case "UserExpired":
|
case "UserExpired":
|
||||||
return &app.storage.customEmails.UserExpired
|
return &app.storage.customEmails.UserExpired
|
||||||
case "UserLogin":
|
|
||||||
return &app.storage.userPage.Login
|
|
||||||
case "UserPage":
|
|
||||||
return &app.storage.userPage.Page
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Sets the corresponding custom email.
|
// @Summary Sets the corresponding custom email.
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param customEmails body customEmails true "Content = email (in markdown)."
|
// @Param customEmail body customEmail true "Content = email (in markdown)."
|
||||||
// @Success 200 {object} boolResponse
|
// @Success 200 {object} boolResponse
|
||||||
// @Failure 400 {object} boolResponse
|
// @Failure 400 {object} boolResponse
|
||||||
// @Failure 500 {object} boolResponse
|
// @Failure 500 {object} boolResponse
|
||||||
@ -92,29 +72,25 @@ func (app *appContext) getCustomMessage(id string) *customContent {
|
|||||||
// @Router /config/emails/{id} [post]
|
// @Router /config/emails/{id} [post]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Configuration
|
// @tags Configuration
|
||||||
func (app *appContext) SetCustomMessage(gc *gin.Context) {
|
func (app *appContext) SetCustomEmail(gc *gin.Context) {
|
||||||
var req customContent
|
var req customEmail
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
id := gc.Param("id")
|
id := gc.Param("id")
|
||||||
if req.Content == "" {
|
if req.Content == "" {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
message := app.getCustomMessage(id)
|
email := app.getCustomEmail(id)
|
||||||
if message == nil {
|
if email == nil {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
message.Content = req.Content
|
email.Content = req.Content
|
||||||
message.Enabled = true
|
email.Enabled = true
|
||||||
if app.storage.storeCustomEmails() != nil {
|
if app.storage.storeCustomEmails() != nil {
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if app.storage.storeUserPageContent() != nil {
|
|
||||||
respondBool(500, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,7 +104,7 @@ func (app *appContext) SetCustomMessage(gc *gin.Context) {
|
|||||||
// @Router /config/emails/{id}/state/{enable/disable} [post]
|
// @Router /config/emails/{id}/state/{enable/disable} [post]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Configuration
|
// @tags Configuration
|
||||||
func (app *appContext) SetCustomMessageState(gc *gin.Context) {
|
func (app *appContext) SetCustomEmailState(gc *gin.Context) {
|
||||||
id := gc.Param("id")
|
id := gc.Param("id")
|
||||||
s := gc.Param("state")
|
s := gc.Param("state")
|
||||||
enabled := false
|
enabled := false
|
||||||
@ -137,24 +113,20 @@ func (app *appContext) SetCustomMessageState(gc *gin.Context) {
|
|||||||
} else if s != "disable" {
|
} else if s != "disable" {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
}
|
}
|
||||||
message := app.getCustomMessage(id)
|
email := app.getCustomEmail(id)
|
||||||
if message == nil {
|
if email == nil {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
message.Enabled = enabled
|
email.Enabled = enabled
|
||||||
if app.storage.storeCustomEmails() != nil {
|
if app.storage.storeCustomEmails() != nil {
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if app.storage.storeUserPageContent() != nil {
|
|
||||||
respondBool(500, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Returns the custom email/message (generating it if not set) and list of used variables in it.
|
// @Summary Returns the custom email (generating it if not set) and list of used variables in it.
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} customEmailDTO
|
// @Success 200 {object} customEmailDTO
|
||||||
// @Failure 400 {object} boolResponse
|
// @Failure 400 {object} boolResponse
|
||||||
@ -163,7 +135,7 @@ func (app *appContext) SetCustomMessageState(gc *gin.Context) {
|
|||||||
// @Router /config/emails/{id} [get]
|
// @Router /config/emails/{id} [get]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Configuration
|
// @tags Configuration
|
||||||
func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
|
func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
|
||||||
lang := app.storage.lang.chosenEmailLang
|
lang := app.storage.lang.chosenEmailLang
|
||||||
id := gc.Param("id")
|
id := gc.Param("id")
|
||||||
var content string
|
var content string
|
||||||
@ -174,26 +146,20 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
|
|||||||
var values map[string]interface{}
|
var values map[string]interface{}
|
||||||
username := app.storage.lang.Email[lang].Strings.get("username")
|
username := app.storage.lang.Email[lang].Strings.get("username")
|
||||||
emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress")
|
emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress")
|
||||||
customMessage := app.getCustomMessage(id)
|
email := app.getCustomEmail(id)
|
||||||
if customMessage == nil {
|
if email == nil {
|
||||||
app.err.Printf("Failed to get custom message with ID \"%s\"", id)
|
app.err.Printf("Failed to get custom email with ID \"%s\"", id)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if id == "WelcomeEmail" {
|
if id == "WelcomeEmail" {
|
||||||
conditionals = []string{"{yourAccountWillExpire}"}
|
conditionals = []string{"{yourAccountWillExpire}"}
|
||||||
customMessage.Conditionals = conditionals
|
email.Conditionals = conditionals
|
||||||
} else if id == "UserPage" {
|
|
||||||
variables = []string{"{username}"}
|
|
||||||
customMessage.Variables = variables
|
|
||||||
} else if id == "UserLogin" {
|
|
||||||
variables = []string{}
|
|
||||||
customMessage.Variables = variables
|
|
||||||
}
|
}
|
||||||
content = customMessage.Content
|
content = email.Content
|
||||||
noContent := content == ""
|
noContent := content == ""
|
||||||
if !noContent {
|
if !noContent {
|
||||||
variables = customMessage.Variables
|
variables = email.Variables
|
||||||
}
|
}
|
||||||
switch id {
|
switch id {
|
||||||
case "Announcement":
|
case "Announcement":
|
||||||
@ -249,14 +215,12 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
|
|||||||
msg, err = app.email.constructUserExpired(app, true)
|
msg, err = app.email.constructUserExpired(app, true)
|
||||||
}
|
}
|
||||||
values = app.email.userExpiredValues(app, false)
|
values = app.email.userExpiredValues(app, false)
|
||||||
case "UserLogin", "UserPage":
|
|
||||||
values = map[string]interface{}{}
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if noContent && id != "Announcement" && id != "UserPage" && id != "UserLogin" {
|
if noContent && id != "Announcement" {
|
||||||
content = msg.Text
|
content = msg.Text
|
||||||
variables = make([]string, strings.Count(content, "{"))
|
variables = make([]string, strings.Count(content, "{"))
|
||||||
i := 0
|
i := 0
|
||||||
@ -275,7 +239,7 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customMessage.Variables = variables
|
email.Variables = variables
|
||||||
}
|
}
|
||||||
if variables == nil {
|
if variables == nil {
|
||||||
variables = []string{}
|
variables = []string{}
|
||||||
@ -284,22 +248,11 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
|
|||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if app.storage.storeUserPageContent() != nil {
|
mail, err := app.email.constructTemplate("", "<div class=\"preview-content\"></div>", app)
|
||||||
respondBool(500, false, gc)
|
|
||||||
}
|
|
||||||
var mail *Message
|
|
||||||
if id != "UserLogin" && id != "UserPage" {
|
|
||||||
mail, err = app.email.constructTemplate("", "<div class=\"preview-content\"></div>", app)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
mail = &Message{
|
|
||||||
HTML: "<div class=\"card ~neutral dark:~d_neutral @low preview-content\"></div>",
|
|
||||||
Markdown: "<div class=\"card ~neutral dark:~d_neutral @low preview-content\"></div>",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gc.JSON(200, customEmailDTO{Content: content, Variables: variables, Conditionals: conditionals, Values: values, HTML: mail.HTML, Plaintext: mail.Text})
|
gc.JSON(200, customEmailDTO{Content: content, Variables: variables, Conditionals: conditionals, Values: values, HTML: mail.HTML, Plaintext: mail.Text})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,12 +285,18 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
|
|||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tgToken, ok := app.telegram.TokenVerified(req.Token)
|
tokenIndex := -1
|
||||||
app.telegram.DeleteVerifiedToken(req.Token)
|
for i, v := range app.telegram.verifiedTokens {
|
||||||
if !ok {
|
if v.Token == req.Token {
|
||||||
|
tokenIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tokenIndex == -1 {
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
tgToken := app.telegram.verifiedTokens[tokenIndex]
|
||||||
tgUser := TelegramUser{
|
tgUser := TelegramUser{
|
||||||
ChatID: tgToken.ChatID,
|
ChatID: tgToken.ChatID,
|
||||||
Username: tgToken.Username,
|
Username: tgToken.Username,
|
||||||
@ -346,7 +305,17 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
|
|||||||
if lang, ok := app.telegram.languages[tgToken.ChatID]; ok {
|
if lang, ok := app.telegram.languages[tgToken.ChatID]; ok {
|
||||||
tgUser.Lang = lang
|
tgUser.Lang = lang
|
||||||
}
|
}
|
||||||
app.storage.SetTelegramKey(req.ID, tgUser)
|
if app.storage.telegram == nil {
|
||||||
|
app.storage.telegram = map[string]TelegramUser{}
|
||||||
|
}
|
||||||
|
app.storage.telegram[req.ID] = tgUser
|
||||||
|
err := app.storage.storeTelegramUsers()
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("Failed to store Telegram users: %v", err)
|
||||||
|
} else {
|
||||||
|
app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1]
|
||||||
|
app.telegram.verifiedTokens = app.telegram.verifiedTokens[:len(app.telegram.verifiedTokens)-1]
|
||||||
|
}
|
||||||
linkExistingOmbiDiscordTelegram(app)
|
linkExistingOmbiDiscordTelegram(app)
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
}
|
}
|
||||||
@ -367,14 +336,15 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
|
|||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.setContactMethods(req, gc)
|
if tgUser, ok := app.storage.telegram[req.ID]; ok {
|
||||||
}
|
|
||||||
|
|
||||||
func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Context) {
|
|
||||||
if tgUser, ok := app.storage.GetTelegramKey(req.ID); ok {
|
|
||||||
change := tgUser.Contact != req.Telegram
|
change := tgUser.Contact != req.Telegram
|
||||||
tgUser.Contact = req.Telegram
|
tgUser.Contact = req.Telegram
|
||||||
app.storage.SetTelegramKey(req.ID, tgUser)
|
app.storage.telegram[req.ID] = tgUser
|
||||||
|
if err := app.storage.storeTelegramUsers(); err != nil {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
app.err.Printf("Telegram: Failed to store users: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
if change {
|
if change {
|
||||||
msg := ""
|
msg := ""
|
||||||
if !req.Telegram {
|
if !req.Telegram {
|
||||||
@ -383,10 +353,10 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
|||||||
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
|
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if dcUser, ok := app.storage.GetDiscordKey(req.ID); ok {
|
if dcUser, ok := app.storage.discord[req.ID]; ok {
|
||||||
change := dcUser.Contact != req.Discord
|
change := dcUser.Contact != req.Discord
|
||||||
dcUser.Contact = req.Discord
|
dcUser.Contact = req.Discord
|
||||||
app.storage.SetDiscordKey(req.ID, dcUser)
|
app.storage.discord[req.ID] = dcUser
|
||||||
if err := app.storage.storeDiscordUsers(); err != nil {
|
if err := app.storage.storeDiscordUsers(); err != nil {
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
app.err.Printf("Discord: Failed to store users: %v", err)
|
app.err.Printf("Discord: Failed to store users: %v", err)
|
||||||
@ -400,10 +370,10 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
|||||||
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
|
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if mxUser, ok := app.storage.GetMatrixKey(req.ID); ok {
|
if mxUser, ok := app.storage.matrix[req.ID]; ok {
|
||||||
change := mxUser.Contact != req.Matrix
|
change := mxUser.Contact != req.Matrix
|
||||||
mxUser.Contact = req.Matrix
|
mxUser.Contact = req.Matrix
|
||||||
app.storage.SetMatrixKey(req.ID, mxUser)
|
app.storage.matrix[req.ID] = mxUser
|
||||||
if err := app.storage.storeMatrixUsers(); err != nil {
|
if err := app.storage.storeMatrixUsers(); err != nil {
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
app.err.Printf("Matrix: Failed to store users: %v", err)
|
app.err.Printf("Matrix: Failed to store users: %v", err)
|
||||||
@ -417,10 +387,10 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
|||||||
app.debug.Printf("Matrix: User \"%s\" will%s be notified through Matrix.", mxUser.UserID, msg)
|
app.debug.Printf("Matrix: User \"%s\" will%s be notified through Matrix.", mxUser.UserID, msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if email, ok := app.storage.GetEmailsKey(req.ID); ok {
|
if email, ok := app.storage.emails[req.ID]; ok {
|
||||||
change := email.Contact != req.Email
|
change := email.Contact != req.Email
|
||||||
email.Contact = req.Email
|
email.Contact = req.Email
|
||||||
app.storage.SetEmailsKey(req.ID, email)
|
app.storage.emails[req.ID] = email
|
||||||
if err := app.storage.storeEmails(); err != nil {
|
if err := app.storage.storeEmails(); err != nil {
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
app.err.Printf("Failed to store emails: %v", err)
|
app.err.Printf("Failed to store emails: %v", err)
|
||||||
@ -446,8 +416,19 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
|||||||
// @tags Other
|
// @tags Other
|
||||||
func (app *appContext) TelegramVerified(gc *gin.Context) {
|
func (app *appContext) TelegramVerified(gc *gin.Context) {
|
||||||
pin := gc.Param("pin")
|
pin := gc.Param("pin")
|
||||||
_, ok := app.telegram.TokenVerified(pin)
|
tokenIndex := -1
|
||||||
respondBool(200, ok, gc)
|
for i, v := range app.telegram.verifiedTokens {
|
||||||
|
if v.Token == pin {
|
||||||
|
tokenIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if tokenIndex != -1 {
|
||||||
|
// length := len(app.telegram.verifiedTokens)
|
||||||
|
// app.telegram.verifiedTokens[length-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[length-1]
|
||||||
|
// app.telegram.verifiedTokens = app.telegram.verifiedTokens[:length-1]
|
||||||
|
// }
|
||||||
|
respondBool(200, tokenIndex != -1, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires invite code.
|
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires invite code.
|
||||||
@ -460,18 +441,32 @@ func (app *appContext) TelegramVerified(gc *gin.Context) {
|
|||||||
// @tags Other
|
// @tags Other
|
||||||
func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
|
func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
|
||||||
code := gc.Param("invCode")
|
code := gc.Param("invCode")
|
||||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
if _, ok := app.storage.invites[code]; !ok {
|
||||||
respondBool(401, false, gc)
|
respondBool(401, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pin := gc.Param("pin")
|
pin := gc.Param("pin")
|
||||||
token, ok := app.telegram.TokenVerified(pin)
|
tokenIndex := -1
|
||||||
if ok && app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(token.Username) {
|
for i, v := range app.telegram.verifiedTokens {
|
||||||
app.discord.DeleteVerifiedUser(pin)
|
if v.Token == pin {
|
||||||
|
tokenIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if app.config.Section("telegram").Key("require_unique").MustBool(false) {
|
||||||
|
for _, u := range app.storage.telegram {
|
||||||
|
if app.telegram.verifiedTokens[tokenIndex].Username == u.Username {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
respondBool(200, ok, gc)
|
}
|
||||||
|
}
|
||||||
|
// if tokenIndex != -1 {
|
||||||
|
// length := len(app.telegram.verifiedTokens)
|
||||||
|
// app.telegram.verifiedTokens[length-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[length-1]
|
||||||
|
// app.telegram.verifiedTokens = app.telegram.verifiedTokens[:length-1]
|
||||||
|
// }
|
||||||
|
respondBool(200, tokenIndex != -1, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code.
|
// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code.
|
||||||
@ -484,17 +479,21 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
|
|||||||
// @tags Other
|
// @tags Other
|
||||||
func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
|
func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
|
||||||
code := gc.Param("invCode")
|
code := gc.Param("invCode")
|
||||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
if _, ok := app.storage.invites[code]; !ok {
|
||||||
respondBool(401, false, gc)
|
respondBool(401, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pin := gc.Param("pin")
|
pin := gc.Param("pin")
|
||||||
user, ok := app.discord.UserVerified(pin)
|
_, ok := app.discord.verifiedTokens[pin]
|
||||||
if ok && app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(user.ID) {
|
if app.config.Section("discord").Key("require_unique").MustBool(false) {
|
||||||
|
for _, u := range app.storage.discord {
|
||||||
|
if app.discord.verifiedTokens[pin].ID == u.ID {
|
||||||
delete(app.discord.verifiedTokens, pin)
|
delete(app.discord.verifiedTokens, pin)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
respondBool(200, ok, gc)
|
respondBool(200, ok, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -513,7 +512,7 @@ func (app *appContext) DiscordServerInvite(gc *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
code := gc.Param("invCode")
|
code := gc.Param("invCode")
|
||||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
if _, ok := app.storage.invites[code]; !ok {
|
||||||
respondBool(401, false, gc)
|
respondBool(401, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -537,7 +536,7 @@ func (app *appContext) DiscordServerInvite(gc *gin.Context) {
|
|||||||
// @tags Other
|
// @tags Other
|
||||||
func (app *appContext) MatrixSendPIN(gc *gin.Context) {
|
func (app *appContext) MatrixSendPIN(gc *gin.Context) {
|
||||||
code := gc.Param("invCode")
|
code := gc.Param("invCode")
|
||||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
if _, ok := app.storage.invites[code]; !ok {
|
||||||
respondBool(401, false, gc)
|
respondBool(401, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -548,7 +547,7 @@ func (app *appContext) MatrixSendPIN(gc *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if app.config.Section("matrix").Key("require_unique").MustBool(false) {
|
if app.config.Section("matrix").Key("require_unique").MustBool(false) {
|
||||||
for _, u := range app.storage.GetMatrix() {
|
for _, u := range app.storage.matrix {
|
||||||
if req.UserID == u.UserID {
|
if req.UserID == u.UserID {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
@ -575,7 +574,7 @@ func (app *appContext) MatrixSendPIN(gc *gin.Context) {
|
|||||||
// @tags Other
|
// @tags Other
|
||||||
func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
|
func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
|
||||||
code := gc.Param("invCode")
|
code := gc.Param("invCode")
|
||||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
if _, ok := app.storage.invites[code]; !ok {
|
||||||
app.debug.Println("Matrix: Invite code was invalid")
|
app.debug.Println("Matrix: Invite code was invalid")
|
||||||
respondBool(401, false, gc)
|
respondBool(401, false, gc)
|
||||||
return
|
return
|
||||||
@ -645,8 +644,8 @@ func (app *appContext) MatrixLogin(gc *gin.Context) {
|
|||||||
func (app *appContext) MatrixConnect(gc *gin.Context) {
|
func (app *appContext) MatrixConnect(gc *gin.Context) {
|
||||||
var req MatrixConnectUserDTO
|
var req MatrixConnectUserDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
if app.storage.GetMatrix() == nil {
|
if app.storage.matrix == nil {
|
||||||
app.storage.matrix = matrixStore{}
|
app.storage.matrix = map[string]MatrixUser{}
|
||||||
}
|
}
|
||||||
roomID, encrypted, err := app.matrix.CreateRoom(req.UserID)
|
roomID, encrypted, err := app.matrix.CreateRoom(req.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -654,13 +653,13 @@ func (app *appContext) MatrixConnect(gc *gin.Context) {
|
|||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.storage.SetMatrixKey(req.JellyfinID, MatrixUser{
|
app.storage.matrix[req.JellyfinID] = MatrixUser{
|
||||||
UserID: req.UserID,
|
UserID: req.UserID,
|
||||||
RoomID: string(roomID),
|
RoomID: string(roomID),
|
||||||
Lang: "en-us",
|
Lang: "en-us",
|
||||||
Contact: true,
|
Contact: true,
|
||||||
Encrypted: encrypted,
|
Encrypted: encrypted,
|
||||||
})
|
}
|
||||||
app.matrix.isEncrypted[roomID] = encrypted
|
app.matrix.isEncrypted[roomID] = encrypted
|
||||||
if err := app.storage.storeMatrixUsers(); err != nil {
|
if err := app.storage.storeMatrixUsers(); err != nil {
|
||||||
app.err.Printf("Failed to store Matrix users: %v", err)
|
app.err.Printf("Failed to store Matrix users: %v", err)
|
||||||
@ -716,7 +715,7 @@ func (app *appContext) DiscordConnect(gc *gin.Context) {
|
|||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.storage.SetDiscordKey(req.JellyfinID, user)
|
app.storage.discord[req.JellyfinID] = user
|
||||||
if err := app.storage.storeDiscordUsers(); err != nil {
|
if err := app.storage.storeDiscordUsers(); err != nil {
|
||||||
app.err.Printf("Failed to store Discord users: %v", err)
|
app.err.Printf("Failed to store Discord users: %v", err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
@ -740,7 +739,8 @@ func (app *appContext) UnlinkDiscord(gc *gin.Context) {
|
|||||||
respond(400, "User not found", gc)
|
respond(400, "User not found", gc)
|
||||||
return
|
return
|
||||||
} */
|
} */
|
||||||
app.storage.DeleteDiscordKey(req.ID)
|
delete(app.storage.discord, req.ID)
|
||||||
|
app.storage.storeDiscordUsers()
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -758,7 +758,8 @@ func (app *appContext) UnlinkTelegram(gc *gin.Context) {
|
|||||||
respond(400, "User not found", gc)
|
respond(400, "User not found", gc)
|
||||||
return
|
return
|
||||||
} */
|
} */
|
||||||
app.storage.DeleteTelegramKey(req.ID)
|
delete(app.storage.telegram, req.ID)
|
||||||
|
app.storage.storeTelegramUsers()
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -776,6 +777,7 @@ func (app *appContext) UnlinkMatrix(gc *gin.Context) {
|
|||||||
respond(400, "User not found", gc)
|
respond(400, "User not found", gc)
|
||||||
return
|
return
|
||||||
} */
|
} */
|
||||||
app.storage.DeleteMatrixKey(req.ID)
|
delete(app.storage.matrix, req.ID)
|
||||||
|
app.storage.storeMatrixUsers()
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, er
|
|||||||
}
|
}
|
||||||
username := jfUser.Name
|
username := jfUser.Name
|
||||||
email := ""
|
email := ""
|
||||||
if e, ok := app.storage.GetEmailsKey(jfID); ok {
|
if e, ok := app.storage.emails[jfID]; ok {
|
||||||
email = e.Addr
|
email = e.Addr
|
||||||
}
|
}
|
||||||
for _, ombiUser := range ombiUsers {
|
for _, ombiUser := range ombiUsers {
|
||||||
|
629
api-userpage.go
629
api-userpage.go
@ -1,629 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/golang-jwt/jwt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// @Summary Returns the logged-in user's Jellyfin ID & Username, and other details.
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} MyDetailsDTO
|
|
||||||
// @Router /my/details [get]
|
|
||||||
// @tags User Page
|
|
||||||
func (app *appContext) MyDetails(gc *gin.Context) {
|
|
||||||
resp := MyDetailsDTO{
|
|
||||||
Id: gc.GetString("jfId"),
|
|
||||||
}
|
|
||||||
|
|
||||||
user, status, err := app.jf.UserByID(resp.Id, false)
|
|
||||||
if status != 200 || err != nil {
|
|
||||||
app.err.Printf("Failed to get Jellyfin user (%d): %+v\n", status, err)
|
|
||||||
respond(500, "Failed to get user", gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp.Username = user.Name
|
|
||||||
resp.Admin = user.Policy.IsAdministrator
|
|
||||||
resp.AccountsAdmin = false
|
|
||||||
if !app.config.Section("ui").Key("allow_all").MustBool(false) {
|
|
||||||
adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
|
|
||||||
if emailStore, ok := app.storage.GetEmailsKey(resp.Id); ok {
|
|
||||||
resp.AccountsAdmin = emailStore.Admin
|
|
||||||
}
|
|
||||||
resp.AccountsAdmin = resp.AccountsAdmin || (adminOnly && resp.Admin)
|
|
||||||
}
|
|
||||||
resp.Disabled = user.Policy.IsDisabled
|
|
||||||
|
|
||||||
if exp, ok := app.storage.users[user.ID]; ok {
|
|
||||||
resp.Expiry = exp.Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.storage.loadEmails()
|
|
||||||
app.storage.loadDiscordUsers()
|
|
||||||
app.storage.loadMatrixUsers()
|
|
||||||
app.storage.loadTelegramUsers()
|
|
||||||
|
|
||||||
if emailEnabled {
|
|
||||||
resp.Email = &MyDetailsContactMethodsDTO{}
|
|
||||||
if email, ok := app.storage.GetEmailsKey(user.ID); ok && email.Addr != "" {
|
|
||||||
resp.Email.Value = email.Addr
|
|
||||||
resp.Email.Enabled = email.Contact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if discordEnabled {
|
|
||||||
resp.Discord = &MyDetailsContactMethodsDTO{}
|
|
||||||
if discord, ok := app.storage.GetDiscordKey(user.ID); ok {
|
|
||||||
resp.Discord.Value = RenderDiscordUsername(discord)
|
|
||||||
resp.Discord.Enabled = discord.Contact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if telegramEnabled {
|
|
||||||
resp.Telegram = &MyDetailsContactMethodsDTO{}
|
|
||||||
if telegram, ok := app.storage.GetTelegramKey(user.ID); ok {
|
|
||||||
resp.Telegram.Value = telegram.Username
|
|
||||||
resp.Telegram.Enabled = telegram.Contact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if matrixEnabled {
|
|
||||||
resp.Matrix = &MyDetailsContactMethodsDTO{}
|
|
||||||
if matrix, ok := app.storage.GetMatrixKey(user.ID); ok {
|
|
||||||
resp.Matrix.Value = matrix.UserID
|
|
||||||
resp.Matrix.Enabled = matrix.Contact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gc.JSON(200, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Sets whether to notify yourself through telegram/discord/matrix/email or not.
|
|
||||||
// @Produce json
|
|
||||||
// @Param SetContactMethodsDTO body SetContactMethodsDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
|
|
||||||
// @Success 200 {object} boolResponse
|
|
||||||
// @Success 400 {object} boolResponse
|
|
||||||
// @Success 500 {object} boolResponse
|
|
||||||
// @Router /my/contact [post]
|
|
||||||
// @Security Bearer
|
|
||||||
// @tags User Page
|
|
||||||
func (app *appContext) SetMyContactMethods(gc *gin.Context) {
|
|
||||||
var req SetContactMethodsDTO
|
|
||||||
gc.BindJSON(&req)
|
|
||||||
req.ID = gc.GetString("jfId")
|
|
||||||
if req.ID == "" {
|
|
||||||
respondBool(400, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
app.setContactMethods(req, gc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Logout by deleting refresh token from cookies.
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} boolResponse
|
|
||||||
// @Failure 500 {object} stringResponse
|
|
||||||
// @Router /my/logout [post]
|
|
||||||
// @Security Bearer
|
|
||||||
// @tags User Page
|
|
||||||
func (app *appContext) LogoutUser(gc *gin.Context) {
|
|
||||||
cookie, err := gc.Cookie("user-refresh")
|
|
||||||
if err != nil {
|
|
||||||
app.debug.Printf("Couldn't get cookies: %s", err)
|
|
||||||
respond(500, "Couldn't fetch cookies", gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
app.invalidTokens = append(app.invalidTokens, cookie)
|
|
||||||
gc.SetCookie("refresh", "invalid", -1, "/my", gc.Request.URL.Hostname(), true, true)
|
|
||||||
respondBool(200, true, gc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary confirm an action (e.g. changing an email address.)
|
|
||||||
// @Produce json
|
|
||||||
// @Param jwt path string true "jwt confirmation code"
|
|
||||||
// @Router /my/confirm/{jwt} [post]
|
|
||||||
// @Success 200 {object} boolResponse
|
|
||||||
// @Failure 400 {object} stringResponse
|
|
||||||
// @Failure 404
|
|
||||||
// @Success 303
|
|
||||||
// @Failure 500 {object} stringResponse
|
|
||||||
// @tags User Page
|
|
||||||
func (app *appContext) ConfirmMyAction(gc *gin.Context) {
|
|
||||||
app.confirmMyAction(gc, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
|
||||||
var claims jwt.MapClaims
|
|
||||||
var target ConfirmationTarget
|
|
||||||
var id string
|
|
||||||
fail := func() {
|
|
||||||
gcHTML(gc, 404, "404.html", gin.H{
|
|
||||||
"cssClass": app.cssClass,
|
|
||||||
"cssVersion": cssVersion,
|
|
||||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate key
|
|
||||||
if key == "" {
|
|
||||||
key = gc.Param("jwt")
|
|
||||||
}
|
|
||||||
token, err := jwt.Parse(key, checkToken)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf("Failed to parse key: %s", err)
|
|
||||||
fail()
|
|
||||||
// respond(500, "unknownError", gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
claims, ok := token.Claims.(jwt.MapClaims)
|
|
||||||
if !ok {
|
|
||||||
app.err.Printf("Failed to parse key: %s", err)
|
|
||||||
fail()
|
|
||||||
// respond(500, "unknownError", gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
expiry := time.Unix(int64(claims["exp"].(float64)), 0)
|
|
||||||
if !(ok && token.Valid && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
|
|
||||||
app.err.Printf("Invalid key")
|
|
||||||
fail()
|
|
||||||
// respond(400, "invalidKey", gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
target = ConfirmationTarget(int(claims["target"].(float64)))
|
|
||||||
id = claims["id"].(string)
|
|
||||||
|
|
||||||
// Perform an Action
|
|
||||||
if target == NoOp {
|
|
||||||
gc.Redirect(http.StatusSeeOther, "/my/account")
|
|
||||||
return
|
|
||||||
} else if target == UserEmailChange {
|
|
||||||
emailStore, ok := app.storage.GetEmailsKey(id)
|
|
||||||
if !ok {
|
|
||||||
emailStore = EmailAddress{
|
|
||||||
Contact: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emailStore.Addr = claims["email"].(string)
|
|
||||||
app.storage.SetEmailsKey(id, emailStore)
|
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
|
||||||
ombiUser, code, err := app.getOmbiUser(id)
|
|
||||||
if code == 200 && err == nil {
|
|
||||||
ombiUser["emailAddress"] = claims["email"].(string)
|
|
||||||
code, err = app.ombi.ModifyUser(ombiUser)
|
|
||||||
if code != 200 || err != nil {
|
|
||||||
app.err.Printf("%s: Failed to change ombi email address (%d): %v", ombiUser["userName"].(string), code, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.storage.storeEmails()
|
|
||||||
app.info.Println("Email list modified")
|
|
||||||
gc.Redirect(http.StatusSeeOther, "/my/account")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Modify your email address.
|
|
||||||
// @Produce json
|
|
||||||
// @Param ModifyMyEmailDTO body ModifyMyEmailDTO true "New email address."
|
|
||||||
// @Success 200 {object} boolResponse
|
|
||||||
// @Failure 400 {object} stringResponse
|
|
||||||
// @Failure 401 {object} stringResponse
|
|
||||||
// @Failure 500 {object} stringResponse
|
|
||||||
// @Router /my/email [post]
|
|
||||||
// @Security Bearer
|
|
||||||
// @tags User Page
|
|
||||||
func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
|
||||||
var req ModifyMyEmailDTO
|
|
||||||
gc.BindJSON(&req)
|
|
||||||
app.debug.Println("Email modification requested")
|
|
||||||
if !strings.ContainsRune(req.Email, '@') {
|
|
||||||
respond(400, "Invalid Email Address", gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
id := gc.GetString("jfId")
|
|
||||||
|
|
||||||
// We'll use the ConfirmMyAction route to do the work, even if we don't need to confirm the address.
|
|
||||||
claims := jwt.MapClaims{
|
|
||||||
"valid": true,
|
|
||||||
"id": id,
|
|
||||||
"email": req.Email,
|
|
||||||
"type": "confirmation",
|
|
||||||
"target": UserEmailChange,
|
|
||||||
"exp": time.Now().Add(time.Hour).Unix(),
|
|
||||||
}
|
|
||||||
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
||||||
key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf("Failed to generate confirmation token: %v", err)
|
|
||||||
respond(500, "errorUnknown", gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
|
|
||||||
user, status, err := app.jf.UserByID(id, false)
|
|
||||||
name := ""
|
|
||||||
if status == 200 && err == nil {
|
|
||||||
name = user.Name
|
|
||||||
}
|
|
||||||
app.debug.Printf("%s: Email confirmation required", id)
|
|
||||||
respond(401, "confirmEmail", gc)
|
|
||||||
msg, err := app.email.constructConfirmation("", name, key, app, false)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf("%s: Failed to construct confirmation email: %v", name, err)
|
|
||||||
} else if err := app.email.send(msg, req.Email); err != nil {
|
|
||||||
app.err.Printf("%s: Failed to send user confirmation email: %v", name, err)
|
|
||||||
} else {
|
|
||||||
app.info.Printf("%s: Sent user confirmation email to \"%s\"", name, req.Email)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
app.confirmMyAction(gc, key)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Returns a 10-minute, one-use Discord server invite
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} DiscordInviteDTO
|
|
||||||
// @Failure 400 {object} boolResponse
|
|
||||||
// @Failure 401 {object} boolResponse
|
|
||||||
// @Failure 500 {object} boolResponse
|
|
||||||
// @Param invCode path string true "invite Code"
|
|
||||||
// @Router /my/discord/invite [get]
|
|
||||||
// @Security Bearer
|
|
||||||
// @tags User Page
|
|
||||||
func (app *appContext) MyDiscordServerInvite(gc *gin.Context) {
|
|
||||||
if app.discord.inviteChannelName == "" {
|
|
||||||
respondBool(400, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
invURL, iconURL := app.discord.NewTempInvite(10*60, 1)
|
|
||||||
if invURL == "" {
|
|
||||||
respondBool(500, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
gc.JSON(200, DiscordInviteDTO{invURL, iconURL})
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Returns a linking PIN for discord/telegram
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} GetMyPINDTO
|
|
||||||
// @Failure 400 {object} stringResponse
|
|
||||||
// Param service path string true "discord/telegram"
|
|
||||||
// @Router /my/pin/{service} [get]
|
|
||||||
// @Security Bearer
|
|
||||||
// @tags User Page
|
|
||||||
func (app *appContext) GetMyPIN(gc *gin.Context) {
|
|
||||||
service := gc.Param("service")
|
|
||||||
resp := GetMyPINDTO{}
|
|
||||||
switch service {
|
|
||||||
case "discord":
|
|
||||||
resp.PIN = app.discord.NewAssignedAuthToken(gc.GetString("jfId"))
|
|
||||||
break
|
|
||||||
case "telegram":
|
|
||||||
resp.PIN = app.telegram.NewAssignedAuthToken(gc.GetString("jfId"))
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
respond(400, "invalid service", gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
gc.JSON(200, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Returns true/false on whether or not your discord PIN was verified, and assigns the discord user to you.
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} boolResponse
|
|
||||||
// @Failure 401 {object} boolResponse
|
|
||||||
// @Param pin path string true "PIN code to check"
|
|
||||||
// @Router /my/discord/verified/{pin} [get]
|
|
||||||
// @Security Bearer
|
|
||||||
// @tags User Page
|
|
||||||
func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
|
|
||||||
pin := gc.Param("pin")
|
|
||||||
dcUser, ok := app.discord.AssignedUserVerified(pin, gc.GetString("jfId"))
|
|
||||||
app.discord.DeleteVerifiedUser(pin)
|
|
||||||
if !ok {
|
|
||||||
respondBool(200, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(dcUser.ID) {
|
|
||||||
respondBool(400, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
existingUser, ok := app.storage.GetDiscordKey(gc.GetString("jfId"))
|
|
||||||
if ok {
|
|
||||||
dcUser.Lang = existingUser.Lang
|
|
||||||
dcUser.Contact = existingUser.Contact
|
|
||||||
}
|
|
||||||
app.storage.SetDiscordKey(gc.GetString("jfId"), dcUser)
|
|
||||||
respondBool(200, true, gc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Returns true/false on whether or not your telegram PIN was verified, and assigns the telegram user to you.
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} boolResponse
|
|
||||||
// @Failure 401 {object} boolResponse
|
|
||||||
// @Param pin path string true "PIN code to check"
|
|
||||||
// @Router /my/telegram/verified/{pin} [get]
|
|
||||||
// @Security Bearer
|
|
||||||
// @tags User Page
|
|
||||||
func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
|
|
||||||
pin := gc.Param("pin")
|
|
||||||
token, ok := app.telegram.AssignedTokenVerified(pin, gc.GetString("jfId"))
|
|
||||||
app.telegram.DeleteVerifiedToken(pin)
|
|
||||||
if !ok {
|
|
||||||
respondBool(200, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(token.Username) {
|
|
||||||
respondBool(400, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tgUser := TelegramUser{
|
|
||||||
ChatID: token.ChatID,
|
|
||||||
Username: token.Username,
|
|
||||||
Contact: true,
|
|
||||||
}
|
|
||||||
if lang, ok := app.telegram.languages[tgUser.ChatID]; ok {
|
|
||||||
tgUser.Lang = lang
|
|
||||||
}
|
|
||||||
|
|
||||||
existingUser, ok := app.storage.GetTelegramKey(gc.GetString("jfId"))
|
|
||||||
if ok {
|
|
||||||
tgUser.Lang = existingUser.Lang
|
|
||||||
tgUser.Contact = existingUser.Contact
|
|
||||||
}
|
|
||||||
app.storage.SetTelegramKey(gc.GetString("jfId"), tgUser)
|
|
||||||
respondBool(200, true, gc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Generate and send a new PIN to your given matrix user.
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} boolResponse
|
|
||||||
// @Failure 400 {object} stringResponse
|
|
||||||
// @Failure 401 {object} boolResponse
|
|
||||||
// @Failure 500 {object} boolResponse
|
|
||||||
// @Param MatrixSendPINDTO body MatrixSendPINDTO true "User's Matrix ID."
|
|
||||||
// @Router /my/matrix/user [post]
|
|
||||||
// @Security Bearer
|
|
||||||
// @tags User Page
|
|
||||||
func (app *appContext) MatrixSendMyPIN(gc *gin.Context) {
|
|
||||||
var req MatrixSendPINDTO
|
|
||||||
gc.BindJSON(&req)
|
|
||||||
if req.UserID == "" {
|
|
||||||
respond(400, "errorNoUserID", gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if app.config.Section("matrix").Key("require_unique").MustBool(false) {
|
|
||||||
for _, u := range app.storage.GetMatrix() {
|
|
||||||
if req.UserID == u.UserID {
|
|
||||||
respondBool(400, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ok := app.matrix.SendStart(req.UserID)
|
|
||||||
if !ok {
|
|
||||||
respondBool(500, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
respondBool(200, true, gc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Check whether your matrix PIN is valid, and link the account to yours if so.
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} boolResponse
|
|
||||||
// @Failure 401 {object} boolResponse
|
|
||||||
// @Param pin path string true "PIN code to check"
|
|
||||||
// @Param invCode path string true "invite Code"
|
|
||||||
// @Param userID path string true "Matrix User ID"
|
|
||||||
// @Router /my/matrix/verified/{userID}/{pin} [get]
|
|
||||||
// @Security Bearer
|
|
||||||
// @tags User Page
|
|
||||||
func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) {
|
|
||||||
userID := gc.Param("userID")
|
|
||||||
pin := gc.Param("pin")
|
|
||||||
user, ok := app.matrix.tokens[pin]
|
|
||||||
if !ok {
|
|
||||||
app.debug.Println("Matrix: PIN not found")
|
|
||||||
respondBool(200, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if user.User.UserID != userID {
|
|
||||||
app.debug.Println("Matrix: User ID of PIN didn't match")
|
|
||||||
respondBool(200, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mxUser := *user.User
|
|
||||||
mxUser.Contact = true
|
|
||||||
existingUser, ok := app.storage.GetMatrixKey(gc.GetString("jfId"))
|
|
||||||
if ok {
|
|
||||||
mxUser.Lang = existingUser.Lang
|
|
||||||
mxUser.Contact = existingUser.Contact
|
|
||||||
}
|
|
||||||
|
|
||||||
app.storage.SetMatrixKey(gc.GetString("jfId"), mxUser)
|
|
||||||
delete(app.matrix.tokens, pin)
|
|
||||||
respondBool(200, true, gc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary unlink the Discord account from your Jellyfin user. Always succeeds.
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} boolResponse
|
|
||||||
// @Router /my/discord [delete]
|
|
||||||
// @Security Bearer
|
|
||||||
// @Tags User Page
|
|
||||||
func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
|
|
||||||
app.storage.DeleteDiscordKey(gc.GetString("jfId"))
|
|
||||||
respondBool(200, true, gc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary unlink the Telegram account from your Jellyfin user. Always succeeds.
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} boolResponse
|
|
||||||
// @Router /my/telegram [delete]
|
|
||||||
// @Security Bearer
|
|
||||||
// @Tags User Page
|
|
||||||
func (app *appContext) UnlinkMyTelegram(gc *gin.Context) {
|
|
||||||
app.storage.DeleteTelegramKey(gc.GetString("jfId"))
|
|
||||||
respondBool(200, true, gc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary unlink the Matrix account from your Jellyfin user. Always succeeds.
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} boolResponse
|
|
||||||
// @Router /my/matrix [delete]
|
|
||||||
// @Security Bearer
|
|
||||||
// @Tags User Page
|
|
||||||
func (app *appContext) UnlinkMyMatrix(gc *gin.Context) {
|
|
||||||
app.storage.DeleteMatrixKey(gc.GetString("jfId"))
|
|
||||||
respondBool(200, true, gc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Generate & send a password reset link if the given username/email/contact method exists. Doesn't give you any info about it's success.
|
|
||||||
// @Produce json
|
|
||||||
// @Param address path string true "address/contact method associated w/ your account."
|
|
||||||
// @Success 204 {object} boolResponse
|
|
||||||
// @Failure 400 {object} boolResponse
|
|
||||||
// @Failure 500 {object} boolResponse
|
|
||||||
// @Router /my/password/reset/{address} [post]
|
|
||||||
// @Tags User Page
|
|
||||||
func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
|
||||||
// All requests should take 1 second, to make it harder to tell if a success occured or not.
|
|
||||||
timerWait := make(chan bool)
|
|
||||||
cancel := time.AfterFunc(1*time.Second, func() {
|
|
||||||
timerWait <- true
|
|
||||||
})
|
|
||||||
address := gc.Param("address")
|
|
||||||
if address == "" {
|
|
||||||
app.debug.Println("Ignoring empty request for PWR")
|
|
||||||
cancel.Stop()
|
|
||||||
respondBool(400, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var pwr InternalPWR
|
|
||||||
var err error
|
|
||||||
|
|
||||||
jfID := app.ReverseUserSearch(address)
|
|
||||||
if jfID == "" {
|
|
||||||
app.debug.Printf("Ignoring PWR request: User not found")
|
|
||||||
|
|
||||||
for range timerWait {
|
|
||||||
respondBool(204, true, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pwr, err = app.GenInternalReset(jfID)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf("Failed to get user from Jellyfin: %v", err)
|
|
||||||
for range timerWait {
|
|
||||||
respondBool(204, true, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if app.internalPWRs == nil {
|
|
||||||
app.internalPWRs = map[string]InternalPWR{}
|
|
||||||
}
|
|
||||||
app.internalPWRs[pwr.PIN] = pwr
|
|
||||||
// FIXME: Send to all contact methods
|
|
||||||
msg, err := app.email.constructReset(
|
|
||||||
PasswordReset{
|
|
||||||
Pin: pwr.PIN,
|
|
||||||
Username: pwr.Username,
|
|
||||||
Expiry: pwr.Expiry,
|
|
||||||
Internal: true,
|
|
||||||
}, app, false,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf("Failed to construct password reset message for \"%s\": %v", pwr.Username, err)
|
|
||||||
for range timerWait {
|
|
||||||
respondBool(204, true, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
} else if err := app.sendByID(msg, jfID); err != nil {
|
|
||||||
app.err.Printf("Failed to send password reset message to \"%s\": %v", address, err)
|
|
||||||
} else {
|
|
||||||
app.info.Printf("Sent password reset message to \"%s\"", address)
|
|
||||||
}
|
|
||||||
for range timerWait {
|
|
||||||
respondBool(204, true, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Change your password, given the old one and the new one.
|
|
||||||
// @Produce json
|
|
||||||
// @Param ChangeMyPasswordDTO body ChangeMyPasswordDTO true "User's old & new passwords."
|
|
||||||
// @Success 204 {object} boolResponse
|
|
||||||
// @Failure 400 {object} PasswordValidation
|
|
||||||
// @Failure 401 {object} boolResponse
|
|
||||||
// @Failure 500 {object} boolResponse
|
|
||||||
// @Router /my/password [post]
|
|
||||||
// @Security Bearer
|
|
||||||
// @Tags User Page
|
|
||||||
func (app *appContext) ChangeMyPassword(gc *gin.Context) {
|
|
||||||
var req ChangeMyPasswordDTO
|
|
||||||
gc.BindJSON(&req)
|
|
||||||
if req.Old == "" || req.New == "" {
|
|
||||||
respondBool(400, false, gc)
|
|
||||||
}
|
|
||||||
validation := app.validator.validate(req.New)
|
|
||||||
for _, val := range validation {
|
|
||||||
if !val {
|
|
||||||
app.debug.Printf("%s: Change password failed: Invalid password", gc.GetString("jfId"))
|
|
||||||
gc.JSON(400, validation)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
user, status, err := app.jf.UserByID(gc.GetString("jfId"), false)
|
|
||||||
if status != 200 || err != nil {
|
|
||||||
app.err.Printf("Failed to change password: couldn't find user (%d): %+v", status, err)
|
|
||||||
respondBool(500, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Authenticate as user to confirm old password.
|
|
||||||
user, status, err = app.authJf.Authenticate(user.Name, req.Old)
|
|
||||||
if status != 200 || err != nil {
|
|
||||||
respondBool(401, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
status, err = app.jf.SetPassword(gc.GetString("jfId"), req.Old, req.New)
|
|
||||||
if (status != 200 && status != 204) || err != nil {
|
|
||||||
respondBool(500, false, gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
|
||||||
func() {
|
|
||||||
ombiUser, status, err := app.getOmbiUser(gc.GetString("jfId"))
|
|
||||||
if status != 200 || err != nil {
|
|
||||||
app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", user.Name, status, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ombiUser["password"] = req.New
|
|
||||||
status, err = app.ombi.ModifyUser(ombiUser)
|
|
||||||
if status != 200 || err != nil {
|
|
||||||
app.err.Printf("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
cookie, err := gc.Cookie("user-refresh")
|
|
||||||
if err == nil {
|
|
||||||
app.invalidTokens = append(app.invalidTokens, cookie)
|
|
||||||
gc.SetCookie("refresh", "invalid", -1, "/my", gc.Request.URL.Hostname(), true, true)
|
|
||||||
} else {
|
|
||||||
app.debug.Printf("Couldn't get cookies: %s", err)
|
|
||||||
}
|
|
||||||
respondBool(204, true, gc)
|
|
||||||
}
|
|
109
api-users.go
109
api-users.go
@ -61,7 +61,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
app.jf.CacheExpiry = time.Now()
|
app.jf.CacheExpiry = time.Now()
|
||||||
if emailEnabled {
|
if emailEnabled {
|
||||||
app.storage.SetEmailsKey(id, EmailAddress{Addr: req.Email, Contact: true})
|
app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
|
||||||
app.storage.storeEmails()
|
app.storage.storeEmails()
|
||||||
}
|
}
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
@ -121,7 +121,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
discordUser, discordVerified = app.discord.UserVerified(req.DiscordPIN)
|
discordUser, discordVerified = app.discord.verifiedTokens[req.DiscordPIN]
|
||||||
if !discordVerified {
|
if !discordVerified {
|
||||||
f = func(gc *gin.Context) {
|
f = func(gc *gin.Context) {
|
||||||
app.debug.Printf("%s: New user failed: Discord PIN was invalid", req.Code)
|
app.debug.Printf("%s: New user failed: Discord PIN was invalid", req.Code)
|
||||||
@ -131,7 +131,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if app.config.Section("discord").Key("require_unique").MustBool(false) {
|
if app.config.Section("discord").Key("require_unique").MustBool(false) {
|
||||||
for _, u := range app.storage.GetDiscord() {
|
for _, u := range app.storage.discord {
|
||||||
if discordUser.ID == u.ID {
|
if discordUser.ID == u.ID {
|
||||||
f = func(gc *gin.Context) {
|
f = func(gc *gin.Context) {
|
||||||
app.debug.Printf("%s: New user failed: Discord user already linked", req.Code)
|
app.debug.Printf("%s: New user failed: Discord user already linked", req.Code)
|
||||||
@ -177,7 +177,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if app.config.Section("matrix").Key("require_unique").MustBool(false) {
|
if app.config.Section("matrix").Key("require_unique").MustBool(false) {
|
||||||
for _, u := range app.storage.GetMatrix() {
|
for _, u := range app.storage.matrix {
|
||||||
if user.User.UserID == u.UserID {
|
if user.User.UserID == u.UserID {
|
||||||
f = func(gc *gin.Context) {
|
f = func(gc *gin.Context) {
|
||||||
app.debug.Printf("%s: New user failed: Matrix user already linked", req.Code)
|
app.debug.Printf("%s: New user failed: Matrix user already linked", req.Code)
|
||||||
@ -193,8 +193,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var tgToken TelegramVerifiedToken
|
telegramTokenIndex := -1
|
||||||
telegramVerified := false
|
|
||||||
if telegramEnabled {
|
if telegramEnabled {
|
||||||
if req.TelegramPIN == "" {
|
if req.TelegramPIN == "" {
|
||||||
if app.config.Section("telegram").Key("required").MustBool(false) {
|
if app.config.Section("telegram").Key("required").MustBool(false) {
|
||||||
@ -206,8 +205,13 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tgToken, telegramVerified = app.telegram.TokenVerified(req.TelegramPIN)
|
for i, v := range app.telegram.verifiedTokens {
|
||||||
if !telegramVerified {
|
if v.Token == req.TelegramPIN {
|
||||||
|
telegramTokenIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if telegramTokenIndex == -1 {
|
||||||
f = func(gc *gin.Context) {
|
f = func(gc *gin.Context) {
|
||||||
app.debug.Printf("%s: New user failed: Telegram PIN was invalid", req.Code)
|
app.debug.Printf("%s: New user failed: Telegram PIN was invalid", req.Code)
|
||||||
respond(401, "errorInvalidPIN", gc)
|
respond(401, "errorInvalidPIN", gc)
|
||||||
@ -215,7 +219,9 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
success = false
|
success = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(tgToken.Username) {
|
if app.config.Section("telegram").Key("require_unique").MustBool(false) {
|
||||||
|
for _, u := range app.storage.telegram {
|
||||||
|
if app.telegram.verifiedTokens[telegramTokenIndex].Username == u.Username {
|
||||||
f = func(gc *gin.Context) {
|
f = func(gc *gin.Context) {
|
||||||
app.debug.Printf("%s: New user failed: Telegram user already linked", req.Code)
|
app.debug.Printf("%s: New user failed: Telegram user already linked", req.Code)
|
||||||
respond(400, "errorAccountLinked", gc)
|
respond(400, "errorAccountLinked", gc)
|
||||||
@ -225,11 +231,17 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) && !confirmed {
|
if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) && !confirmed {
|
||||||
claims := jwt.MapClaims{
|
claims := jwt.MapClaims{
|
||||||
"valid": true,
|
"valid": true,
|
||||||
"invite": req.Code,
|
"invite": req.Code,
|
||||||
"exp": time.Now().Add(30 * time.Minute).Unix(),
|
"email": req.Email,
|
||||||
|
"username": req.Username,
|
||||||
|
"password": req.Password,
|
||||||
|
"telegramPIN": req.TelegramPIN,
|
||||||
|
"exp": time.Now().Add(time.Hour * 12).Unix(),
|
||||||
"type": "confirmation",
|
"type": "confirmation",
|
||||||
}
|
}
|
||||||
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
@ -242,17 +254,10 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
success = false
|
success = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if app.ConfirmationKeys == nil {
|
inv := app.storage.invites[req.Code]
|
||||||
app.ConfirmationKeys = map[string]map[string]newUserDTO{}
|
inv.Keys = append(inv.Keys, key)
|
||||||
}
|
app.storage.invites[req.Code] = inv
|
||||||
cKeys, ok := app.ConfirmationKeys[req.Code]
|
app.storage.storeInvites()
|
||||||
if !ok {
|
|
||||||
cKeys = map[string]newUserDTO{}
|
|
||||||
}
|
|
||||||
cKeys[key] = req
|
|
||||||
app.confirmationKeysLock.Lock()
|
|
||||||
app.ConfirmationKeys[req.Code] = cKeys
|
|
||||||
app.confirmationKeysLock.Unlock()
|
|
||||||
f = func(gc *gin.Context) {
|
f = func(gc *gin.Context) {
|
||||||
app.debug.Printf("%s: Email confirmation required", req.Code)
|
app.debug.Printf("%s: Email confirmation required", req.Code)
|
||||||
respond(401, "confirmEmail", gc)
|
respond(401, "confirmEmail", gc)
|
||||||
@ -279,7 +284,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.storage.loadProfiles()
|
app.storage.loadProfiles()
|
||||||
invite, _ := app.storage.GetInvitesKey(req.Code)
|
invite := app.storage.invites[req.Code]
|
||||||
app.checkInvite(req.Code, true, req.Username)
|
app.checkInvite(req.Code, true, req.Username)
|
||||||
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) {
|
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) {
|
||||||
for address, settings := range invite.Notify {
|
for address, settings := range invite.Notify {
|
||||||
@ -334,7 +339,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
}
|
}
|
||||||
// if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
// if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
||||||
if req.Email != "" {
|
if req.Email != "" {
|
||||||
app.storage.SetEmailsKey(id, EmailAddress{Addr: req.Email, Contact: true})
|
app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
|
||||||
app.storage.storeEmails()
|
app.storage.storeEmails()
|
||||||
}
|
}
|
||||||
expiry := time.Time{}
|
expiry := time.Time{}
|
||||||
@ -347,19 +352,20 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
app.err.Printf("Failed to store user duration: %v", err)
|
app.err.Printf("Failed to store user duration: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if discordVerified {
|
if discordEnabled && discordVerified {
|
||||||
discordUser.Contact = req.DiscordContact
|
discordUser.Contact = req.DiscordContact
|
||||||
if app.storage.discord == nil {
|
if app.storage.discord == nil {
|
||||||
app.storage.discord = discordStore{}
|
app.storage.discord = map[string]DiscordUser{}
|
||||||
}
|
}
|
||||||
app.storage.SetDiscordKey(user.ID, discordUser)
|
app.storage.discord[user.ID] = discordUser
|
||||||
if err := app.storage.storeDiscordUsers(); err != nil {
|
if err := app.storage.storeDiscordUsers(); err != nil {
|
||||||
app.err.Printf("Failed to store Discord users: %v", err)
|
app.err.Printf("Failed to store Discord users: %v", err)
|
||||||
} else {
|
} else {
|
||||||
delete(app.discord.verifiedTokens, req.DiscordPIN)
|
delete(app.discord.verifiedTokens, req.DiscordPIN)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if telegramVerified {
|
if telegramEnabled && telegramTokenIndex != -1 {
|
||||||
|
tgToken := app.telegram.verifiedTokens[telegramTokenIndex]
|
||||||
tgUser := TelegramUser{
|
tgUser := TelegramUser{
|
||||||
ChatID: tgToken.ChatID,
|
ChatID: tgToken.ChatID,
|
||||||
Username: tgToken.Username,
|
Username: tgToken.Username,
|
||||||
@ -369,10 +375,15 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
tgUser.Lang = lang
|
tgUser.Lang = lang
|
||||||
}
|
}
|
||||||
if app.storage.telegram == nil {
|
if app.storage.telegram == nil {
|
||||||
app.storage.telegram = telegramStore{}
|
app.storage.telegram = map[string]TelegramUser{}
|
||||||
|
}
|
||||||
|
app.storage.telegram[user.ID] = tgUser
|
||||||
|
if err := app.storage.storeTelegramUsers(); err != nil {
|
||||||
|
app.err.Printf("Failed to store Telegram users: %v", err)
|
||||||
|
} else {
|
||||||
|
app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1], app.telegram.verifiedTokens[telegramTokenIndex] = app.telegram.verifiedTokens[telegramTokenIndex], app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1]
|
||||||
|
app.telegram.verifiedTokens = app.telegram.verifiedTokens[:len(app.telegram.verifiedTokens)-1]
|
||||||
}
|
}
|
||||||
app.telegram.DeleteVerifiedToken(req.TelegramPIN)
|
|
||||||
app.storage.SetTelegramKey(user.ID, tgUser)
|
|
||||||
}
|
}
|
||||||
if invite.Profile != "" && app.config.Section("ombi").Key("enabled").MustBool(false) {
|
if invite.Profile != "" && app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
if profile.Ombi != nil && len(profile.Ombi) != 0 {
|
if profile.Ombi != nil && len(profile.Ombi) != 0 {
|
||||||
@ -383,19 +394,18 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", "))
|
app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", "))
|
||||||
} else {
|
} else {
|
||||||
app.info.Println("Created Ombi user")
|
app.info.Println("Created Ombi user")
|
||||||
if discordVerified || telegramVerified {
|
if (discordEnabled && discordVerified) || (telegramEnabled && telegramTokenIndex != -1) {
|
||||||
ombiUser, status, err := app.getOmbiUser(id)
|
ombiUser, status, err := app.getOmbiUser(id)
|
||||||
if status != 200 || err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf("Failed to get Ombi user (%d): %v", status, err)
|
app.err.Printf("Failed to get Ombi user (%d): %v", status, err)
|
||||||
} else {
|
} else {
|
||||||
dID := ""
|
dID := ""
|
||||||
tUser := ""
|
tUser := ""
|
||||||
if discordVerified {
|
if discordEnabled && discordVerified {
|
||||||
dID = discordUser.ID
|
dID = discordUser.ID
|
||||||
}
|
}
|
||||||
if telegramVerified {
|
if telegramEnabled && telegramTokenIndex != -1 {
|
||||||
u, _ := app.storage.GetTelegramKey(user.ID)
|
tUser = app.storage.telegram[user.ID].Username
|
||||||
tUser = u.Username
|
|
||||||
}
|
}
|
||||||
resp, status, err := app.ombi.SetNotificationPrefs(ombiUser, dID, tUser)
|
resp, status, err := app.ombi.SetNotificationPrefs(ombiUser, dID, tUser)
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
@ -413,14 +423,14 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
|||||||
matrixUser.Contact = req.MatrixContact
|
matrixUser.Contact = req.MatrixContact
|
||||||
delete(app.matrix.tokens, req.MatrixPIN)
|
delete(app.matrix.tokens, req.MatrixPIN)
|
||||||
if app.storage.matrix == nil {
|
if app.storage.matrix == nil {
|
||||||
app.storage.matrix = matrixStore{}
|
app.storage.matrix = map[string]MatrixUser{}
|
||||||
}
|
}
|
||||||
app.storage.SetMatrixKey(user.ID, matrixUser)
|
app.storage.matrix[user.ID] = matrixUser
|
||||||
if err := app.storage.storeMatrixUsers(); err != nil {
|
if err := app.storage.storeMatrixUsers(); err != nil {
|
||||||
app.err.Printf("Failed to store Matrix users: %v", err)
|
app.err.Printf("Failed to store Matrix users: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramVerified || discordVerified || matrixVerified {
|
if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 || discordVerified {
|
||||||
name := app.getAddressOrName(user.ID)
|
name := app.getAddressOrName(user.ID)
|
||||||
app.debug.Printf("%s: Sending welcome message to %s", req.Username, name)
|
app.debug.Printf("%s: Sending welcome message to %s", req.Username, name)
|
||||||
msg, err := app.email.constructWelcome(req.Username, expiry, app, false)
|
msg, err := app.email.constructWelcome(req.Username, expiry, app, false)
|
||||||
@ -463,7 +473,6 @@ func (app *appContext) NewUser(gc *gin.Context) {
|
|||||||
for _, val := range validation {
|
for _, val := range validation {
|
||||||
if !val {
|
if !val {
|
||||||
valid = false
|
valid = false
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !valid {
|
if !valid {
|
||||||
@ -479,7 +488,7 @@ func (app *appContext) NewUser(gc *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if app.config.Section("email").Key("require_unique").MustBool(false) && req.Email != "" {
|
if app.config.Section("email").Key("require_unique").MustBool(false) && req.Email != "" {
|
||||||
for _, email := range app.storage.GetEmails() {
|
for _, email := range app.storage.emails {
|
||||||
if req.Email == email.Addr {
|
if req.Email == email.Addr {
|
||||||
app.info.Printf("%s: New user failed: Email already in use", req.Code)
|
app.info.Printf("%s: New user failed: Email already in use", req.Code)
|
||||||
respond(400, "errorEmailLinked", gc)
|
respond(400, "errorEmailLinked", gc)
|
||||||
@ -888,7 +897,7 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
|||||||
if !jfUser.LastActivityDate.IsZero() {
|
if !jfUser.LastActivityDate.IsZero() {
|
||||||
user.LastActive = jfUser.LastActivityDate.Unix()
|
user.LastActive = jfUser.LastActivityDate.Unix()
|
||||||
}
|
}
|
||||||
if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok {
|
if email, ok := app.storage.emails[jfUser.ID]; ok {
|
||||||
user.Email = email.Addr
|
user.Email = email.Addr
|
||||||
user.NotifyThroughEmail = email.Contact
|
user.NotifyThroughEmail = email.Contact
|
||||||
user.Label = email.Label
|
user.Label = email.Label
|
||||||
@ -898,15 +907,15 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
|||||||
if ok {
|
if ok {
|
||||||
user.Expiry = expiry.Unix()
|
user.Expiry = expiry.Unix()
|
||||||
}
|
}
|
||||||
if tgUser, ok := app.storage.GetTelegramKey(jfUser.ID); ok {
|
if tgUser, ok := app.storage.telegram[jfUser.ID]; ok {
|
||||||
user.Telegram = tgUser.Username
|
user.Telegram = tgUser.Username
|
||||||
user.NotifyThroughTelegram = tgUser.Contact
|
user.NotifyThroughTelegram = tgUser.Contact
|
||||||
}
|
}
|
||||||
if mxUser, ok := app.storage.GetMatrixKey(jfUser.ID); ok {
|
if mxUser, ok := app.storage.matrix[jfUser.ID]; ok {
|
||||||
user.Matrix = mxUser.UserID
|
user.Matrix = mxUser.UserID
|
||||||
user.NotifyThroughMatrix = mxUser.Contact
|
user.NotifyThroughMatrix = mxUser.Contact
|
||||||
}
|
}
|
||||||
if dcUser, ok := app.storage.GetDiscordKey(jfUser.ID); ok {
|
if dcUser, ok := app.storage.discord[jfUser.ID]; ok {
|
||||||
user.Discord = RenderDiscordUsername(dcUser)
|
user.Discord = RenderDiscordUsername(dcUser)
|
||||||
// user.Discord = dcUser.Username + "#" + dcUser.Discriminator
|
// user.Discord = dcUser.Username + "#" + dcUser.Discriminator
|
||||||
user.DiscordID = dcUser.ID
|
user.DiscordID = dcUser.ID
|
||||||
@ -940,11 +949,11 @@ func (app *appContext) SetAccountsAdmin(gc *gin.Context) {
|
|||||||
id := jfUser.ID
|
id := jfUser.ID
|
||||||
if admin, ok := req[id]; ok {
|
if admin, ok := req[id]; ok {
|
||||||
var emailStore = EmailAddress{}
|
var emailStore = EmailAddress{}
|
||||||
if oldEmail, ok := app.storage.GetEmailsKey(id); ok {
|
if oldEmail, ok := app.storage.emails[id]; ok {
|
||||||
emailStore = oldEmail
|
emailStore = oldEmail
|
||||||
}
|
}
|
||||||
emailStore.Admin = admin
|
emailStore.Admin = admin
|
||||||
app.storage.SetEmailsKey(id, emailStore)
|
app.storage.emails[id] = emailStore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := app.storage.storeEmails(); err != nil {
|
if err := app.storage.storeEmails(); err != nil {
|
||||||
@ -977,11 +986,11 @@ func (app *appContext) ModifyLabels(gc *gin.Context) {
|
|||||||
id := jfUser.ID
|
id := jfUser.ID
|
||||||
if label, ok := req[id]; ok {
|
if label, ok := req[id]; ok {
|
||||||
var emailStore = EmailAddress{}
|
var emailStore = EmailAddress{}
|
||||||
if oldEmail, ok := app.storage.GetEmailsKey(id); ok {
|
if oldEmail, ok := app.storage.emails[id]; ok {
|
||||||
emailStore = oldEmail
|
emailStore = oldEmail
|
||||||
}
|
}
|
||||||
emailStore.Label = label
|
emailStore.Label = label
|
||||||
app.storage.SetEmailsKey(id, emailStore)
|
app.storage.emails[id] = emailStore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := app.storage.storeEmails(); err != nil {
|
if err := app.storage.storeEmails(); err != nil {
|
||||||
@ -1015,7 +1024,7 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
|
|||||||
id := jfUser.ID
|
id := jfUser.ID
|
||||||
if address, ok := req[id]; ok {
|
if address, ok := req[id]; ok {
|
||||||
var emailStore = EmailAddress{}
|
var emailStore = EmailAddress{}
|
||||||
oldEmail, ok := app.storage.GetEmailsKey(id)
|
oldEmail, ok := app.storage.emails[id]
|
||||||
if ok {
|
if ok {
|
||||||
emailStore = oldEmail
|
emailStore = oldEmail
|
||||||
}
|
}
|
||||||
@ -1026,7 +1035,7 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
emailStore.Addr = address
|
emailStore.Addr = address
|
||||||
app.storage.SetEmailsKey(id, emailStore)
|
app.storage.emails[id] = emailStore
|
||||||
if ombiEnabled {
|
if ombiEnabled {
|
||||||
ombiUser, code, err := app.getOmbiUser(id)
|
ombiUser, code, err := app.getOmbiUser(id)
|
||||||
if code == 200 && err == nil {
|
if code == 200 && err == nil {
|
||||||
|
13
api.go
13
api.go
@ -143,7 +143,6 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
respondBool(status, false, gc)
|
respondBool(status, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
delete(app.internalPWRs, req.PIN)
|
|
||||||
} else {
|
} else {
|
||||||
resp, status, err := app.jf.ResetPassword(req.PIN)
|
resp, status, err := app.jf.ResetPassword(req.PIN)
|
||||||
if status != 200 || err != nil || !resp.Success {
|
if status != 200 || err != nil || !resp.Success {
|
||||||
@ -215,7 +214,7 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
|||||||
app.info.Println("Config requested")
|
app.info.Println("Config requested")
|
||||||
resp := app.configBase
|
resp := app.configBase
|
||||||
// Load language options
|
// Load language options
|
||||||
formOptions := app.storage.lang.User.getOptions()
|
formOptions := app.storage.lang.Form.getOptions()
|
||||||
fl := resp.Sections["ui"].Settings["language-form"]
|
fl := resp.Sections["ui"].Settings["language-form"]
|
||||||
fl.Options = formOptions
|
fl.Options = formOptions
|
||||||
fl.Value = app.config.Section("ui").Key("language-form").MustString("en-us")
|
fl.Value = app.config.Section("ui").Key("language-form").MustString("en-us")
|
||||||
@ -269,7 +268,7 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
|||||||
val := app.config.Section(sectName).Key(settingName)
|
val := app.config.Section(sectName).Key(settingName)
|
||||||
s := resp.Sections[sectName].Settings[settingName]
|
s := resp.Sections[sectName].Settings[settingName]
|
||||||
switch setting.Type {
|
switch setting.Type {
|
||||||
case "text", "email", "select", "password", "note":
|
case "text", "email", "select", "password":
|
||||||
s.Value = val.MustString("")
|
s.Value = val.MustString("")
|
||||||
case "number":
|
case "number":
|
||||||
s.Value = val.MustInt(0)
|
s.Value = val.MustInt(0)
|
||||||
@ -453,8 +452,8 @@ func (app *appContext) GetLanguages(gc *gin.Context) {
|
|||||||
page := gc.Param("page")
|
page := gc.Param("page")
|
||||||
resp := langDTO{}
|
resp := langDTO{}
|
||||||
switch page {
|
switch page {
|
||||||
case "form", "user":
|
case "form":
|
||||||
for key, lang := range app.storage.lang.User {
|
for key, lang := range app.storage.lang.Form {
|
||||||
resp[key] = lang.Meta.Name
|
resp[key] = lang.Meta.Name
|
||||||
}
|
}
|
||||||
case "admin":
|
case "admin":
|
||||||
@ -495,8 +494,8 @@ func (app *appContext) ServeLang(gc *gin.Context) {
|
|||||||
if page == "admin" {
|
if page == "admin" {
|
||||||
gc.JSON(200, app.storage.lang.Admin[lang])
|
gc.JSON(200, app.storage.lang.Admin[lang])
|
||||||
return
|
return
|
||||||
} else if page == "form" || page == "user" {
|
} else if page == "form" {
|
||||||
gc.JSON(200, app.storage.lang.User[lang])
|
gc.JSON(200, app.storage.lang.Form[lang])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
|
159
auth.go
159
auth.go
@ -9,28 +9,21 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt"
|
"github.com/golang-jwt/jwt"
|
||||||
"github.com/hrfee/mediabrowser"
|
|
||||||
"github.com/lithammer/shortuuid/v3"
|
"github.com/lithammer/shortuuid/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
TOKEN_VALIDITY_SEC = 20 * 60
|
|
||||||
REFRESH_TOKEN_VALIDITY_SEC = 3600 * 24
|
|
||||||
)
|
|
||||||
|
|
||||||
func (app *appContext) webAuth() gin.HandlerFunc {
|
func (app *appContext) webAuth() gin.HandlerFunc {
|
||||||
return app.authenticate
|
return app.authenticate
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateToken returns a web token as well as a refresh token, which can be used to obtain new tokens.
|
// CreateToken returns a web token as well as a refresh token, which can be used to obtain new tokens.
|
||||||
func CreateToken(userId, jfId string, admin bool) (string, string, error) {
|
func CreateToken(userId, jfId string) (string, string, error) {
|
||||||
var token, refresh string
|
var token, refresh string
|
||||||
claims := jwt.MapClaims{
|
claims := jwt.MapClaims{
|
||||||
"valid": true,
|
"valid": true,
|
||||||
"id": userId,
|
"id": userId,
|
||||||
"exp": time.Now().Add(time.Second * TOKEN_VALIDITY_SEC).Unix(),
|
"exp": time.Now().Add(time.Minute * 20).Unix(),
|
||||||
"jfid": jfId,
|
"jfid": jfId,
|
||||||
"admin": admin,
|
|
||||||
"type": "bearer",
|
"type": "bearer",
|
||||||
}
|
}
|
||||||
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
@ -38,7 +31,7 @@ func CreateToken(userId, jfId string, admin bool) (string, string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
claims["exp"] = time.Now().Add(time.Second * REFRESH_TOKEN_VALIDITY_SEC).Unix()
|
claims["exp"] = time.Now().Add(time.Hour * 24).Unix()
|
||||||
claims["type"] = "refresh"
|
claims["type"] = "refresh"
|
||||||
tk = jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
tk = jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
refresh, err = tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
|
refresh, err = tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
|
||||||
@ -48,9 +41,8 @@ func CreateToken(userId, jfId string, admin bool) (string, string, error) {
|
|||||||
return token, refresh, nil
|
return token, refresh, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caller should return if this returns false.
|
// Check header for token
|
||||||
func (app *appContext) decodeValidateAuthHeader(gc *gin.Context) (claims jwt.MapClaims, ok bool) {
|
func (app *appContext) authenticate(gc *gin.Context) {
|
||||||
ok = false
|
|
||||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
||||||
if header[0] != "Bearer" {
|
if header[0] != "Bearer" {
|
||||||
app.debug.Println("Invalid authorization header")
|
app.debug.Println("Invalid authorization header")
|
||||||
@ -63,48 +55,23 @@ func (app *appContext) decodeValidateAuthHeader(gc *gin.Context) (claims jwt.Map
|
|||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
claims, ok = token.Claims.(jwt.MapClaims)
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
if !ok {
|
|
||||||
app.debug.Println("Invalid JWT")
|
|
||||||
respond(401, "Unauthorized", gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
expiryUnix := int64(claims["exp"].(float64))
|
expiryUnix := int64(claims["exp"].(float64))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.debug.Printf("Auth denied: %s", err)
|
app.debug.Printf("Auth denied: %s", err)
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
ok = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
expiry := time.Unix(expiryUnix, 0)
|
expiry := time.Unix(expiryUnix, 0)
|
||||||
if !(ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now())) {
|
if !(ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now())) {
|
||||||
app.debug.Printf("Auth denied: Invalid token")
|
app.debug.Printf("Auth denied: Invalid token")
|
||||||
// app.debug.Printf("Expiry: %+v, OK: %t, Valid: %t, ClaimType: %s\n", expiry, ok, token.Valid, claims["type"].(string))
|
|
||||||
respond(401, "Unauthorized", gc)
|
|
||||||
ok = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ok = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check header for token
|
|
||||||
func (app *appContext) authenticate(gc *gin.Context) {
|
|
||||||
claims, ok := app.decodeValidateAuthHeader(gc)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isAdminToken := claims["admin"].(bool)
|
|
||||||
if !isAdminToken {
|
|
||||||
app.debug.Printf("Auth denied: Token was not for admin access")
|
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userID := claims["id"].(string)
|
userID := claims["id"].(string)
|
||||||
jfID := claims["jfid"].(string)
|
jfID := claims["jfid"].(string)
|
||||||
match := false
|
match := false
|
||||||
for _, user := range app.adminUsers {
|
for _, user := range app.users {
|
||||||
if user.UserID == userID {
|
if user.UserID == userID {
|
||||||
match = true
|
match = true
|
||||||
break
|
break
|
||||||
@ -117,7 +84,6 @@ func (app *appContext) authenticate(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
gc.Set("jfId", jfID)
|
gc.Set("jfId", jfID)
|
||||||
gc.Set("userId", userID)
|
gc.Set("userId", userID)
|
||||||
gc.Set("userMode", false)
|
|
||||||
app.debug.Println("Auth succeeded")
|
app.debug.Println("Auth succeeded")
|
||||||
gc.Next()
|
gc.Next()
|
||||||
}
|
}
|
||||||
@ -133,44 +99,6 @@ type getTokenDTO struct {
|
|||||||
Token string `json:"token" example:"kjsdklsfdkljfsjsdfklsdfkldsfjdfskjsdfjklsdf"` // API token for use with everything else.
|
Token string `json:"token" example:"kjsdklsfdkljfsjsdfklsdfkldsfjdfskjsdfjklsdf"` // API token for use with everything else.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) decodeValidateLoginHeader(gc *gin.Context) (username, password string, ok bool) {
|
|
||||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
|
||||||
auth, _ := base64.StdEncoding.DecodeString(header[1])
|
|
||||||
creds := strings.SplitN(string(auth), ":", 2)
|
|
||||||
username = creds[0]
|
|
||||||
password = creds[1]
|
|
||||||
ok = false
|
|
||||||
if username == "" || password == "" {
|
|
||||||
app.debug.Println("Auth denied: blank username/password")
|
|
||||||
respond(401, "Unauthorized", gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ok = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *appContext) validateJellyfinCredentials(username, password string, gc *gin.Context) (user mediabrowser.User, ok bool) {
|
|
||||||
ok = false
|
|
||||||
user, status, err := app.authJf.Authenticate(username, password)
|
|
||||||
if status != 200 || err != nil {
|
|
||||||
if status == 401 || status == 400 {
|
|
||||||
app.info.Println("Auth denied: Invalid username/password (Jellyfin)")
|
|
||||||
respond(401, "Unauthorized", gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if status == 403 {
|
|
||||||
app.info.Println("Auth denied: Jellyfin account disabled")
|
|
||||||
respond(403, "yourAccountWasDisabled", gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin (%d/%s)", status, err)
|
|
||||||
respond(500, "Jellyfin error", gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ok = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Grabs an API token using username & password.
|
// @Summary Grabs an API token using username & password.
|
||||||
// @description If viewing docs locally, click the lock icon next to this, login with your normal jfa-go credentials. Click 'try it out', then 'execute' and an API Key will be returned, copy it (not including quotes). On any of the other routes, click the lock icon and set the API key as "Bearer `your api key`".
|
// @description If viewing docs locally, click the lock icon next to this, login with your normal jfa-go credentials. Click 'try it out', then 'execute' and an API Key will be returned, copy it (not including quotes). On any of the other routes, click the lock icon and set the API key as "Bearer `your api key`".
|
||||||
// @Produce json
|
// @Produce json
|
||||||
@ -181,14 +109,18 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc
|
|||||||
// @Security getTokenAuth
|
// @Security getTokenAuth
|
||||||
func (app *appContext) getTokenLogin(gc *gin.Context) {
|
func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||||
app.info.Println("Token requested (login attempt)")
|
app.info.Println("Token requested (login attempt)")
|
||||||
username, password, ok := app.decodeValidateLoginHeader(gc)
|
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
||||||
if !ok {
|
auth, _ := base64.StdEncoding.DecodeString(header[1])
|
||||||
|
creds := strings.SplitN(string(auth), ":", 2)
|
||||||
|
var userID, jfID string
|
||||||
|
if creds[0] == "" || creds[1] == "" {
|
||||||
|
app.debug.Println("Auth denied: blank username/password")
|
||||||
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var userID, jfID string
|
|
||||||
match := false
|
match := false
|
||||||
for _, user := range app.adminUsers {
|
for _, user := range app.users {
|
||||||
if user.Username == username && user.Password == password {
|
if user.Username == creds[0] && user.Password == creds[1] {
|
||||||
match = true
|
match = true
|
||||||
app.debug.Println("Found existing user")
|
app.debug.Println("Found existing user")
|
||||||
userID = user.UserID
|
userID = user.UserID
|
||||||
@ -201,20 +133,27 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !match {
|
if !match {
|
||||||
user, ok := app.validateJellyfinCredentials(username, password, gc)
|
user, status, err := app.authJf.Authenticate(creds[0], creds[1])
|
||||||
if !ok {
|
if status != 200 || err != nil {
|
||||||
|
if status == 401 || status == 400 {
|
||||||
|
app.info.Println("Auth denied: Invalid username/password (Jellyfin)")
|
||||||
|
respond(401, "Unauthorized", gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin (%d/%s)", status, err)
|
||||||
|
respond(500, "Jellyfin error", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jfID = user.ID
|
jfID = user.ID
|
||||||
if !app.config.Section("ui").Key("allow_all").MustBool(false) {
|
if !app.config.Section("ui").Key("allow_all").MustBool(false) {
|
||||||
accountsAdmin := false
|
accountsAdmin := false
|
||||||
adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
|
adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
|
||||||
if emailStore, ok := app.storage.GetEmailsKey(jfID); ok {
|
if emailStore, ok := app.storage.emails[jfID]; ok {
|
||||||
accountsAdmin = emailStore.Admin
|
accountsAdmin = emailStore.Admin
|
||||||
}
|
}
|
||||||
accountsAdmin = accountsAdmin || (adminOnly && user.Policy.IsAdministrator)
|
accountsAdmin = accountsAdmin || (adminOnly && user.Policy.IsAdministrator)
|
||||||
if !accountsAdmin {
|
if !accountsAdmin {
|
||||||
app.debug.Printf("Auth denied: Users \"%s\" isn't admin", username)
|
app.debug.Printf("Auth denied: Users \"%s\" isn't admin", creds[0])
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -224,10 +163,10 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
|||||||
newUser := User{
|
newUser := User{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
}
|
}
|
||||||
app.debug.Printf("Token generated for user \"%s\"", username)
|
app.debug.Printf("Token generated for user \"%s\"", creds[0])
|
||||||
app.adminUsers = append(app.adminUsers, newUser)
|
app.users = append(app.users, newUser)
|
||||||
}
|
}
|
||||||
token, refresh, err := CreateToken(userID, jfID, true)
|
token, refresh, err := CreateToken(userID, jfID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf("getToken failed: Couldn't generate token (%s)", err)
|
app.err.Printf("getToken failed: Couldn't generate token (%s)", err)
|
||||||
respond(500, "Couldn't generate token", gc)
|
respond(500, "Couldn't generate token", gc)
|
||||||
@ -237,9 +176,15 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
|||||||
gc.JSON(200, getTokenDTO{token})
|
gc.JSON(200, getTokenDTO{token})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName string) (claims jwt.MapClaims, ok bool) {
|
// @Summary Grabs an API token using a refresh token from cookies.
|
||||||
ok = false
|
// @Produce json
|
||||||
cookie, err := gc.Cookie(cookieName)
|
// @Success 200 {object} getTokenDTO
|
||||||
|
// @Failure 401 {object} stringResponse
|
||||||
|
// @Router /token/refresh [get]
|
||||||
|
// @tags Auth
|
||||||
|
func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
||||||
|
app.debug.Println("Token requested (refresh token)")
|
||||||
|
cookie, err := gc.Cookie("refresh")
|
||||||
if err != nil || cookie == "" {
|
if err != nil || cookie == "" {
|
||||||
app.debug.Printf("getTokenRefresh denied: Couldn't get token: %s", err)
|
app.debug.Printf("getTokenRefresh denied: Couldn't get token: %s", err)
|
||||||
respond(400, "Couldn't get token", gc)
|
respond(400, "Couldn't get token", gc)
|
||||||
@ -258,45 +203,27 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName s
|
|||||||
respond(400, "Invalid token", gc)
|
respond(400, "Invalid token", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
claims, ok = token.Claims.(jwt.MapClaims)
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
expiryUnix := int64(claims["exp"].(float64))
|
expiryUnix := int64(claims["exp"].(float64))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.debug.Printf("getTokenRefresh: Invalid token expiry: %s", err)
|
app.debug.Printf("getTokenRefresh: Invalid token expiry: %s", err)
|
||||||
respond(401, "Invalid token", gc)
|
respond(401, "Invalid token", gc)
|
||||||
ok = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
expiry := time.Unix(expiryUnix, 0)
|
expiry := time.Unix(expiryUnix, 0)
|
||||||
if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) {
|
if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) {
|
||||||
app.debug.Printf("getTokenRefresh: Invalid token: %+v", err)
|
app.debug.Printf("getTokenRefresh: Invalid token: %s", err)
|
||||||
respond(401, "Invalid token", gc)
|
respond(401, "Invalid token", gc)
|
||||||
ok = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ok = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Grabs an API token using a refresh token from cookies.
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} getTokenDTO
|
|
||||||
// @Failure 401 {object} stringResponse
|
|
||||||
// @Router /token/refresh [get]
|
|
||||||
// @tags Auth
|
|
||||||
func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
|
||||||
app.debug.Println("Token requested (refresh token)")
|
|
||||||
claims, ok := app.decodeValidateRefreshCookie(gc, "refresh")
|
|
||||||
if !ok {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
userID := claims["id"].(string)
|
userID := claims["id"].(string)
|
||||||
jfID := claims["jfid"].(string)
|
jfID := claims["jfid"].(string)
|
||||||
jwt, refresh, err := CreateToken(userID, jfID, true)
|
jwt, refresh, err := CreateToken(userID, jfID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf("getTokenRefresh failed: Couldn't generate token (%s)", err)
|
app.err.Printf("getTokenRefresh failed: Couldn't generate token (%s)", err)
|
||||||
respond(500, "Couldn't generate token", gc)
|
respond(500, "Couldn't generate token", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", gc.Request.URL.Hostname(), true, true)
|
gc.SetCookie("refresh", refresh, (3600 * 24), "/", gc.Request.URL.Hostname(), true, true)
|
||||||
gc.JSON(200, getTokenDTO{jwt})
|
gc.JSON(200, getTokenDTO{jwt})
|
||||||
}
|
}
|
||||||
|
12
config.go
12
config.go
@ -46,7 +46,7 @@ func (app *appContext) loadConfig() error {
|
|||||||
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
|
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users", "matrix_users", "announcements", "custom_user_page_content"} {
|
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users", "matrix_users", "announcements"} {
|
||||||
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
|
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
|
||||||
}
|
}
|
||||||
for _, key := range []string{"matrix_sql"} {
|
for _, key := range []string{"matrix_sql"} {
|
||||||
@ -160,12 +160,6 @@ func (app *appContext) loadConfig() error {
|
|||||||
app.storage.customEmails_path = app.config.Section("files").Key("custom_emails").String()
|
app.storage.customEmails_path = app.config.Section("files").Key("custom_emails").String()
|
||||||
app.storage.loadCustomEmails()
|
app.storage.loadCustomEmails()
|
||||||
|
|
||||||
app.MustSetValue("user_page", "enabled", "true")
|
|
||||||
if app.config.Section("user_page").Key("enabled").MustBool(false) {
|
|
||||||
app.storage.userPage_path = app.config.Section("files").Key("custom_user_page_content").String()
|
|
||||||
app.storage.loadUserPageContent()
|
|
||||||
}
|
|
||||||
|
|
||||||
substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("")
|
substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("")
|
||||||
|
|
||||||
if substituteStrings != "" {
|
if substituteStrings != "" {
|
||||||
@ -175,11 +169,11 @@ func (app *appContext) loadConfig() error {
|
|||||||
|
|
||||||
oldFormLang := app.config.Section("ui").Key("language").MustString("")
|
oldFormLang := app.config.Section("ui").Key("language").MustString("")
|
||||||
if oldFormLang != "" {
|
if oldFormLang != "" {
|
||||||
app.storage.lang.chosenUserLang = oldFormLang
|
app.storage.lang.chosenFormLang = oldFormLang
|
||||||
}
|
}
|
||||||
newFormLang := app.config.Section("ui").Key("language-form").MustString("")
|
newFormLang := app.config.Section("ui").Key("language-form").MustString("")
|
||||||
if newFormLang != "" {
|
if newFormLang != "" {
|
||||||
app.storage.lang.chosenUserLang = newFormLang
|
app.storage.lang.chosenFormLang = newFormLang
|
||||||
}
|
}
|
||||||
app.storage.lang.chosenAdminLang = app.config.Section("ui").Key("language-admin").MustString("en-us")
|
app.storage.lang.chosenAdminLang = app.config.Section("ui").Key("language-admin").MustString("en-us")
|
||||||
app.storage.lang.chosenEmailLang = app.config.Section("email").Key("language").MustString("en-us")
|
app.storage.lang.chosenEmailLang = app.config.Section("email").Key("language").MustString("en-us")
|
||||||
|
@ -373,40 +373,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"user_page": {
|
|
||||||
"order": [],
|
|
||||||
"meta": {
|
|
||||||
"name": "User Page",
|
|
||||||
"description": "The User Page (My Account) allows users to access and modify info directly, such as changing/adding contact methods, seeing their expiry date, or changing their password. Password resets can also be initiated from here, given a contact method or username. ",
|
|
||||||
"depends_true": "ui|jellyfin_login"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"enabled": {
|
|
||||||
"name": "Enabled",
|
|
||||||
"required": false,
|
|
||||||
"requires_restart": false,
|
|
||||||
"type": "bool",
|
|
||||||
"value": true
|
|
||||||
},
|
|
||||||
"jellyfin_login_note": {
|
|
||||||
"name": "Note:",
|
|
||||||
"type": "note",
|
|
||||||
"value": "",
|
|
||||||
"depends_true": "enabled",
|
|
||||||
"required": "false",
|
|
||||||
"description": "Jellyfin Login must be enabled to use this feature, and password resets with a link must be enabled for self-service.",
|
|
||||||
"style": "critical"
|
|
||||||
},
|
|
||||||
"edit_note": {
|
|
||||||
"name": "Message Cards:",
|
|
||||||
"type": "note",
|
|
||||||
"value": "",
|
|
||||||
"depends_true": "enabled",
|
|
||||||
"required": "false",
|
|
||||||
"description": "Click the edit icon next to the \"User Page\" Setting to add custom Markdown messages that will be shown to the user. Note message cards are not private, little effort is required for anyone to view them."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"password_validation": {
|
"password_validation": {
|
||||||
"order": [],
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
@ -499,14 +465,6 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"value": "Need help? contact me.",
|
"value": "Need help? contact me.",
|
||||||
"description": "Message displayed at bottom of emails."
|
"description": "Message displayed at bottom of emails."
|
||||||
},
|
|
||||||
"edit_note": {
|
|
||||||
"name": "Customize Messages:",
|
|
||||||
"type": "note",
|
|
||||||
"value": "",
|
|
||||||
"depends_true": "enabled",
|
|
||||||
"required": "false",
|
|
||||||
"description": "Click the edit icon next to the \"Messages/Notifications\" Setting to customize the messages sent to users with Markdown."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1576,14 +1534,6 @@
|
|||||||
"value": "",
|
"value": "",
|
||||||
"description": "JSON file generated by program in settings, different from email_html/email_text. See wiki for more info."
|
"description": "JSON file generated by program in settings, different from email_html/email_text. See wiki for more info."
|
||||||
},
|
},
|
||||||
"custom_user_page_content": {
|
|
||||||
"name": "Custom user page content",
|
|
||||||
"required": false,
|
|
||||||
"requires_restart": false,
|
|
||||||
"type": "text",
|
|
||||||
"value": "",
|
|
||||||
"description": "JSON file generated by program in settings, containing user page messages. See wiki for more info."
|
|
||||||
},
|
|
||||||
"telegram_users": {
|
"telegram_users": {
|
||||||
"name": "Telegram users",
|
"name": "Telegram users",
|
||||||
"required": false,
|
"required": false,
|
||||||
|
11
css/base.css
11
css/base.css
@ -13,8 +13,6 @@
|
|||||||
--border-width-2: 3px;
|
--border-width-2: 3px;
|
||||||
--border-width-4: 5px;
|
--border-width-4: 5px;
|
||||||
--border-width-8: 8px;
|
--border-width-8: 8px;
|
||||||
|
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.light {
|
.light {
|
||||||
@ -487,15 +485,6 @@ a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) {
|
|||||||
color: var(--color-urge-200);
|
color: var(--color-urge-200);
|
||||||
}
|
}
|
||||||
|
|
||||||
a.button,
|
|
||||||
a.button:link,
|
|
||||||
a.button:visited,
|
|
||||||
a.button:focus,
|
|
||||||
a.buton:hover {
|
|
||||||
color: var(--color-content) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.link-center {
|
.link-center {
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
24
daemon.go
24
daemon.go
@ -13,16 +13,14 @@ func (app *appContext) clearEmails() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Rebuild email storage to from existing users to reduce time complexity
|
// Rebuild email storage to from existing users to reduce time complexity
|
||||||
emails := emailStore{}
|
emails := map[string]EmailAddress{}
|
||||||
app.storage.emailsLock.Lock()
|
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
if email, ok := app.storage.GetEmailsKey(user.ID); ok {
|
if email, ok := app.storage.emails[user.ID]; ok {
|
||||||
emails[user.ID] = email
|
emails[user.ID] = email
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.storage.emails = emails
|
app.storage.emails = emails
|
||||||
app.storage.storeEmails()
|
app.storage.storeEmails()
|
||||||
app.storage.emailsLock.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clearDiscord does the same as clearEmails, but for Discord Users.
|
// clearDiscord does the same as clearEmails, but for Discord Users.
|
||||||
@ -34,16 +32,14 @@ func (app *appContext) clearDiscord() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Rebuild discord storage to from existing users to reduce time complexity
|
// Rebuild discord storage to from existing users to reduce time complexity
|
||||||
dcUsers := discordStore{}
|
dcUsers := map[string]DiscordUser{}
|
||||||
app.storage.discordLock.Lock()
|
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
if dcUser, ok := app.storage.GetDiscordKey(user.ID); ok {
|
if dcUser, ok := app.storage.discord[user.ID]; ok {
|
||||||
dcUsers[user.ID] = dcUser
|
dcUsers[user.ID] = dcUser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.storage.discord = dcUsers
|
app.storage.discord = dcUsers
|
||||||
app.storage.storeDiscordUsers()
|
app.storage.storeDiscordUsers()
|
||||||
app.storage.discordLock.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clearMatrix does the same as clearEmails, but for Matrix Users.
|
// clearMatrix does the same as clearEmails, but for Matrix Users.
|
||||||
@ -55,16 +51,14 @@ func (app *appContext) clearMatrix() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Rebuild matrix storage to from existing users to reduce time complexity
|
// Rebuild matrix storage to from existing users to reduce time complexity
|
||||||
mxUsers := matrixStore{}
|
mxUsers := map[string]MatrixUser{}
|
||||||
app.storage.matrixLock.Lock()
|
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
if mxUser, ok := app.storage.GetMatrixKey(user.ID); ok {
|
if mxUser, ok := app.storage.matrix[user.ID]; ok {
|
||||||
mxUsers[user.ID] = mxUser
|
mxUsers[user.ID] = mxUser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.storage.matrix = mxUsers
|
app.storage.matrix = mxUsers
|
||||||
app.storage.storeMatrixUsers()
|
app.storage.storeMatrixUsers()
|
||||||
app.storage.matrixLock.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clearTelegram does the same as clearEmails, but for Telegram Users.
|
// clearTelegram does the same as clearEmails, but for Telegram Users.
|
||||||
@ -76,16 +70,14 @@ func (app *appContext) clearTelegram() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Rebuild telegram storage to from existing users to reduce time complexity
|
// Rebuild telegram storage to from existing users to reduce time complexity
|
||||||
tgUsers := telegramStore{}
|
tgUsers := map[string]TelegramUser{}
|
||||||
app.storage.telegramLock.Lock()
|
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
if tgUser, ok := app.storage.GetTelegramKey(user.ID); ok {
|
if tgUser, ok := app.storage.telegram[user.ID]; ok {
|
||||||
tgUsers[user.ID] = tgUser
|
tgUsers[user.ID] = tgUser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.storage.telegram = tgUsers
|
app.storage.telegram = tgUsers
|
||||||
app.storage.storeTelegramUsers()
|
app.storage.storeTelegramUsers()
|
||||||
app.storage.telegramLock.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
|
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
|
||||||
|
106
discord.go
106
discord.go
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
dg "github.com/bwmarrin/discordgo"
|
dg "github.com/bwmarrin/discordgo"
|
||||||
)
|
)
|
||||||
@ -13,8 +12,8 @@ type DiscordDaemon struct {
|
|||||||
ShutdownChannel chan string
|
ShutdownChannel chan string
|
||||||
bot *dg.Session
|
bot *dg.Session
|
||||||
username string
|
username string
|
||||||
tokens map[string]VerifToken // Map of pins to tokens.
|
tokens []string
|
||||||
verifiedTokens map[string]DiscordUser // Map of token pins to discord users.
|
verifiedTokens map[string]DiscordUser // Map of tokens to discord users.
|
||||||
channelID, channelName, inviteChannelID, inviteChannelName string
|
channelID, channelName, inviteChannelID, inviteChannelName string
|
||||||
guildID string
|
guildID string
|
||||||
serverChannelName, serverName string
|
serverChannelName, serverName string
|
||||||
@ -38,7 +37,7 @@ func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
|
|||||||
Stopped: false,
|
Stopped: false,
|
||||||
ShutdownChannel: make(chan string),
|
ShutdownChannel: make(chan string),
|
||||||
bot: bot,
|
bot: bot,
|
||||||
tokens: map[string]VerifToken{},
|
tokens: []string{},
|
||||||
verifiedTokens: map[string]DiscordUser{},
|
verifiedTokens: map[string]DiscordUser{},
|
||||||
users: map[string]DiscordUser{},
|
users: map[string]DiscordUser{},
|
||||||
app: app,
|
app: app,
|
||||||
@ -49,7 +48,7 @@ func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
|
|||||||
dd.commandHandlers[app.config.Section("discord").Key("start_command").MustString("start")] = dd.cmdStart
|
dd.commandHandlers[app.config.Section("discord").Key("start_command").MustString("start")] = dd.cmdStart
|
||||||
dd.commandHandlers["lang"] = dd.cmdLang
|
dd.commandHandlers["lang"] = dd.cmdLang
|
||||||
dd.commandHandlers["pin"] = dd.cmdPIN
|
dd.commandHandlers["pin"] = dd.cmdPIN
|
||||||
for _, user := range app.storage.GetDiscord() {
|
for _, user := range app.storage.discord {
|
||||||
dd.users[user.ID] = user
|
dd.users[user.ID] = user
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,15 +58,7 @@ func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
|
|||||||
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
||||||
func (d *DiscordDaemon) NewAuthToken() string {
|
func (d *DiscordDaemon) NewAuthToken() string {
|
||||||
pin := genAuthToken()
|
pin := genAuthToken()
|
||||||
d.tokens[pin] = VerifToken{Expiry: time.Now().Add(VERIF_TOKEN_EXPIRY_SEC * time.Second), JellyfinID: ""}
|
d.tokens = append(d.tokens, pin)
|
||||||
return pin
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAssignedAuthToken generates an 8-character pin in the form "A1-2B-CD",
|
|
||||||
// and assigns it for access only with the given Jellyfin ID.
|
|
||||||
func (d *DiscordDaemon) NewAssignedAuthToken(id string) string {
|
|
||||||
pin := genAuthToken()
|
|
||||||
d.tokens[pin] = VerifToken{Expiry: time.Now().Add(VERIF_TOKEN_EXPIRY_SEC * time.Second), JellyfinID: id}
|
|
||||||
return pin
|
return pin
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,9 +210,7 @@ func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconU
|
|||||||
d.app.err.Printf("Discord: Failed to get guild: %v", err)
|
d.app.err.Printf("Discord: Failed to get guild: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// FIXME: Fix CSS, and handle no icon
|
|
||||||
iconURL = guild.IconURL("256")
|
iconURL = guild.IconURL("256")
|
||||||
fmt.Println("GOT ICON", iconURL)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -440,8 +429,14 @@ func (d *DiscordDaemon) cmdStart(s *dg.Session, i *dg.InteractionCreate, lang st
|
|||||||
|
|
||||||
func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang string) {
|
func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang string) {
|
||||||
pin := i.ApplicationCommandData().Options[0].StringValue()
|
pin := i.ApplicationCommandData().Options[0].StringValue()
|
||||||
user, ok := d.tokens[pin]
|
tokenIndex := -1
|
||||||
if !ok || time.Now().After(user.Expiry) {
|
for i, token := range d.tokens {
|
||||||
|
if pin == token {
|
||||||
|
tokenIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tokenIndex == -1 {
|
||||||
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
||||||
// Type: dg.InteractionResponseChannelMessageWithSource,
|
// Type: dg.InteractionResponseChannelMessageWithSource,
|
||||||
Type: dg.InteractionResponseChannelMessageWithSource,
|
Type: dg.InteractionResponseChannelMessageWithSource,
|
||||||
@ -453,7 +448,6 @@ func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang stri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
|
||||||
}
|
}
|
||||||
delete(d.tokens, pin)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
||||||
@ -467,21 +461,23 @@ func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang stri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
|
||||||
}
|
}
|
||||||
dcUser := d.users[i.Interaction.Member.User.ID]
|
d.verifiedTokens[pin] = d.users[i.Interaction.Member.User.ID]
|
||||||
dcUser.JellyfinID = user.JellyfinID
|
d.tokens[len(d.tokens)-1], d.tokens[tokenIndex] = d.tokens[tokenIndex], d.tokens[len(d.tokens)-1]
|
||||||
d.verifiedTokens[pin] = dcUser
|
d.tokens = d.tokens[:len(d.tokens)-1]
|
||||||
delete(d.tokens, pin)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang string) {
|
func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang string) {
|
||||||
code := i.ApplicationCommandData().Options[0].StringValue()
|
code := i.ApplicationCommandData().Options[0].StringValue()
|
||||||
if _, ok := d.app.storage.lang.Telegram[code]; ok {
|
if _, ok := d.app.storage.lang.Telegram[code]; ok {
|
||||||
var user DiscordUser
|
var user DiscordUser
|
||||||
for jfID, u := range d.app.storage.GetDiscord() {
|
for jfID, u := range d.app.storage.discord {
|
||||||
if u.ID == i.Interaction.Member.User.ID {
|
if u.ID == i.Interaction.Member.User.ID {
|
||||||
u.Lang = code
|
u.Lang = code
|
||||||
lang = code
|
lang = code
|
||||||
d.app.storage.SetDiscordKey(jfID, u)
|
d.app.storage.discord[jfID] = u
|
||||||
|
if err := d.app.storage.storeDiscordUsers(); err != nil {
|
||||||
|
d.app.err.Printf("Failed to store Discord users: %v", err)
|
||||||
|
}
|
||||||
user = u
|
user = u
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -584,10 +580,13 @@ func (d *DiscordDaemon) msgLang(s *dg.Session, m *dg.MessageCreate, sects []stri
|
|||||||
}
|
}
|
||||||
if _, ok := d.app.storage.lang.Telegram[sects[1]]; ok {
|
if _, ok := d.app.storage.lang.Telegram[sects[1]]; ok {
|
||||||
var user DiscordUser
|
var user DiscordUser
|
||||||
for jfID, u := range d.app.storage.GetDiscord() {
|
for jfID, u := range d.app.storage.discord {
|
||||||
if u.ID == m.Author.ID {
|
if u.ID == m.Author.ID {
|
||||||
u.Lang = sects[1]
|
u.Lang = sects[1]
|
||||||
d.app.storage.SetDiscordKey(jfID, u)
|
d.app.storage.discord[jfID] = u
|
||||||
|
if err := d.app.storage.storeDiscordUsers(); err != nil {
|
||||||
|
d.app.err.Printf("Failed to store Discord users: %v", err)
|
||||||
|
}
|
||||||
user = u
|
user = u
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -611,8 +610,14 @@ func (d *DiscordDaemon) msgPIN(s *dg.Session, m *dg.MessageCreate, sects []strin
|
|||||||
d.app.debug.Println("Discord: Ignoring message as user was not found")
|
d.app.debug.Println("Discord: Ignoring message as user was not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, ok := d.tokens[sects[0]]
|
tokenIndex := -1
|
||||||
if !ok || time.Now().After(user.Expiry) {
|
for i, token := range d.tokens {
|
||||||
|
if sects[0] == token {
|
||||||
|
tokenIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tokenIndex == -1 {
|
||||||
_, err := s.ChannelMessageSend(
|
_, err := s.ChannelMessageSend(
|
||||||
m.ChannelID,
|
m.ChannelID,
|
||||||
d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"),
|
d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"),
|
||||||
@ -620,7 +625,6 @@ func (d *DiscordDaemon) msgPIN(s *dg.Session, m *dg.MessageCreate, sects []strin
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||||
}
|
}
|
||||||
delete(d.tokens, sects[0])
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err := s.ChannelMessageSend(
|
_, err := s.ChannelMessageSend(
|
||||||
@ -630,10 +634,9 @@ func (d *DiscordDaemon) msgPIN(s *dg.Session, m *dg.MessageCreate, sects []strin
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||||
}
|
}
|
||||||
dcUser := d.users[m.Author.ID]
|
d.verifiedTokens[sects[0]] = d.users[m.Author.ID]
|
||||||
dcUser.JellyfinID = user.JellyfinID
|
d.tokens[len(d.tokens)-1], d.tokens[tokenIndex] = d.tokens[tokenIndex], d.tokens[len(d.tokens)-1]
|
||||||
d.verifiedTokens[sects[0]] = dcUser
|
d.tokens = d.tokens[:len(d.tokens)-1]
|
||||||
delete(d.tokens, sects[0])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DiscordDaemon) SendDM(message *Message, userID ...string) error {
|
func (d *DiscordDaemon) SendDM(message *Message, userID ...string) error {
|
||||||
@ -687,38 +690,3 @@ func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserVerified returns whether or not a token with the given PIN has been verified, and the user itself.
|
|
||||||
func (d *DiscordDaemon) UserVerified(pin string) (user DiscordUser, ok bool) {
|
|
||||||
user, ok = d.verifiedTokens[pin]
|
|
||||||
// delete(d.verifiedTokens, pin)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssignedUserVerified returns whether or not a user with the given PIN has been verified, and the token itself.
|
|
||||||
// Returns false if the given Jellyfin ID does not match the one in the user.
|
|
||||||
func (d *DiscordDaemon) AssignedUserVerified(pin string, jfID string) (user DiscordUser, ok bool) {
|
|
||||||
user, ok = d.verifiedTokens[pin]
|
|
||||||
if ok && user.JellyfinID != jfID {
|
|
||||||
ok = false
|
|
||||||
}
|
|
||||||
// delete(d.verifiedUsers, pin)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserExists returns whether or not a user with the given ID exists.
|
|
||||||
func (d *DiscordDaemon) UserExists(id string) (ok bool) {
|
|
||||||
ok = false
|
|
||||||
for _, u := range d.app.storage.GetDiscord() {
|
|
||||||
if u.ID == id {
|
|
||||||
ok = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteVerifiedUser removes the token with the given PIN.
|
|
||||||
func (d *DiscordDaemon) DeleteVerifiedUser(pin string) {
|
|
||||||
delete(d.verifiedTokens, pin)
|
|
||||||
}
|
|
||||||
|
109
email.go
109
email.go
@ -10,7 +10,6 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -24,7 +23,7 @@ import (
|
|||||||
sMail "github.com/xhit/go-simple-mail/v2"
|
sMail "github.com/xhit/go-simple-mail/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var markdownRenderer = html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
|
var renderer = html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
|
||||||
|
|
||||||
// EmailClient implements email sending, right now via smtp, mailgun or a dummy client.
|
// EmailClient implements email sending, right now via smtp, mailgun or a dummy client.
|
||||||
type EmailClient interface {
|
type EmailClient interface {
|
||||||
@ -305,17 +304,10 @@ func (emailer *Emailer) confirmationValues(code, username, key string, app *appC
|
|||||||
} else {
|
} else {
|
||||||
message := app.config.Section("messages").Key("message").String()
|
message := app.config.Section("messages").Key("message").String()
|
||||||
inviteLink := app.config.Section("invite_emails").Key("url_base").String()
|
inviteLink := app.config.Section("invite_emails").Key("url_base").String()
|
||||||
if code == "" { // Personal email change
|
|
||||||
if strings.HasSuffix(inviteLink, "/invite") {
|
|
||||||
inviteLink = strings.TrimSuffix(inviteLink, "/invite")
|
|
||||||
}
|
|
||||||
inviteLink = fmt.Sprintf("%s/my/confirm/%s", inviteLink, url.PathEscape(key))
|
|
||||||
} else { // Invite email confirmation
|
|
||||||
if !strings.HasSuffix(inviteLink, "/invite") {
|
if !strings.HasSuffix(inviteLink, "/invite") {
|
||||||
inviteLink += "/invite"
|
inviteLink += "/invite"
|
||||||
}
|
}
|
||||||
inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, url.PathEscape(key))
|
inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key)
|
||||||
}
|
|
||||||
template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": username})
|
template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": username})
|
||||||
template["confirmationURL"] = inviteLink
|
template["confirmationURL"] = inviteLink
|
||||||
template["message"] = message
|
template["message"] = message
|
||||||
@ -353,7 +345,7 @@ func (emailer *Emailer) constructTemplate(subject, md string, app *appContext, u
|
|||||||
subject = templateEmail(subject, []string{"{username}"}, nil, map[string]interface{}{"username": username[0]})
|
subject = templateEmail(subject, []string{"{username}"}, nil, map[string]interface{}{"username": username[0]})
|
||||||
}
|
}
|
||||||
email := &Message{Subject: subject}
|
email := &Message{Subject: subject}
|
||||||
html := markdown.ToHTML([]byte(md), nil, markdownRenderer)
|
html := markdown.ToHTML([]byte(md), nil, renderer)
|
||||||
text := stripMarkdown(md)
|
text := stripMarkdown(md)
|
||||||
message := app.config.Section("messages").Key("message").String()
|
message := app.config.Section("messages").Key("message").String()
|
||||||
var err error
|
var err error
|
||||||
@ -522,6 +514,18 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
|
|||||||
return email, nil
|
return email, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenResetLink generates and returns a password reset link.
|
||||||
|
func (app *appContext) GenResetLink(pin string) (string, error) {
|
||||||
|
url := app.config.Section("password_resets").Key("url_base").String()
|
||||||
|
var pinLink string
|
||||||
|
if url == "" {
|
||||||
|
return pinLink, fmt.Errorf("disabled as no URL Base provided. Set in Settings > Password Resets.")
|
||||||
|
}
|
||||||
|
// Strip /invite from end of this URL, ik it's ugly.
|
||||||
|
pinLink = fmt.Sprintf("%s/reset?pin=%s", url, pin)
|
||||||
|
return pinLink, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bool) map[string]interface{} {
|
func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bool) map[string]interface{} {
|
||||||
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("messages").Key("message").String()
|
message := app.config.Section("messages").Key("message").String()
|
||||||
@ -823,82 +827,49 @@ func (emailer *Emailer) send(email *Message, address ...string) error {
|
|||||||
return emailer.sender.Send(emailer.fromName, emailer.fromAddr, email, address...)
|
return emailer.sender.Send(emailer.fromName, emailer.fromAddr, email, address...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) sendByID(email *Message, ID ...string) (err error) {
|
func (app *appContext) sendByID(email *Message, ID ...string) error {
|
||||||
for _, id := range ID {
|
for _, id := range ID {
|
||||||
if tgChat, ok := app.storage.GetTelegramKey(id); ok && tgChat.Contact && telegramEnabled {
|
var err error
|
||||||
|
if tgChat, ok := app.storage.telegram[id]; ok && tgChat.Contact && telegramEnabled {
|
||||||
err = app.telegram.Send(email, tgChat.ChatID)
|
err = app.telegram.Send(email, tgChat.ChatID)
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// return err
|
return err
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
if dcChat, ok := app.storage.GetDiscordKey(id); ok && dcChat.Contact && discordEnabled {
|
}
|
||||||
|
if dcChat, ok := app.storage.discord[id]; ok && dcChat.Contact && discordEnabled {
|
||||||
err = app.discord.Send(email, dcChat.ChannelID)
|
err = app.discord.Send(email, dcChat.ChannelID)
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// return err
|
return err
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
if mxChat, ok := app.storage.GetMatrixKey(id); ok && mxChat.Contact && matrixEnabled {
|
}
|
||||||
|
if mxChat, ok := app.storage.matrix[id]; ok && mxChat.Contact && matrixEnabled {
|
||||||
err = app.matrix.Send(email, mxChat)
|
err = app.matrix.Send(email, mxChat)
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// return err
|
return err
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
if address, ok := app.storage.GetEmailsKey(id); ok && address.Contact && emailEnabled {
|
}
|
||||||
|
if address, ok := app.storage.emails[id]; ok && address.Contact && emailEnabled {
|
||||||
err = app.email.send(email, address.Addr)
|
err = app.email.send(email, address.Addr)
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// return err
|
return err
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
return
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) getAddressOrName(jfID string) string {
|
func (app *appContext) getAddressOrName(jfID string) string {
|
||||||
if dcChat, ok := app.storage.GetDiscordKey(jfID); ok && dcChat.Contact && discordEnabled {
|
if dcChat, ok := app.storage.discord[jfID]; ok && dcChat.Contact && discordEnabled {
|
||||||
return RenderDiscordUsername(dcChat)
|
return RenderDiscordUsername(dcChat)
|
||||||
}
|
}
|
||||||
if tgChat, ok := app.storage.GetTelegramKey(jfID); ok && tgChat.Contact && telegramEnabled {
|
if tgChat, ok := app.storage.telegram[jfID]; ok && tgChat.Contact && telegramEnabled {
|
||||||
return "@" + tgChat.Username
|
return "@" + tgChat.Username
|
||||||
}
|
}
|
||||||
if addr, ok := app.storage.GetEmailsKey(jfID); ok {
|
if addr, ok := app.storage.emails[jfID]; ok {
|
||||||
return addr.Addr
|
return addr.Addr
|
||||||
}
|
}
|
||||||
if mxChat, ok := app.storage.GetMatrixKey(jfID); ok && mxChat.Contact && matrixEnabled {
|
|
||||||
return mxChat.UserID
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReverseUserSearch returns the jellyfin ID of the user with the given username, email, or contact method username.
|
|
||||||
// returns "" if none found. returns only the first match, might be an issue if there are users with the same contact method usernames.
|
|
||||||
func (app *appContext) ReverseUserSearch(address string) string {
|
|
||||||
user, status, err := app.jf.UserByName(address, false)
|
|
||||||
if status == 200 && err == nil {
|
|
||||||
return user.ID
|
|
||||||
}
|
|
||||||
for id, email := range app.storage.GetEmails() {
|
|
||||||
if strings.ToLower(address) == strings.ToLower(email.Addr) {
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for id, dcUser := range app.storage.GetDiscord() {
|
|
||||||
if RenderDiscordUsername(dcUser) == strings.ToLower(address) {
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tgUsername := strings.TrimPrefix(address, "@")
|
|
||||||
for id, tgUser := range app.storage.GetTelegram() {
|
|
||||||
if tgUsername == tgUser.Username {
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for id, mxUser := range app.storage.GetMatrix() {
|
|
||||||
if address == mxUser.UserID {
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -1,52 +0,0 @@
|
|||||||
{{ if .discordEnabled }}
|
|
||||||
<div id="modal-discord" class="modal">
|
|
||||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
|
||||||
<span class="heading mb-4">{{ .strings.linkDiscord }}</span>
|
|
||||||
<p class="content mb-4"> {{ .discordSendPINMessage }}</p>
|
|
||||||
<h1 class="text-center text-2xl mb-2 pin"></h1>
|
|
||||||
<div class="row center">
|
|
||||||
<a class="my-5 hover:underline">
|
|
||||||
<span class="mr-2">{{ .strings.joinTheServer }}</span>
|
|
||||||
<span id="discord-invite"></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<span class="button ~info @low full-width center mt-4" id="discord-waiting">{{ .strings.success }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
{{ if .telegramEnabled }}
|
|
||||||
<div id="modal-telegram" class="modal">
|
|
||||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
|
||||||
<span class="heading mb-4">{{ .strings.linkTelegram }}</span>
|
|
||||||
<p class="content mb-4">{{ .strings.sendPIN }}</p>
|
|
||||||
<p class="text-center text-2xl mb-2 pin"></p>
|
|
||||||
<a class="subheading link-center" href="{{ .telegramURL }}" target="_blank">
|
|
||||||
<span class="shield ~info mr-4">
|
|
||||||
<span class="icon">
|
|
||||||
<i class="ri-telegram-line"></i>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
@{{ .telegramUsername }}
|
|
||||||
</a>
|
|
||||||
<span class="button ~info @low full-width center mt-4" id="telegram-waiting">{{ .strings.success }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
{{ if .matrixEnabled }}
|
|
||||||
<div id="modal-matrix" class="modal">
|
|
||||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
|
||||||
<span class="heading mb-4">{{ .strings.linkMatrix }}</span>
|
|
||||||
<p class="content mb-4"> {{ .strings.matrixEnterUser }}</p>
|
|
||||||
<input type="text" class="input ~neutral @high" placeholder="@user:riot.im" id="matrix-userid">
|
|
||||||
<div class="subheading link-center mt-4">
|
|
||||||
<span class="shield ~info mr-4">
|
|
||||||
<span class="icon">
|
|
||||||
<i class="ri-chat-3-line"></i>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
{{ .matrixUser }}
|
|
||||||
</div>
|
|
||||||
<span class="button ~info @low full-width center mt-4" id="matrix-send">{{ .strings.submit }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
@ -22,7 +22,17 @@
|
|||||||
{{ template "header.html" . }}
|
{{ template "header.html" . }}
|
||||||
</head>
|
</head>
|
||||||
<body class="max-w-full overflow-x-hidden section">
|
<body class="max-w-full overflow-x-hidden section">
|
||||||
{{ template "login-modal.html" . }}
|
<div id="modal-login" class="modal">
|
||||||
|
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-login" href="">
|
||||||
|
<span class="heading">{{ .strings.login }}</span>
|
||||||
|
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="login-user">
|
||||||
|
<input type="password" class="field input ~neutral @high mb-4" placeholder="{{ .strings.password }}" id="login-password">
|
||||||
|
<label>
|
||||||
|
<input type="submit" class="unfocused">
|
||||||
|
<span class="button ~urge @low full-width center supra submit">{{ .strings.login }}</span>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<div id="modal-add-user" class="modal">
|
<div id="modal-add-user" class="modal">
|
||||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-add-user" href="">
|
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-add-user" href="">
|
||||||
<span class="heading">{{ .strings.newUser }} <span class="modal-close">×</span></span>
|
<span class="heading">{{ .strings.newUser }} <span class="modal-close">×</span></span>
|
||||||
@ -410,9 +420,6 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
|
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="top-4 right-4 absolute">
|
|
||||||
<a class="button ~info" href="/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
|
|
||||||
</div>
|
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<header class="flex flex-wrap items-center justify-between">
|
<header class="flex flex-wrap items-center justify-between">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="{{ .cssClass }}">
|
<html lang="en" class="{{ .cssClass }}">
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
|
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
|
||||||
{{ template "header.html" . }}
|
{{ template "header.html" . }}
|
||||||
<title>{{ .strings.successHeader }} - jfa-go</title>
|
<title>{{ .strings.successHeader }} - jfa-go</title>
|
||||||
</head>
|
</head>
|
||||||
|
@ -29,8 +29,6 @@
|
|||||||
window.captcha = {{ .captcha }};
|
window.captcha = {{ .captcha }};
|
||||||
window.reCAPTCHA = {{ .reCAPTCHA }};
|
window.reCAPTCHA = {{ .reCAPTCHA }};
|
||||||
window.reCAPTCHASiteKey = "{{ .reCAPTCHASiteKey }}";
|
window.reCAPTCHASiteKey = "{{ .reCAPTCHASiteKey }}";
|
||||||
window.userPageEnabled = {{ .userPageEnabled }};
|
|
||||||
window.userPageAddress = "{{ .userPageAddress }}";
|
|
||||||
</script>
|
</script>
|
||||||
{{ if .passwordReset }}
|
{{ if .passwordReset }}
|
||||||
<script src="js/pwr.js" type="module"></script>
|
<script src="js/pwr.js" type="module"></script>
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||||
<span class="heading mb-4">{{ if .passwordReset }}{{ .strings.passwordReset }}{{ else }}{{ .strings.successHeader }}{{ end }}</span>
|
<span class="heading mb-4">{{ if .passwordReset }}{{ .strings.passwordReset }}{{ else }}{{ .strings.successHeader }}{{ end }}</span>
|
||||||
<p class="content mb-4">{{ if .passwordReset }}{{ .strings.youCanLoginPassword }}{{ else }}{{ .successMessage }}{{ end }}</p>
|
<p class="content mb-4">{{ if .passwordReset }}{{ .strings.youCanLoginPassword }}{{ else }}{{ .successMessage }}{{ end }}</p>
|
||||||
{{ if .userPageEnabled }}<p class="content mb-4" id="modal-success-user-page-area" my-account-term="{{ .strings.myAccount }}">{{ .strings.userPageSuccessMessage }}</p>{{ end }}
|
|
||||||
<a class="button ~urge @low full-width center supra submit" href="{{ .jfLink }}" id="create-success-button">{{ .strings.continue }}</a>
|
<a class="button ~urge @low full-width center supra submit" href="{{ .jfLink }}" id="create-success-button">{{ .strings.continue }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -27,7 +26,53 @@
|
|||||||
<p class="content mb-4">{{ .strings.confirmationRequiredMessage }}</p>
|
<p class="content mb-4">{{ .strings.confirmationRequiredMessage }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ template "account-linking.html" . }}
|
{{ if .telegramEnabled }}
|
||||||
|
<div id="modal-telegram" class="modal">
|
||||||
|
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||||
|
<span class="heading mb-4">{{ .strings.linkTelegram }}</span>
|
||||||
|
<p class="content mb-4">{{ .strings.sendPIN }}</p>
|
||||||
|
<p class="text-center text-2xl mb-2">{{ .telegramPIN }}</p>
|
||||||
|
<a class="subheading link-center" href="{{ .telegramURL }}" target="_blank">
|
||||||
|
<span class="shield ~info mr-4">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="ri-telegram-line"></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
@{{ .telegramUsername }}
|
||||||
|
</a>
|
||||||
|
<span class="button ~info @low full-width center mt-4" id="telegram-waiting">{{ .strings.success }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ if .discordEnabled }}
|
||||||
|
<div id="modal-discord" class="modal">
|
||||||
|
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||||
|
<span class="heading mb-4">{{ .strings.linkDiscord }}</span>
|
||||||
|
<p class="content mb-4"> {{ .discordSendPINMessage }}</p>
|
||||||
|
<h1 class="text-center text-2xl mb-2">{{ .discordPIN }}</h1>
|
||||||
|
<a id="discord-invite"></a>
|
||||||
|
<span class="button ~info @low full-width center mt-4" id="discord-waiting">{{ .strings.success }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ if .matrixEnabled }}
|
||||||
|
<div id="modal-matrix" class="modal">
|
||||||
|
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||||
|
<span class="heading mb-4">{{ .strings.linkMatrix }}</span>
|
||||||
|
<p class="content mb-4"> {{ .strings.matrixEnterUser }}</p>
|
||||||
|
<input type="text" class="input ~neutral @high" placeholder="@user:riot.im" id="matrix-userid">
|
||||||
|
<div class="subheading link-center mt-4">
|
||||||
|
<span class="shield ~info mr-4">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="ri-chat-3-line"></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{{ .matrixUser }}
|
||||||
|
</div>
|
||||||
|
<span class="button ~info @low full-width center mt-4" id="matrix-send">{{ .strings.submit }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
<div class="top-4 left-4 absolute">
|
<div class="top-4 left-4 absolute">
|
||||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||||
<span class="button ~urge dropdown-button">
|
<span class="button ~urge dropdown-button">
|
||||||
@ -124,7 +169,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-initial">
|
<div class="flex-initial">
|
||||||
<div class="card ~neutral @low mb-4">
|
<div class="card ~neutral @low mb-4">
|
||||||
<span class="label supra">{{ .strings.passwordRequirementsHeader }}</span>
|
<span class="label supra" for="inv-uses">{{ .strings.passwordRequirementsHeader }}</span>
|
||||||
<ul>
|
<ul>
|
||||||
{{ range $key, $value := .requirements }}
|
{{ range $key, $value := .requirements }}
|
||||||
<li class="" id="requirement-{{ $key }}" min="{{ $value }}">
|
<li class="" id="requirement-{{ $key }}" min="{{ $value }}">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<meta charset="UTF-8"/>
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<meta name="Description" content="jfa-go, a better way to manage Jellyfin users.">
|
<meta name="Description" content="jfa-go, a better way to manage Jellyfin users.">
|
||||||
<meta name="color-scheme" content="dark light">
|
<meta name="color-scheme" content="dark light">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="{{ .cssClass }}">
|
<html lang="en" class="{{ .cssClass }}">
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
|
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
|
||||||
{{ template "header.html" . }}
|
{{ template "header.html" . }}
|
||||||
<title>Invalid Code - jfa-go</title>
|
<title>Invalid Code - jfa-go</title>
|
||||||
</head>
|
</head>
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
<div id="modal-login" class="modal">
|
|
||||||
<div class="my-[10%] row items-stretch relative mx-auto w-[40%] lg:w-[60%]">
|
|
||||||
{{ if index . "LoginMessageEnabled" }}
|
|
||||||
{{ if .LoginMessageEnabled }}
|
|
||||||
<div class="card mx-2 flex-initial w-[100%] xl:w-[35%] mb-4 xl:mb-0 dark:~d_neutral @low content">
|
|
||||||
{{ .LoginMessageContent }}
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
<form class="card mx-2 flex-auto form-login w-[100%] xl:w-[55%] mb-0" href="">
|
|
||||||
<span class="heading">{{ .strings.login }}</span>
|
|
||||||
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="login-user">
|
|
||||||
<input type="password" class="field input ~neutral @high mb-4" placeholder="{{ .strings.password }}" id="login-password">
|
|
||||||
<label>
|
|
||||||
<input type="submit" class="unfocused">
|
|
||||||
<span class="button ~urge @low full-width center supra submit">{{ .strings.login }}</span>
|
|
||||||
{{ if index . "pwrEnabled" }}
|
|
||||||
{{ if .pwrEnabled }}
|
|
||||||
<span class="button ~info @low full-width center supra submit my-2" id="modal-login-pwr">{{ .strings.resetPassword }}</span>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
158
html/user.html
158
html/user.html
@ -1,158 +0,0 @@
|
|||||||
<html lang="en" class="light">
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
|
|
||||||
<script>
|
|
||||||
window.URLBase = "{{ .urlBase }}";
|
|
||||||
window.notificationsEnabled = {{ .notifications }};
|
|
||||||
window.ombiEnabled = {{ .ombiEnabled }};
|
|
||||||
window.langFile = JSON.parse({{ .language }});
|
|
||||||
window.pwrEnabled = {{ .pwrEnabled }};
|
|
||||||
window.linkResetEnabled = {{ .linkResetEnabled }};
|
|
||||||
window.language = "{{ .langName }}";
|
|
||||||
window.telegramEnabled = {{ .telegramEnabled }};
|
|
||||||
window.telegramRequired = {{ .telegramRequired }};
|
|
||||||
window.telegramUsername = {{ .telegramUsername }};
|
|
||||||
window.telegramURL = {{ .telegramURL }};
|
|
||||||
window.emailEnabled = {{ .emailEnabled }};
|
|
||||||
window.emailRequired = {{ .emailRequired }};
|
|
||||||
window.discordEnabled = {{ .discordEnabled }};
|
|
||||||
window.discordRequired = {{ .discordRequired }};
|
|
||||||
window.discordServerName = "{{ .discordServerName }}";
|
|
||||||
window.discordInviteLink = {{ .discordInviteLink }};
|
|
||||||
window.discordSendPINMessage = "{{ .discordSendPINMessage }}";
|
|
||||||
window.matrixEnabled = {{ .matrixEnabled }};
|
|
||||||
window.matrixRequired = {{ .matrixRequired }};
|
|
||||||
window.matrixUserID = "{{ .matrixUser }}";
|
|
||||||
window.validationStrings = JSON.parse({{ .validationStrings }});
|
|
||||||
</script>
|
|
||||||
{{ template "header.html" . }}
|
|
||||||
<title>{{ .strings.myAccount }}</title>
|
|
||||||
</head>
|
|
||||||
<body class="max-w-full overflow-x-hidden section">
|
|
||||||
<div id="modal-email" class="modal">
|
|
||||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
|
||||||
<div class="content">
|
|
||||||
<span class="heading mb-4 my-2"></span>
|
|
||||||
<label class="label supra row m-1" for="modal-email-input">{{ .strings.emailAddress }}</label>
|
|
||||||
<div class="row">
|
|
||||||
<input type="email" class="col sm field ~neutral @low input" id="modal-email-input" placeholder="{{ .strings.emailAddress }}">
|
|
||||||
</div>
|
|
||||||
<button class="button ~urge @low supra full-width center lg my-2 modal-submit">{{ .strings.submit }}</button>
|
|
||||||
</div>
|
|
||||||
<div class="confirmation-required unfocused">
|
|
||||||
<span class="heading mb-4">{{ .strings.confirmationRequired }} <span class="modal-close">×</span></span>
|
|
||||||
<p class="content mb-4">{{ .strings.confirmationRequiredMessage }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ if .pwrEnabled }}
|
|
||||||
<div id="modal-pwr" class="modal">
|
|
||||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~neutral @low">
|
|
||||||
<span class="heading">{{ .strings.resetPassword }}</span>
|
|
||||||
<p class="content my-2">
|
|
||||||
{{ if .linkResetEnabled }}
|
|
||||||
{{ .strings.resetPasswordThroughLink }}
|
|
||||||
{{ else }}
|
|
||||||
{{ .strings.resetPasswordThroughJellyfin }}
|
|
||||||
{{ end }}
|
|
||||||
</p>
|
|
||||||
<div class="row">
|
|
||||||
<input type="text" class="col sm field ~neutral @low input" id="pwr-address" placeholder="username | example@example.com | user#1234 | @user:host | @username">
|
|
||||||
</div>
|
|
||||||
{{ if .linkResetEnabled }}
|
|
||||||
<span class="button ~info @low full-width center mt-4" id="pwr-submit">
|
|
||||||
{{ .strings.submit }}
|
|
||||||
</span>
|
|
||||||
{{ else }}
|
|
||||||
<a class="button ~info @low full-width center mt-4" href="{{ .jfLink }}" target="_blank">{{ .strings.continue }}</a>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
{{ template "login-modal.html" . }}
|
|
||||||
{{ template "account-linking.html" . }}
|
|
||||||
<div id="notification-box"></div>
|
|
||||||
<div class="top-4 left-4 absolute">
|
|
||||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
|
||||||
<span class="button ~urge dropdown-button">
|
|
||||||
<i class="ri-global-line"></i>
|
|
||||||
<span class="ml-2 chev"></span>
|
|
||||||
</span>
|
|
||||||
<div class="dropdown-display">
|
|
||||||
<div class="card ~neutral @low">
|
|
||||||
<label class="switch pb-4">
|
|
||||||
<input type="radio" name="lang-time" id="lang-12h">
|
|
||||||
<span>{{ .strings.time12h }}</span>
|
|
||||||
</label>
|
|
||||||
<label class="switch pb-4">
|
|
||||||
<input type="radio" name="lang-time" id="lang-24h">
|
|
||||||
<span>{{ .strings.time24h }}</span>
|
|
||||||
</label>
|
|
||||||
<div id="lang-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
|
|
||||||
<span class="button ~critical @low mb-4 unfocused" id="logout-button">{{ .strings.logout }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="top-4 right-4 absolute">
|
|
||||||
<a class="button ~info unfocused" href="/" id="admin-back-button"><i class="ri-arrow-left-fill mr-2"></i>{{ .strings.admin }}</a>
|
|
||||||
</div>
|
|
||||||
<div class="page-container unfocused">
|
|
||||||
<div class="card @low dark:~d_neutral mb-4" id="card-user">
|
|
||||||
<span class="heading mb-2"></span>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
||||||
{{ if index . "PageMessageEnabled" }}
|
|
||||||
{{ if .PageMessageEnabled }}
|
|
||||||
<div class="card @low dark:~d_neutral content" id="card-message">
|
|
||||||
{{ .PageMessageContent }}
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
<div class="card @low dark:~d_neutral flex-col" id="card-contact">
|
|
||||||
<span class="heading mb-2">{{ .strings.contactMethods }}</span>
|
|
||||||
<div class="content flex justify-between flex-col h-100"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="card @low dark:~d_neutral content" id="card-password">
|
|
||||||
<span class="heading row mb-2">{{ .strings.changePassword }}</span>
|
|
||||||
<div class="">
|
|
||||||
<div class="my-2">
|
|
||||||
<span class="label supra row">{{ .strings.passwordRequirementsHeader }}</span>
|
|
||||||
<ul>
|
|
||||||
{{ range $key, $value := .requirements }}
|
|
||||||
<li class="" id="requirement-{{ $key }}" min="{{ $value }}">
|
|
||||||
<span class="badge lg ~positive requirement-valid"></span> <span class="content requirement-content"></span>
|
|
||||||
</li>
|
|
||||||
{{ end }}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="my-2">
|
|
||||||
<label class="label supra" for="user-old-password">{{ .strings.oldPassword }}</label>
|
|
||||||
<input type="password" class="input ~neutral @low mt-2 mb-4" placeholder="{{ .strings.password }}" id="user-old-password" aria-label="{{ .strings.oldPassword }}">
|
|
||||||
<label class="label supra" for="user-new-password">{{ .strings.newPassword }}</label>
|
|
||||||
<input type="password" class="input ~neutral @low mt-2 mb-4" placeholder="{{ .strings.password }}" id="user-new-password" aria-label="{{ .strings.newPassword }}">
|
|
||||||
|
|
||||||
<label class="label supra" for="user-reenter-password">{{ .strings.reEnterPassword }}</label>
|
|
||||||
<input type="password" class="input ~neutral @low mt-2 mb-4" placeholder="{{ .strings.password }}" id="user-reenter-new-password" aria-label="{{ .strings.reEnterPassword }}">
|
|
||||||
<span class="button ~info @low full-width center mt-4" id="user-password-submit">
|
|
||||||
{{ .strings.changePassword }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="card @low dark:~d_neutral unfocused" id="card-status">
|
|
||||||
<span class="heading mb-2">{{ .strings.expiry }}</span>
|
|
||||||
<aside class="aside ~warning user-expiry my-4"></aside>
|
|
||||||
<div class="user-expiry-countdown"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="{{ .urlBase }}/js/user.js" type="module"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
10
lang.go
10
lang.go
@ -28,8 +28,6 @@ type commonLangs map[string]commonLang
|
|||||||
type commonLang struct {
|
type commonLang struct {
|
||||||
Meta langMeta `json:"meta"`
|
Meta langMeta `json:"meta"`
|
||||||
Strings langSection `json:"strings"`
|
Strings langSection `json:"strings"`
|
||||||
Notifications langSection `json:"notifications"`
|
|
||||||
QuantityStrings map[string]quantityString `json:"quantityStrings"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type adminLang struct {
|
type adminLang struct {
|
||||||
@ -40,9 +38,9 @@ type adminLang struct {
|
|||||||
JSON string
|
JSON string
|
||||||
}
|
}
|
||||||
|
|
||||||
type userLangs map[string]userLang
|
type formLangs map[string]formLang
|
||||||
|
|
||||||
func (ls *userLangs) getOptions() [][2]string {
|
func (ls *formLangs) getOptions() [][2]string {
|
||||||
opts := make([][2]string, len(*ls))
|
opts := make([][2]string, len(*ls))
|
||||||
i := 0
|
i := 0
|
||||||
for key, lang := range *ls {
|
for key, lang := range *ls {
|
||||||
@ -52,15 +50,13 @@ func (ls *userLangs) getOptions() [][2]string {
|
|||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
type userLang struct {
|
type formLang struct {
|
||||||
Meta langMeta `json:"meta"`
|
Meta langMeta `json:"meta"`
|
||||||
Strings langSection `json:"strings"`
|
Strings langSection `json:"strings"`
|
||||||
Notifications langSection `json:"notifications"`
|
Notifications langSection `json:"notifications"`
|
||||||
notificationsJSON string
|
notificationsJSON string
|
||||||
ValidationStrings map[string]quantityString `json:"validationStrings"`
|
ValidationStrings map[string]quantityString `json:"validationStrings"`
|
||||||
validationStringsJSON string
|
validationStringsJSON string
|
||||||
QuantityStrings map[string]quantityString `json:"quantityStrings"`
|
|
||||||
JSON string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type pwrLangs map[string]pwrLang
|
type pwrLangs map[string]pwrLang
|
||||||
|
@ -15,11 +15,20 @@
|
|||||||
"warning": "Advarsel",
|
"warning": "Advarsel",
|
||||||
"inviteInfiniteUsesWarning": "invitationer med uendelig brug kan blive misbrugt",
|
"inviteInfiniteUsesWarning": "invitationer med uendelig brug kan blive misbrugt",
|
||||||
"inviteSendToEmail": "Send til",
|
"inviteSendToEmail": "Send til",
|
||||||
|
"login": "Log på",
|
||||||
|
"logout": "Log ud",
|
||||||
"create": "Opret",
|
"create": "Opret",
|
||||||
"apply": "Anvend",
|
"apply": "Anvend",
|
||||||
|
"delete": "Slet",
|
||||||
|
"add": "Tilføj",
|
||||||
"select": "Vælg",
|
"select": "Vælg",
|
||||||
"name": "Navn",
|
"name": "Navn",
|
||||||
"date": "Dato",
|
"date": "Dato",
|
||||||
|
"enabled": "Aktiveret",
|
||||||
|
"disabled": "Deaktiveret",
|
||||||
|
"reEnable": "Genaktiver",
|
||||||
|
"disable": "Deaktiver",
|
||||||
|
"admin": "Administrator",
|
||||||
"updates": "Opdateringer",
|
"updates": "Opdateringer",
|
||||||
"update": "Opdatering",
|
"update": "Opdatering",
|
||||||
"download": "Hent",
|
"download": "Hent",
|
||||||
@ -28,6 +37,7 @@
|
|||||||
"lastActiveTime": "Sidst Aktiv",
|
"lastActiveTime": "Sidst Aktiv",
|
||||||
"from": "Fra",
|
"from": "Fra",
|
||||||
"user": "Bruger",
|
"user": "Bruger",
|
||||||
|
"expiry": "Udløb",
|
||||||
"userExpiry": "Brugerens Udløb",
|
"userExpiry": "Brugerens Udløb",
|
||||||
"userExpiryDescription": "En specificeret tid efter hver tilmelding, sletter/deaktiverer jfa-go kontoen. Du kan ændre denne adfærd i indstillingerne.",
|
"userExpiryDescription": "En specificeret tid efter hver tilmelding, sletter/deaktiverer jfa-go kontoen. Du kan ændre denne adfærd i indstillingerne.",
|
||||||
"aboutProgram": "Om",
|
"aboutProgram": "Om",
|
||||||
@ -44,6 +54,7 @@
|
|||||||
"conditionals": "Betingelser",
|
"conditionals": "Betingelser",
|
||||||
"preview": "Eksempel",
|
"preview": "Eksempel",
|
||||||
"reset": "Nulstil",
|
"reset": "Nulstil",
|
||||||
|
"edit": "Rediger",
|
||||||
"donate": "Doner",
|
"donate": "Doner",
|
||||||
"contactThrough": "Kontakt gennem:",
|
"contactThrough": "Kontakt gennem:",
|
||||||
"extendExpiry": "Forlæng udløb",
|
"extendExpiry": "Forlæng udløb",
|
||||||
@ -117,9 +128,13 @@
|
|||||||
"updateAppliedRefresh": "Opdatering anvendt, genindlæs venligst siden.",
|
"updateAppliedRefresh": "Opdatering anvendt, genindlæs venligst siden.",
|
||||||
"telegramVerified": "Telegram konto verificeret.",
|
"telegramVerified": "Telegram konto verificeret.",
|
||||||
"accountConnected": "Konto tilsluttet.",
|
"accountConnected": "Konto tilsluttet.",
|
||||||
|
"errorConnection": "Kunne ikke oprette forbindelse til jfa-go.",
|
||||||
|
"error401Unauthorized": "Adgang nægtet. Prøv at genindlæse siden.",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "Indstillingerne blev anvendt, men anvendelse af startskærmens layout mislykkedes muligvis.",
|
"errorSettingsAppliedNoHomescreenLayout": "Indstillingerne blev anvendt, men anvendelse af startskærmens layout mislykkedes muligvis.",
|
||||||
"errorHomescreenAppliedNoSettings": "Startskærmens layout blev anvendt, men anvendelsen af indstillingerne mislykkedes muligvis.",
|
"errorHomescreenAppliedNoSettings": "Startskærmens layout blev anvendt, men anvendelsen af indstillingerne mislykkedes muligvis.",
|
||||||
"errorSettingsFailed": "Ansøgningen mislykkedes.",
|
"errorSettingsFailed": "Ansøgningen mislykkedes.",
|
||||||
|
"errorLoginBlank": "Brugernavnet og/eller adgangskoden blev efterladt tomme.",
|
||||||
|
"errorUnknown": "Ukendt fejl.",
|
||||||
"errorSaveEmail": "Kunne ikke gemme e-mail.",
|
"errorSaveEmail": "Kunne ikke gemme e-mail.",
|
||||||
"errorBlankFields": "Felter blev efterladt tomme",
|
"errorBlankFields": "Felter blev efterladt tomme",
|
||||||
"errorDeleteProfile": "Kunne ikke slette profilen {n}",
|
"errorDeleteProfile": "Kunne ikke slette profilen {n}",
|
||||||
@ -127,6 +142,7 @@
|
|||||||
"errorCreateProfile": "Kunne ikke oprette profilen {n}",
|
"errorCreateProfile": "Kunne ikke oprette profilen {n}",
|
||||||
"errorSetDefaultProfile": "Standard profilen kunne ikke indstilles.",
|
"errorSetDefaultProfile": "Standard profilen kunne ikke indstilles.",
|
||||||
"errorLoadUsers": "Kunne ikke indlæse brugere.",
|
"errorLoadUsers": "Kunne ikke indlæse brugere.",
|
||||||
|
"errorSaveSettings": "Kunne ikke gemme indstillingerne.",
|
||||||
"errorLoadSettings": "Indstillingerne kunne ikke indlæses.",
|
"errorLoadSettings": "Indstillingerne kunne ikke indlæses.",
|
||||||
"errorSetOmbiDefaults": "Ombi standarderne kunne ikke gemmes.",
|
"errorSetOmbiDefaults": "Ombi standarderne kunne ikke gemmes.",
|
||||||
"errorLoadOmbiUsers": "Kunne ikke indlæse ombi brugere.",
|
"errorLoadOmbiUsers": "Kunne ikke indlæse ombi brugere.",
|
||||||
|
@ -13,8 +13,11 @@
|
|||||||
"warning": "Warnung",
|
"warning": "Warnung",
|
||||||
"inviteInfiniteUsesWarning": "Invites mit unendlich vielen Verwendungen können missbräuchlich verwendet werden",
|
"inviteInfiniteUsesWarning": "Invites mit unendlich vielen Verwendungen können missbräuchlich verwendet werden",
|
||||||
"inviteSendToEmail": "Senden an",
|
"inviteSendToEmail": "Senden an",
|
||||||
|
"login": "Anmelden",
|
||||||
|
"logout": "Abmelden",
|
||||||
"create": "Erstellen",
|
"create": "Erstellen",
|
||||||
"apply": "Anwenden",
|
"apply": "Anwenden",
|
||||||
|
"delete": "Löschen",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"date": "Datum",
|
"date": "Datum",
|
||||||
"lastActiveTime": "Zuletzt aktiv",
|
"lastActiveTime": "Zuletzt aktiv",
|
||||||
@ -65,6 +68,7 @@
|
|||||||
"variables": "Variablen",
|
"variables": "Variablen",
|
||||||
"preview": "Vorschau",
|
"preview": "Vorschau",
|
||||||
"reset": "Zurücksetzen",
|
"reset": "Zurücksetzen",
|
||||||
|
"edit": "Bearbeiten",
|
||||||
"customizeMessages": "Benachrichtigungen anpassen",
|
"customizeMessages": "Benachrichtigungen anpassen",
|
||||||
"customizeMessagesDescription": "Wenn du jfa-go's E-Mail-Vorlagen nicht benutzen willst, kannst du deinen eigenen unter Verwendung von Markdown erstellen.",
|
"customizeMessagesDescription": "Wenn du jfa-go's E-Mail-Vorlagen nicht benutzen willst, kannst du deinen eigenen unter Verwendung von Markdown erstellen.",
|
||||||
"announce": "Ankündigen",
|
"announce": "Ankündigen",
|
||||||
@ -75,16 +79,23 @@
|
|||||||
"search": "Suchen",
|
"search": "Suchen",
|
||||||
"userExpiry": "Benutzer Ablaufdatum",
|
"userExpiry": "Benutzer Ablaufdatum",
|
||||||
"inviteDuration": "Invite Dauer",
|
"inviteDuration": "Invite Dauer",
|
||||||
|
"enabled": "Aktiviert",
|
||||||
"userExpiryDescription": "Eine bestimmte Zeit nach der Anmeldung wird jfa-go das Konto löschen/deaktivieren. Du kannst dieses Verhalten in den Einstellungen ändern.",
|
"userExpiryDescription": "Eine bestimmte Zeit nach der Anmeldung wird jfa-go das Konto löschen/deaktivieren. Du kannst dieses Verhalten in den Einstellungen ändern.",
|
||||||
|
"disabled": "Deaktiviert",
|
||||||
|
"admin": "Admin",
|
||||||
"download": "Herunterladen",
|
"download": "Herunterladen",
|
||||||
"update": "Aktualisieren",
|
"update": "Aktualisieren",
|
||||||
"updates": "Aktualisierungen",
|
"updates": "Aktualisierungen",
|
||||||
|
"expiry": "Ablaufdatum",
|
||||||
"extendExpiry": "Ablaufdatum verlängern",
|
"extendExpiry": "Ablaufdatum verlängern",
|
||||||
|
"reEnable": "Wieder aktivieren",
|
||||||
|
"disable": "Deaktivieren",
|
||||||
"donate": "Spenden",
|
"donate": "Spenden",
|
||||||
"conditionals": "Bedingungen",
|
"conditionals": "Bedingungen",
|
||||||
"contactThrough": "Kontakt über:",
|
"contactThrough": "Kontakt über:",
|
||||||
"sendPIN": "Bitte den Benutzer, die unten stehende PIN an den Bot zu senden.",
|
"sendPIN": "Bitte den Benutzer, die unten stehende PIN an den Bot zu senden.",
|
||||||
"inviteMonths": "Monate",
|
"inviteMonths": "Monate",
|
||||||
|
"add": "Hinzufügen",
|
||||||
"select": "Auswählen",
|
"select": "Auswählen",
|
||||||
"searchDiscordUser": "Gib den Discord-Benutzername ein, um den Benutzer zu finden.",
|
"searchDiscordUser": "Gib den Discord-Benutzername ein, um den Benutzer zu finden.",
|
||||||
"findDiscordUser": "Suche Discord-Benutzer",
|
"findDiscordUser": "Suche Discord-Benutzer",
|
||||||
@ -111,15 +122,20 @@
|
|||||||
"createProfile": "Profil {n} erstellt.",
|
"createProfile": "Profil {n} erstellt.",
|
||||||
"saveSettings": "Einstellungen wurden gespeichert",
|
"saveSettings": "Einstellungen wurden gespeichert",
|
||||||
"setOmbiDefaults": "Ombi-Standardeinstellungen gespeichert.",
|
"setOmbiDefaults": "Ombi-Standardeinstellungen gespeichert.",
|
||||||
|
"errorConnection": "Konnte keine Verbindung zu jfa-go herstellen.",
|
||||||
|
"error401Unauthorized": "Unberechtigt. Versuch, die Seite zu aktualisieren.",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "Einstellungen wurden angewendet, aber die Anwendung des Startbildschirmlayouts ist möglicherweise fehlgeschlagen.",
|
"errorSettingsAppliedNoHomescreenLayout": "Einstellungen wurden angewendet, aber die Anwendung des Startbildschirmlayouts ist möglicherweise fehlgeschlagen.",
|
||||||
"errorHomescreenAppliedNoSettings": "Startbildschirmlayout wurde angewendet, aber die Anwendung der Einstellungen ist möglicherweise fehlgeschlagen.",
|
"errorHomescreenAppliedNoSettings": "Startbildschirmlayout wurde angewendet, aber die Anwendung der Einstellungen ist möglicherweise fehlgeschlagen.",
|
||||||
"errorSettingsFailed": "Anwendung ist fehlgeschlagen.",
|
"errorSettingsFailed": "Anwendung ist fehlgeschlagen.",
|
||||||
|
"errorLoginBlank": "Der Benutzername und/oder das Passwort wurden nicht ausgefüllt.",
|
||||||
|
"errorUnknown": "Unbekannter Fehler.",
|
||||||
"errorBlankFields": "Felder wurden nicht ausgefüllt",
|
"errorBlankFields": "Felder wurden nicht ausgefüllt",
|
||||||
"errorDeleteProfile": "Fehler beim Löschen des Profils {n}",
|
"errorDeleteProfile": "Fehler beim Löschen des Profils {n}",
|
||||||
"errorLoadProfiles": "Fehler beim Laden der Profile.",
|
"errorLoadProfiles": "Fehler beim Laden der Profile.",
|
||||||
"errorCreateProfile": "Fehler beim Erstellen des Profils {n}",
|
"errorCreateProfile": "Fehler beim Erstellen des Profils {n}",
|
||||||
"errorSetDefaultProfile": "Fehler beim Setzen des Standardprofils.",
|
"errorSetDefaultProfile": "Fehler beim Setzen des Standardprofils.",
|
||||||
"errorLoadUsers": "Fehler beim Laden der Benutzer.",
|
"errorLoadUsers": "Fehler beim Laden der Benutzer.",
|
||||||
|
"errorSaveSettings": "Einstellungen konnten nicht gespeichert werden.",
|
||||||
"errorLoadSettings": "Fehler beim Laden der Einstellungen.",
|
"errorLoadSettings": "Fehler beim Laden der Einstellungen.",
|
||||||
"errorSetOmbiDefaults": "Fehler beim Speichern der Ombi-Standardeinstellungen.",
|
"errorSetOmbiDefaults": "Fehler beim Speichern der Ombi-Standardeinstellungen.",
|
||||||
"errorLoadOmbiUsers": "Fehler beim Laden der Ombi-Benutzer.",
|
"errorLoadOmbiUsers": "Fehler beim Laden der Ombi-Benutzer.",
|
||||||
|
@ -13,8 +13,11 @@
|
|||||||
"warning": "Προσοχή",
|
"warning": "Προσοχή",
|
||||||
"inviteInfiniteUsesWarning": "μπορεί να γίνει κατάχρηση των προσκλήσεων με άπειρες χρήσεις",
|
"inviteInfiniteUsesWarning": "μπορεί να γίνει κατάχρηση των προσκλήσεων με άπειρες χρήσεις",
|
||||||
"inviteSendToEmail": "Αποστολή σε",
|
"inviteSendToEmail": "Αποστολή σε",
|
||||||
|
"login": "Σύνδεση",
|
||||||
|
"logout": "Αποσύνδεση",
|
||||||
"create": "Δημιουργία",
|
"create": "Δημιουργία",
|
||||||
"apply": "Εφαρμογή",
|
"apply": "Εφαρμογή",
|
||||||
|
"delete": "Διαγραφή",
|
||||||
"name": "Όνομα",
|
"name": "Όνομα",
|
||||||
"date": "Ημερομηνία",
|
"date": "Ημερομηνία",
|
||||||
"lastActiveTime": "Τελευταία Ενεργός",
|
"lastActiveTime": "Τελευταία Ενεργός",
|
||||||
@ -65,6 +68,7 @@
|
|||||||
"variables": "Μεταβλητές",
|
"variables": "Μεταβλητές",
|
||||||
"preview": "Προεπισκόπηση",
|
"preview": "Προεπισκόπηση",
|
||||||
"reset": "Επαναφορά",
|
"reset": "Επαναφορά",
|
||||||
|
"edit": "Επεξεργασία",
|
||||||
"customizeMessages": "Παραμετροποίηση Emails",
|
"customizeMessages": "Παραμετροποίηση Emails",
|
||||||
"advancedSettings": "Προχωρημένες Ρυθμίσεις",
|
"advancedSettings": "Προχωρημένες Ρυθμίσεις",
|
||||||
"customizeMessagesDescription": "Αν δεν θέλετε να ζρησιμοποιήσετε τα πρότυπα email του jfa-go, μπορείτε να δημιουργήσετε τα δικά σας με χρήση Markdown.",
|
"customizeMessagesDescription": "Αν δεν θέλετε να ζρησιμοποιήσετε τα πρότυπα email του jfa-go, μπορείτε να δημιουργήσετε τα δικά σας με χρήση Markdown.",
|
||||||
@ -73,6 +77,10 @@
|
|||||||
"download": "Λήψη",
|
"download": "Λήψη",
|
||||||
"search": "Αναζήτηση",
|
"search": "Αναζήτηση",
|
||||||
"inviteDuration": "Διάρκεια Πρόσκλησης",
|
"inviteDuration": "Διάρκεια Πρόσκλησης",
|
||||||
|
"enabled": "Ενεργοποιημένο",
|
||||||
|
"disabled": "Απενεργοποιημένο",
|
||||||
|
"admin": "Διαχειριστής",
|
||||||
|
"expiry": "Λήξη",
|
||||||
"userExpiry": "Λήξη Χρήστη",
|
"userExpiry": "Λήξη Χρήστη",
|
||||||
"userExpiryDescription": "Μετά απο ένα καθορισμένο χρόνο μετά απο κάθε εγγραφή, το jfa-go θα διαγράφει/απενεργοποιεί τον λογαριασμό. Μπορείτε να αλλάξετε αυτή την συμπεριφορά στις ρυθμίσεις.",
|
"userExpiryDescription": "Μετά απο ένα καθορισμένο χρόνο μετά απο κάθε εγγραφή, το jfa-go θα διαγράφει/απενεργοποιεί τον λογαριασμό. Μπορείτε να αλλάξετε αυτή την συμπεριφορά στις ρυθμίσεις.",
|
||||||
"announce": "Ανακοίνωση",
|
"announce": "Ανακοίνωση",
|
||||||
@ -80,6 +88,8 @@
|
|||||||
"message": "Μήνυμα",
|
"message": "Μήνυμα",
|
||||||
"extendExpiry": "Παράταση λήξης",
|
"extendExpiry": "Παράταση λήξης",
|
||||||
"markdownSupported": "Το Markdown υποστυρίζεται.",
|
"markdownSupported": "Το Markdown υποστυρίζεται.",
|
||||||
|
"reEnable": "Επανα-ενεργοποίηση",
|
||||||
|
"disable": "Απενεργοποίηση",
|
||||||
"inviteMonths": "Μήνες"
|
"inviteMonths": "Μήνες"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
@ -88,15 +98,20 @@
|
|||||||
"createProfile": "Δημιουργήθηκε το {n} προφίλ.",
|
"createProfile": "Δημιουργήθηκε το {n} προφίλ.",
|
||||||
"saveSettings": "Οι ρυθμίσεις αποθηκεύτηκαν",
|
"saveSettings": "Οι ρυθμίσεις αποθηκεύτηκαν",
|
||||||
"setOmbiDefaults": "Αποθηκεύτηκαν οι προκαθορισμένες ρυθμίσεις του ombi.",
|
"setOmbiDefaults": "Αποθηκεύτηκαν οι προκαθορισμένες ρυθμίσεις του ombi.",
|
||||||
|
"errorConnection": "Δεν μπόρεσε να συνδεθεί με το jfa-go.",
|
||||||
|
"error401Unauthorized": "Ανεξουσιοδότητος. Προσπαθήστε να κάνετε επαναφόρτωση την σελίδα.",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "Οι ρυθμίσεις αποθηκεύτηκαν, αλλά η καταχώρηση δομής αρχικής οθόνης ίσως απέτυχε.",
|
"errorSettingsAppliedNoHomescreenLayout": "Οι ρυθμίσεις αποθηκεύτηκαν, αλλά η καταχώρηση δομής αρχικής οθόνης ίσως απέτυχε.",
|
||||||
"errorHomescreenAppliedNoSettings": "Η δομή αρχικής οθόνης εφαρμόστηκε, αλλά οι ρυθμίσεις ίσως απέτυχαν.",
|
"errorHomescreenAppliedNoSettings": "Η δομή αρχικής οθόνης εφαρμόστηκε, αλλά οι ρυθμίσεις ίσως απέτυχαν.",
|
||||||
"errorSettingsFailed": "Η εφαρμογή απέτυχε.",
|
"errorSettingsFailed": "Η εφαρμογή απέτυχε.",
|
||||||
|
"errorLoginBlank": "Το όνομα χρήστη και/ή ο κωδικός ήταν κενά.",
|
||||||
|
"errorUnknown": "Άγνωστο σφάλμα.",
|
||||||
"errorBlankFields": "Τα πεφία ήταν κενά",
|
"errorBlankFields": "Τα πεφία ήταν κενά",
|
||||||
"errorDeleteProfile": "Αποτυχία διαγραφής του προφίλ {n}",
|
"errorDeleteProfile": "Αποτυχία διαγραφής του προφίλ {n}",
|
||||||
"errorLoadProfiles": "Αποτυχία φόρτωσης των προφίλ.",
|
"errorLoadProfiles": "Αποτυχία φόρτωσης των προφίλ.",
|
||||||
"errorCreateProfile": "Αποτυχία δημιουργίας του προφίλ {n}",
|
"errorCreateProfile": "Αποτυχία δημιουργίας του προφίλ {n}",
|
||||||
"errorSetDefaultProfile": "Αποτυχία ορισμού του προκαθορισμένου προφίλ.",
|
"errorSetDefaultProfile": "Αποτυχία ορισμού του προκαθορισμένου προφίλ.",
|
||||||
"errorLoadUsers": "Αποτυχία φόρτωσης χρηστών.",
|
"errorLoadUsers": "Αποτυχία φόρτωσης χρηστών.",
|
||||||
|
"errorSaveSettings": "Αποτυχία αποθήκευσης ρυθμίσεων.",
|
||||||
"errorLoadSettings": "Αποτυχία φόρτωσης ρυθμίσεων.",
|
"errorLoadSettings": "Αποτυχία φόρτωσης ρυθμίσεων.",
|
||||||
"errorSetOmbiDefaults": "Αποτυχία αποθήκευσης προκαθορισμένων ρυθμίσεων για το Ombi.",
|
"errorSetOmbiDefaults": "Αποτυχία αποθήκευσης προκαθορισμένων ρυθμίσεων για το Ombi.",
|
||||||
"errorLoadOmbiUsers": "Αποτυχία φόρτωσης χρηστών Ombi.",
|
"errorLoadOmbiUsers": "Αποτυχία φόρτωσης χρηστών Ombi.",
|
||||||
|
@ -68,8 +68,12 @@
|
|||||||
"inviteHours": "Hours",
|
"inviteHours": "Hours",
|
||||||
"inviteInfiniteUsesWarning": "invites with infinite uses can be used abusively",
|
"inviteInfiniteUsesWarning": "invites with infinite uses can be used abusively",
|
||||||
"inviteSendToEmail": "Send to",
|
"inviteSendToEmail": "Send to",
|
||||||
|
"login": "Login",
|
||||||
|
"logout": "Logout",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
|
"delete": "Delete",
|
||||||
"updates": "Updates",
|
"updates": "Updates",
|
||||||
|
"expiry": "Expiry",
|
||||||
"variables": "Variables",
|
"variables": "Variables",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"markdownSupported": "Markdown is supported.",
|
"markdownSupported": "Markdown is supported.",
|
||||||
@ -92,10 +96,16 @@
|
|||||||
"contactThrough": "Contact through:",
|
"contactThrough": "Contact through:",
|
||||||
"select": "Select",
|
"select": "Select",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"disable": "Disable",
|
||||||
|
"edit": "Edit",
|
||||||
"extendExpiry": "Extend expiry",
|
"extendExpiry": "Extend expiry",
|
||||||
"sendPWR": "Send Password Reset",
|
"sendPWR": "Send Password Reset",
|
||||||
"inviteMonths": "Months",
|
"inviteMonths": "Months",
|
||||||
"inviteDuration": "Invite Duration",
|
"inviteDuration": "Invite Duration",
|
||||||
|
"add": "Add",
|
||||||
|
"reEnable": "Re-enable",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
"userExpiryDescription": "A specified amount of time after each signup, jfa-go will delete/disable the account. You can change this behaviour in settings.",
|
"userExpiryDescription": "A specified amount of time after each signup, jfa-go will delete/disable the account. You can change this behaviour in settings.",
|
||||||
@ -140,6 +150,7 @@
|
|||||||
"settingsApplyRestartLater": "Apply, restart later",
|
"settingsApplyRestartLater": "Apply, restart later",
|
||||||
"subject": "Subject",
|
"subject": "Subject",
|
||||||
"setExpiry": "Set expiry",
|
"setExpiry": "Set expiry",
|
||||||
|
"admin": "Admin",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"advancedSettings": "Advanced Settings",
|
"advancedSettings": "Advanced Settings",
|
||||||
@ -164,7 +175,9 @@
|
|||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorSettingsFailed": "Application failed.",
|
"errorSettingsFailed": "Application failed.",
|
||||||
|
"errorLoginBlank": "The username and/or password was left blank.",
|
||||||
"errorLoadSettings": "Failed to load settings.",
|
"errorLoadSettings": "Failed to load settings.",
|
||||||
|
"errorUnknown": "Unknown error.",
|
||||||
"errorDeleteProfile": "Failed to delete profile {n}",
|
"errorDeleteProfile": "Failed to delete profile {n}",
|
||||||
"sentAnnouncement": "Announcement sent.",
|
"sentAnnouncement": "Announcement sent.",
|
||||||
"savedAnnouncement": "Announcement saved.",
|
"savedAnnouncement": "Announcement saved.",
|
||||||
@ -173,11 +186,13 @@
|
|||||||
"updateAppliedRefresh": "Update applied, please refresh.",
|
"updateAppliedRefresh": "Update applied, please refresh.",
|
||||||
"telegramVerified": "Telegram account verified.",
|
"telegramVerified": "Telegram account verified.",
|
||||||
"accountConnected": "Account connected.",
|
"accountConnected": "Account connected.",
|
||||||
|
"error401Unauthorized": "Unauthorised. Try refreshing the page.",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
||||||
"errorSaveEmail": "Failed to save email.",
|
"errorSaveEmail": "Failed to save email.",
|
||||||
"errorLoadProfiles": "Failed to load profiles.",
|
"errorLoadProfiles": "Failed to load profiles.",
|
||||||
"errorCreateProfile": "Failed to create profile {n}",
|
"errorCreateProfile": "Failed to create profile {n}",
|
||||||
"errorLoadUsers": "Failed to load users.",
|
"errorLoadUsers": "Failed to load users.",
|
||||||
|
"errorSaveSettings": "Couldn't save settings.",
|
||||||
"errorSetOmbiProfile": "Failed to store ombi profile.",
|
"errorSetOmbiProfile": "Failed to store ombi profile.",
|
||||||
"errorLoadOmbiUsers": "Failed to load ombi users.",
|
"errorLoadOmbiUsers": "Failed to load ombi users.",
|
||||||
"errorFailureCheckLogs": "Failed (check console/logs)",
|
"errorFailureCheckLogs": "Failed (check console/logs)",
|
||||||
@ -192,6 +207,7 @@
|
|||||||
"saveEmail": "Email saved.",
|
"saveEmail": "Email saved.",
|
||||||
"createProfile": "Created profile {n}.",
|
"createProfile": "Created profile {n}.",
|
||||||
"saveSettings": "Settings were saved",
|
"saveSettings": "Settings were saved",
|
||||||
|
"errorConnection": "Couldn't connect to jfa-go.",
|
||||||
"errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.",
|
"errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.",
|
||||||
"errorBlankFields": "Fields were left blank",
|
"errorBlankFields": "Fields were left blank",
|
||||||
"errorSetDefaultProfile": "Failed to set default profile.",
|
"errorSetDefaultProfile": "Failed to set default profile.",
|
||||||
|
@ -15,12 +15,21 @@
|
|||||||
"warning": "Warning",
|
"warning": "Warning",
|
||||||
"inviteInfiniteUsesWarning": "invites with infinite uses can be used abusively",
|
"inviteInfiniteUsesWarning": "invites with infinite uses can be used abusively",
|
||||||
"inviteSendToEmail": "Send to",
|
"inviteSendToEmail": "Send to",
|
||||||
|
"login": "Login",
|
||||||
|
"logout": "Logout",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
|
"delete": "Delete",
|
||||||
|
"add": "Add",
|
||||||
"select": "Select",
|
"select": "Select",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"reEnable": "Re-enable",
|
||||||
"setExpiry": "Set expiry",
|
"setExpiry": "Set expiry",
|
||||||
|
"disable": "Disable",
|
||||||
|
"admin": "Admin",
|
||||||
"updates": "Updates",
|
"updates": "Updates",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
@ -31,6 +40,7 @@
|
|||||||
"after": "After",
|
"after": "After",
|
||||||
"before": "Before",
|
"before": "Before",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
|
"expiry": "Expiry",
|
||||||
"userExpiry": "User Expiry",
|
"userExpiry": "User Expiry",
|
||||||
"userExpiryDescription": "A specified amount of time after each signup, jfa-go will delete/disable the account. You can change this behaviour in settings.",
|
"userExpiryDescription": "A specified amount of time after each signup, jfa-go will delete/disable the account. You can change this behaviour in settings.",
|
||||||
"aboutProgram": "About",
|
"aboutProgram": "About",
|
||||||
@ -49,6 +59,7 @@
|
|||||||
"conditionals": "Conditionals",
|
"conditionals": "Conditionals",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
|
"edit": "Edit",
|
||||||
"donate": "Donate",
|
"donate": "Donate",
|
||||||
"unlink": "Unlink Account",
|
"unlink": "Unlink Account",
|
||||||
"sendPWR": "Send Password Reset",
|
"sendPWR": "Send Password Reset",
|
||||||
@ -113,9 +124,7 @@
|
|||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"searchOptions": "Search Options",
|
"searchOptions": "Search Options",
|
||||||
"matchText": "Match Text",
|
"matchText": "Match Text",
|
||||||
"jellyfinID": "Jellyfin ID",
|
"jellyfinID": "Jellyfin ID"
|
||||||
"userPageLogin": "User Page: Login",
|
|
||||||
"userPagePage": "User Page: Page"
|
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"changedEmailAddress": "Changed email address of {n}.",
|
"changedEmailAddress": "Changed email address of {n}.",
|
||||||
@ -130,9 +139,13 @@
|
|||||||
"updateAppliedRefresh": "Update applied, please refresh.",
|
"updateAppliedRefresh": "Update applied, please refresh.",
|
||||||
"telegramVerified": "Telegram account verified.",
|
"telegramVerified": "Telegram account verified.",
|
||||||
"accountConnected": "Account connected.",
|
"accountConnected": "Account connected.",
|
||||||
|
"errorConnection": "Couldn't connect to jfa-go.",
|
||||||
|
"error401Unauthorized": "Unauthorized. Try refreshing the page.",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
||||||
"errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.",
|
"errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.",
|
||||||
"errorSettingsFailed": "Application failed.",
|
"errorSettingsFailed": "Application failed.",
|
||||||
|
"errorLoginBlank": "The username and/or password were left blank.",
|
||||||
|
"errorUnknown": "Unknown error.",
|
||||||
"errorSaveEmail": "Failed to save email.",
|
"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}",
|
||||||
@ -140,6 +153,7 @@
|
|||||||
"errorCreateProfile": "Failed to create profile {n}",
|
"errorCreateProfile": "Failed to create profile {n}",
|
||||||
"errorSetDefaultProfile": "Failed to set default profile.",
|
"errorSetDefaultProfile": "Failed to set default profile.",
|
||||||
"errorLoadUsers": "Failed to load users.",
|
"errorLoadUsers": "Failed to load users.",
|
||||||
|
"errorSaveSettings": "Couldn't save settings.",
|
||||||
"errorLoadSettings": "Failed to load settings.",
|
"errorLoadSettings": "Failed to load settings.",
|
||||||
"errorSetOmbiProfile": "Failed to store ombi profile.",
|
"errorSetOmbiProfile": "Failed to store ombi profile.",
|
||||||
"errorLoadOmbiUsers": "Failed to load ombi users.",
|
"errorLoadOmbiUsers": "Failed to load ombi users.",
|
||||||
|
@ -15,10 +15,18 @@
|
|||||||
"warning": "Advertencia",
|
"warning": "Advertencia",
|
||||||
"inviteInfiniteUsesWarning": "Las invitaciones con usos infinitos se pueden usar de forma abusiva",
|
"inviteInfiniteUsesWarning": "Las invitaciones con usos infinitos se pueden usar de forma abusiva",
|
||||||
"inviteSendToEmail": "Enviar a",
|
"inviteSendToEmail": "Enviar a",
|
||||||
|
"login": "Acceso",
|
||||||
|
"logout": "Cerrar sesión",
|
||||||
"create": "Crear",
|
"create": "Crear",
|
||||||
"apply": "Aplicar",
|
"apply": "Aplicar",
|
||||||
|
"delete": "Eliminar",
|
||||||
"name": "Nombre",
|
"name": "Nombre",
|
||||||
"date": "Fecha",
|
"date": "Fecha",
|
||||||
|
"enabled": "Activado",
|
||||||
|
"disabled": "Desactivado",
|
||||||
|
"reEnable": "Reactivar",
|
||||||
|
"disable": "Desactivar",
|
||||||
|
"admin": "Administrador",
|
||||||
"updates": "Actualizaciones",
|
"updates": "Actualizaciones",
|
||||||
"update": "Actualizar",
|
"update": "Actualizar",
|
||||||
"download": "Descargar",
|
"download": "Descargar",
|
||||||
@ -27,6 +35,7 @@
|
|||||||
"lastActiveTime": "Último activo",
|
"lastActiveTime": "Último activo",
|
||||||
"from": "De",
|
"from": "De",
|
||||||
"user": "Usuario",
|
"user": "Usuario",
|
||||||
|
"expiry": "Expiración",
|
||||||
"userExpiry": "Caducidad del usuario",
|
"userExpiry": "Caducidad del usuario",
|
||||||
"userExpiryDescription": "Una cantidad de tiempo específica después de cada registro, jfa-go eliminará / deshabilitará la cuenta. Puede cambiar este comportamiento en la configuración.",
|
"userExpiryDescription": "Una cantidad de tiempo específica después de cada registro, jfa-go eliminará / deshabilitará la cuenta. Puede cambiar este comportamiento en la configuración.",
|
||||||
"aboutProgram": "Acerca de",
|
"aboutProgram": "Acerca de",
|
||||||
@ -42,6 +51,7 @@
|
|||||||
"variables": "Variables",
|
"variables": "Variables",
|
||||||
"preview": "Vista previa",
|
"preview": "Vista previa",
|
||||||
"reset": "Reiniciar",
|
"reset": "Reiniciar",
|
||||||
|
"edit": "Editar",
|
||||||
"extendExpiry": "Extender el vencimiento",
|
"extendExpiry": "Extender el vencimiento",
|
||||||
"customizeMessages": "Personalizar mensajes",
|
"customizeMessages": "Personalizar mensajes",
|
||||||
"customizeMessagesDescription": "Si no desea utilizar las plantillas de mensajes de jfa-go, puede crear las suyas con Markdown.",
|
"customizeMessagesDescription": "Si no desea utilizar las plantillas de mensajes de jfa-go, puede crear las suyas con Markdown.",
|
||||||
@ -83,6 +93,7 @@
|
|||||||
"notifyUserCreation": "Sobre la creación de usuarios",
|
"notifyUserCreation": "Sobre la creación de usuarios",
|
||||||
"conditionals": "Condicionales",
|
"conditionals": "Condicionales",
|
||||||
"donate": "Donar",
|
"donate": "Donar",
|
||||||
|
"add": "Agregar",
|
||||||
"templates": "Plantillas",
|
"templates": "Plantillas",
|
||||||
"contactThrough": "Contactar a través de:",
|
"contactThrough": "Contactar a través de:",
|
||||||
"select": "Seleccionar",
|
"select": "Seleccionar",
|
||||||
@ -114,9 +125,13 @@
|
|||||||
"sentAnnouncement": "Anuncio enviado.",
|
"sentAnnouncement": "Anuncio enviado.",
|
||||||
"setOmbiDefaults": "Valores predeterminados de ombi almacenados.",
|
"setOmbiDefaults": "Valores predeterminados de ombi almacenados.",
|
||||||
"updateApplied": "Actualización aplicada, por favor reinicie.",
|
"updateApplied": "Actualización aplicada, por favor reinicie.",
|
||||||
|
"errorConnection": "No se pudo conectar a jfa-go.",
|
||||||
|
"error401Unauthorized": "No autorizado. Intente actualizar la página.",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "Se aplicó la configuración, pero es posible que no se haya aplicado el diseño de la pantalla de inicio.",
|
"errorSettingsAppliedNoHomescreenLayout": "Se aplicó la configuración, pero es posible que no se haya aplicado el diseño de la pantalla de inicio.",
|
||||||
"errorHomescreenAppliedNoSettings": "Se aplicó el diseño de la pantalla de inicio, pero es posible que la aplicación de la configuración haya fallado.",
|
"errorHomescreenAppliedNoSettings": "Se aplicó el diseño de la pantalla de inicio, pero es posible que la aplicación de la configuración haya fallado.",
|
||||||
"errorSettingsFailed": "La aplicación falló.",
|
"errorSettingsFailed": "La aplicación falló.",
|
||||||
|
"errorLoginBlank": "El nombre de usuario y/o la contraseña se dejaron en blanco.",
|
||||||
|
"errorUnknown": "Error desconocido.",
|
||||||
"errorSaveEmail": "No se pudo guardar el correo electrónico.",
|
"errorSaveEmail": "No se pudo guardar el correo electrónico.",
|
||||||
"errorBlankFields": "Los campos se dejaron en blanco",
|
"errorBlankFields": "Los campos se dejaron en blanco",
|
||||||
"errorDeleteProfile": "No se pudo borrar el perfil {n}",
|
"errorDeleteProfile": "No se pudo borrar el perfil {n}",
|
||||||
@ -124,6 +139,7 @@
|
|||||||
"errorCreateProfile": "No se pudo crear el perfil {n}",
|
"errorCreateProfile": "No se pudo crear el perfil {n}",
|
||||||
"errorSetDefaultProfile": "No se pudo establecer el perfil predeterminado.",
|
"errorSetDefaultProfile": "No se pudo establecer el perfil predeterminado.",
|
||||||
"errorLoadUsers": "No se pudieron cargar los usuarios.",
|
"errorLoadUsers": "No se pudieron cargar los usuarios.",
|
||||||
|
"errorSaveSettings": "No se pudo guardar la configuración.",
|
||||||
"errorLoadSettings": "No se pudo cargar la configuración.",
|
"errorLoadSettings": "No se pudo cargar la configuración.",
|
||||||
"errorSetOmbiDefaults": "No se pudieron almacenar los valores predeterminados de ombi.",
|
"errorSetOmbiDefaults": "No se pudieron almacenar los valores predeterminados de ombi.",
|
||||||
"errorLoadOmbiUsers": "No se pudieron cargar los usuarios de Ombi.",
|
"errorLoadOmbiUsers": "No se pudieron cargar los usuarios de Ombi.",
|
||||||
|
@ -15,8 +15,11 @@
|
|||||||
"warning": "Attention",
|
"warning": "Attention",
|
||||||
"inviteInfiniteUsesWarning": "les invitations infinies peuvent être utilisées abusivement",
|
"inviteInfiniteUsesWarning": "les invitations infinies peuvent être utilisées abusivement",
|
||||||
"inviteSendToEmail": "Envoyer à",
|
"inviteSendToEmail": "Envoyer à",
|
||||||
|
"login": "S'identifier",
|
||||||
|
"logout": "Se déconnecter",
|
||||||
"create": "Créer",
|
"create": "Créer",
|
||||||
"apply": "Appliquer",
|
"apply": "Appliquer",
|
||||||
|
"delete": "Effacer",
|
||||||
"name": "Nom",
|
"name": "Nom",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"lastActiveTime": "Dernière activité",
|
"lastActiveTime": "Dernière activité",
|
||||||
@ -72,8 +75,15 @@
|
|||||||
"variables": "Variables",
|
"variables": "Variables",
|
||||||
"preview": "Aperçu",
|
"preview": "Aperçu",
|
||||||
"reset": "Réinitialisation",
|
"reset": "Réinitialisation",
|
||||||
|
"edit": "Éditer",
|
||||||
"customizeMessages": "Personnaliser les e-mails",
|
"customizeMessages": "Personnaliser les e-mails",
|
||||||
"inviteDuration": "Durée de l'invitation",
|
"inviteDuration": "Durée de l'invitation",
|
||||||
|
"enabled": "Activé",
|
||||||
|
"disabled": "Désactivé",
|
||||||
|
"reEnable": "Ré-activé",
|
||||||
|
"disable": "Désactivé",
|
||||||
|
"admin": "Administrateur",
|
||||||
|
"expiry": "Expiration",
|
||||||
"advancedSettings": "Paramètres avancés",
|
"advancedSettings": "Paramètres avancés",
|
||||||
"userExpiry": "Expiration de l'utilisateur",
|
"userExpiry": "Expiration de l'utilisateur",
|
||||||
"updates": "Mises à jour",
|
"updates": "Mises à jour",
|
||||||
@ -86,6 +96,7 @@
|
|||||||
"extendExpiry": "Prolonger l'expiration",
|
"extendExpiry": "Prolonger l'expiration",
|
||||||
"contactThrough": "Contacté par :",
|
"contactThrough": "Contacté par :",
|
||||||
"sendPIN": "Demandez à l'utilisateur d'envoyer le code PIN ci-dessous au bot.",
|
"sendPIN": "Demandez à l'utilisateur d'envoyer le code PIN ci-dessous au bot.",
|
||||||
|
"add": "Ajouter",
|
||||||
"select": "Sélectionner",
|
"select": "Sélectionner",
|
||||||
"findDiscordUser": "Trouver l'utilisateur Discord",
|
"findDiscordUser": "Trouver l'utilisateur Discord",
|
||||||
"linkMatrixDescription": "Entrez le nom d'utilisateur et le mot de passe de l'utilisateur pour l’utilisateur comme bot. Une fois soumis, l'application va redémarrer.",
|
"linkMatrixDescription": "Entrez le nom d'utilisateur et le mot de passe de l'utilisateur pour l’utilisateur comme bot. Une fois soumis, l'application va redémarrer.",
|
||||||
@ -112,15 +123,20 @@
|
|||||||
"createProfile": "Profil créé {n}.",
|
"createProfile": "Profil créé {n}.",
|
||||||
"saveSettings": "Les paramètres ont été enregistrés",
|
"saveSettings": "Les paramètres ont été enregistrés",
|
||||||
"setOmbiDefaults": "Valeurs par défaut de Ombi.",
|
"setOmbiDefaults": "Valeurs par défaut de Ombi.",
|
||||||
|
"errorConnection": "Impossible de se connecter à jfa-go.",
|
||||||
|
"error401Unauthorized": "Non autorisé. Essayez d'actualiser la page.",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "Les paramètres ont été appliqués, mais l'application de la disposition de l'écran d'accueil a peut-être échoué.",
|
"errorSettingsAppliedNoHomescreenLayout": "Les paramètres ont été appliqués, mais l'application de la disposition de l'écran d'accueil a peut-être échoué.",
|
||||||
"errorHomescreenAppliedNoSettings": "La disposition de l'écran d'accueil a été appliquée, mais l'application des paramètres a peut-être échoué.",
|
"errorHomescreenAppliedNoSettings": "La disposition de l'écran d'accueil a été appliquée, mais l'application des paramètres a peut-être échoué.",
|
||||||
"errorSettingsFailed": "L'application a échoué.",
|
"errorSettingsFailed": "L'application a échoué.",
|
||||||
|
"errorLoginBlank": "Le nom d'utilisateur et/ou le mot de passe sont vides.",
|
||||||
|
"errorUnknown": "Erreur inconnue.",
|
||||||
"errorBlankFields": "Les champs sont vides",
|
"errorBlankFields": "Les champs sont vides",
|
||||||
"errorDeleteProfile": "Échec de la suppression du profil {n}",
|
"errorDeleteProfile": "Échec de la suppression du profil {n}",
|
||||||
"errorLoadProfiles": "Échec du chargement des profils.",
|
"errorLoadProfiles": "Échec du chargement des profils.",
|
||||||
"errorCreateProfile": "Échec de la création du profil {n}",
|
"errorCreateProfile": "Échec de la création du profil {n}",
|
||||||
"errorSetDefaultProfile": "Échec de la définition du profil par défaut.",
|
"errorSetDefaultProfile": "Échec de la définition du profil par défaut.",
|
||||||
"errorLoadUsers": "Échec du chargement des utilisateurs.",
|
"errorLoadUsers": "Échec du chargement des utilisateurs.",
|
||||||
|
"errorSaveSettings": "Impossible d'enregistrer les paramètres.",
|
||||||
"errorLoadSettings": "Échec du chargement des paramètres.",
|
"errorLoadSettings": "Échec du chargement des paramètres.",
|
||||||
"errorSetOmbiDefaults": "Impossible de stocker les valeurs par défaut d'Ombi.",
|
"errorSetOmbiDefaults": "Impossible de stocker les valeurs par défaut d'Ombi.",
|
||||||
"errorLoadOmbiUsers": "Échec du chargement des utilisateurs Ombi.",
|
"errorLoadOmbiUsers": "Échec du chargement des utilisateurs Ombi.",
|
||||||
|
@ -15,12 +15,21 @@
|
|||||||
"warning": "Figyelmeztetés",
|
"warning": "Figyelmeztetés",
|
||||||
"inviteInfiniteUsesWarning": "a végtelen felhasználású meghívókkal visszaélhetnek",
|
"inviteInfiniteUsesWarning": "a végtelen felhasználású meghívókkal visszaélhetnek",
|
||||||
"inviteSendToEmail": "Címzett",
|
"inviteSendToEmail": "Címzett",
|
||||||
|
"login": "Belépés",
|
||||||
|
"logout": "Kijelentkezés",
|
||||||
"create": "Létrehozás",
|
"create": "Létrehozás",
|
||||||
"apply": "Alkalmaz",
|
"apply": "Alkalmaz",
|
||||||
|
"delete": "Törlés",
|
||||||
|
"add": "Hozzáadás",
|
||||||
"select": "Kiválasztás",
|
"select": "Kiválasztás",
|
||||||
"name": "Név",
|
"name": "Név",
|
||||||
"date": "Dátum",
|
"date": "Dátum",
|
||||||
|
"enabled": "Engedélyezve",
|
||||||
|
"disabled": "Tiltva",
|
||||||
|
"reEnable": "Újra engedélyezés",
|
||||||
"setExpiry": "Lejárat beállítása",
|
"setExpiry": "Lejárat beállítása",
|
||||||
|
"disable": "Letiltás",
|
||||||
|
"admin": "Adminisztrátor",
|
||||||
"updates": "Frissítések",
|
"updates": "Frissítések",
|
||||||
"update": "Frissítés",
|
"update": "Frissítés",
|
||||||
"download": "Letöltés",
|
"download": "Letöltés",
|
||||||
@ -29,6 +38,7 @@
|
|||||||
"lastActiveTime": "Utoljára aktív",
|
"lastActiveTime": "Utoljára aktív",
|
||||||
"from": "Feladó",
|
"from": "Feladó",
|
||||||
"user": "Felhasználó",
|
"user": "Felhasználó",
|
||||||
|
"expiry": "Lejárat",
|
||||||
"userExpiry": "Felhasználói lejárat",
|
"userExpiry": "Felhasználói lejárat",
|
||||||
"userExpiryDescription": "Egy meghatározott idő után minden regisztrációt töröl, vagy felfüggeszt a jfa-go. Ezt a működést megváltoztathatod a beállításokban.",
|
"userExpiryDescription": "Egy meghatározott idő után minden regisztrációt töröl, vagy felfüggeszt a jfa-go. Ezt a működést megváltoztathatod a beállításokban.",
|
||||||
"aboutProgram": "Névjegy",
|
"aboutProgram": "Névjegy",
|
||||||
@ -47,6 +57,7 @@
|
|||||||
"conditionals": "Feltételek",
|
"conditionals": "Feltételek",
|
||||||
"preview": "Előnézet",
|
"preview": "Előnézet",
|
||||||
"reset": "Visszaállítás",
|
"reset": "Visszaállítás",
|
||||||
|
"edit": "Szerkesztés",
|
||||||
"donate": "Támogatás",
|
"donate": "Támogatás",
|
||||||
"sendPWR": "Jelszó visszaállítás küldése",
|
"sendPWR": "Jelszó visszaállítás küldése",
|
||||||
"contactThrough": "",
|
"contactThrough": "",
|
||||||
@ -115,9 +126,13 @@
|
|||||||
"updateAppliedRefresh": "",
|
"updateAppliedRefresh": "",
|
||||||
"telegramVerified": "",
|
"telegramVerified": "",
|
||||||
"accountConnected": "",
|
"accountConnected": "",
|
||||||
|
"errorConnection": "",
|
||||||
|
"error401Unauthorized": "",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "",
|
"errorSettingsAppliedNoHomescreenLayout": "",
|
||||||
"errorHomescreenAppliedNoSettings": "",
|
"errorHomescreenAppliedNoSettings": "",
|
||||||
"errorSettingsFailed": "",
|
"errorSettingsFailed": "",
|
||||||
|
"errorLoginBlank": "",
|
||||||
|
"errorUnknown": "",
|
||||||
"errorSaveEmail": "",
|
"errorSaveEmail": "",
|
||||||
"errorBlankFields": "",
|
"errorBlankFields": "",
|
||||||
"errorDeleteProfile": "",
|
"errorDeleteProfile": "",
|
||||||
@ -125,6 +140,7 @@
|
|||||||
"errorCreateProfile": "",
|
"errorCreateProfile": "",
|
||||||
"errorSetDefaultProfile": "",
|
"errorSetDefaultProfile": "",
|
||||||
"errorLoadUsers": "",
|
"errorLoadUsers": "",
|
||||||
|
"errorSaveSettings": "",
|
||||||
"errorLoadSettings": "",
|
"errorLoadSettings": "",
|
||||||
"errorSetOmbiProfile": "",
|
"errorSetOmbiProfile": "",
|
||||||
"errorLoadOmbiUsers": "",
|
"errorLoadOmbiUsers": "",
|
||||||
|
@ -13,8 +13,11 @@
|
|||||||
"warning": "Peringatan",
|
"warning": "Peringatan",
|
||||||
"inviteInfiniteUsesWarning": "Undangan dalam jumlah tak terbatas dapat disalahgunakan",
|
"inviteInfiniteUsesWarning": "Undangan dalam jumlah tak terbatas dapat disalahgunakan",
|
||||||
"inviteSendToEmail": "Dikirim kepada",
|
"inviteSendToEmail": "Dikirim kepada",
|
||||||
|
"login": "Masuk",
|
||||||
|
"logout": "Keluar",
|
||||||
"create": "Buat",
|
"create": "Buat",
|
||||||
"apply": "Terapkan",
|
"apply": "Terapkan",
|
||||||
|
"delete": "Hapus",
|
||||||
"name": "Nama",
|
"name": "Nama",
|
||||||
"date": "Tanggal",
|
"date": "Tanggal",
|
||||||
"lastActiveTime": "Terakhir Aktif",
|
"lastActiveTime": "Terakhir Aktif",
|
||||||
@ -65,6 +68,7 @@
|
|||||||
"variables": "Variabel",
|
"variables": "Variabel",
|
||||||
"preview": "Pratinjau",
|
"preview": "Pratinjau",
|
||||||
"reset": "Setel ulang",
|
"reset": "Setel ulang",
|
||||||
|
"edit": "Edit",
|
||||||
"customizeMessages": "Sesuaikan Email",
|
"customizeMessages": "Sesuaikan Email",
|
||||||
"customizeMessagesDescription": "Jika Anda tidak ingin menggunakan templat email jfa-go, Anda dapat membuatnya sendiri menggunakan Markdown.",
|
"customizeMessagesDescription": "Jika Anda tidak ingin menggunakan templat email jfa-go, Anda dapat membuatnya sendiri menggunakan Markdown.",
|
||||||
"announce": "Mengumumkan",
|
"announce": "Mengumumkan",
|
||||||
@ -78,15 +82,20 @@
|
|||||||
"createProfile": "Membuat profil {n}.",
|
"createProfile": "Membuat profil {n}.",
|
||||||
"saveSettings": "Pengaturan telah disimpan",
|
"saveSettings": "Pengaturan telah disimpan",
|
||||||
"setOmbiDefaults": "Default ombi tersimpan.",
|
"setOmbiDefaults": "Default ombi tersimpan.",
|
||||||
|
"errorConnection": "Tidak dapat terhubung ke jfa-go.",
|
||||||
|
"error401Unauthorized": "Tidak ter-otorisasi. Coba segarkan halaman.",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "Pengaturan telah diterapkan, tetapi menerapkan tata letak layar utama mungkin gagal.",
|
"errorSettingsAppliedNoHomescreenLayout": "Pengaturan telah diterapkan, tetapi menerapkan tata letak layar utama mungkin gagal.",
|
||||||
"errorHomescreenAppliedNoSettings": "Tata letak layar beranda diterapkan, tetapi menerapkan pengaturan mungkin gagal.",
|
"errorHomescreenAppliedNoSettings": "Tata letak layar beranda diterapkan, tetapi menerapkan pengaturan mungkin gagal.",
|
||||||
"errorSettingsFailed": "Aplikasi gagal.",
|
"errorSettingsFailed": "Aplikasi gagal.",
|
||||||
|
"errorLoginBlank": "Nama pengguna dan / atau sandi kosong.",
|
||||||
|
"errorUnknown": "Kesalahan yang tidak diketahui.",
|
||||||
"errorBlankFields": "Isian dibiarkan kosong",
|
"errorBlankFields": "Isian dibiarkan kosong",
|
||||||
"errorDeleteProfile": "Gagal menghapus profil {n}",
|
"errorDeleteProfile": "Gagal menghapus profil {n}",
|
||||||
"errorLoadProfiles": "Gagal memuat profil.",
|
"errorLoadProfiles": "Gagal memuat profil.",
|
||||||
"errorCreateProfile": "Gagal membuat profil {n}",
|
"errorCreateProfile": "Gagal membuat profil {n}",
|
||||||
"errorSetDefaultProfile": "Gagal menyetel profil default.",
|
"errorSetDefaultProfile": "Gagal menyetel profil default.",
|
||||||
"errorLoadUsers": "Gagal memuat pengguna.",
|
"errorLoadUsers": "Gagal memuat pengguna.",
|
||||||
|
"errorSaveSettings": "Tidak dapat menyimpan pengaturan.",
|
||||||
"errorLoadSettings": "Gagal memuat pengaturan.",
|
"errorLoadSettings": "Gagal memuat pengaturan.",
|
||||||
"errorSetOmbiDefaults": "Gagal menyimpan default ombi.",
|
"errorSetOmbiDefaults": "Gagal menyimpan default ombi.",
|
||||||
"errorLoadOmbiUsers": "Gagal memuat pengguna ombi.",
|
"errorLoadOmbiUsers": "Gagal memuat pengguna ombi.",
|
||||||
|
@ -13,8 +13,11 @@
|
|||||||
"warning": "Waarschuwing",
|
"warning": "Waarschuwing",
|
||||||
"inviteInfiniteUsesWarning": "ongelimiteerde uitnodigingen kunnen misbruikt worden",
|
"inviteInfiniteUsesWarning": "ongelimiteerde uitnodigingen kunnen misbruikt worden",
|
||||||
"inviteSendToEmail": "Stuur naar",
|
"inviteSendToEmail": "Stuur naar",
|
||||||
|
"login": "Inloggen",
|
||||||
|
"logout": "Uitloggen",
|
||||||
"create": "Aanmaken",
|
"create": "Aanmaken",
|
||||||
"apply": "Toepassen",
|
"apply": "Toepassen",
|
||||||
|
"delete": "Verwijderen",
|
||||||
"name": "Naam",
|
"name": "Naam",
|
||||||
"date": "Datum",
|
"date": "Datum",
|
||||||
"lastActiveTime": "Laatst actief",
|
"lastActiveTime": "Laatst actief",
|
||||||
@ -70,9 +73,14 @@
|
|||||||
"customizeMessagesDescription": "Als je de e-mailsjablonen van jfa-go niet wilt gebruiken, kun je met gebruik van Markdown je eigen aanmaken.",
|
"customizeMessagesDescription": "Als je de e-mailsjablonen van jfa-go niet wilt gebruiken, kun je met gebruik van Markdown je eigen aanmaken.",
|
||||||
"preview": "Voorbeeld",
|
"preview": "Voorbeeld",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
|
"edit": "Bewerken",
|
||||||
"customizeMessages": "E-mails aanpassen",
|
"customizeMessages": "E-mails aanpassen",
|
||||||
"inviteDuration": "Geldigheidsduur uitnodiging",
|
"inviteDuration": "Geldigheidsduur uitnodiging",
|
||||||
"userExpiryDescription": "Een bepaalde tijd na elke aanmelding, wordt de account verwijderd/uitgeschakeld door jfa-go. Dit kan aangepast worden in de instellingen.",
|
"userExpiryDescription": "Een bepaalde tijd na elke aanmelding, wordt de account verwijderd/uitgeschakeld door jfa-go. Dit kan aangepast worden in de instellingen.",
|
||||||
|
"enabled": "Ingeschakeld",
|
||||||
|
"disabled": "Uitgeschakeld",
|
||||||
|
"admin": "Beheerder",
|
||||||
|
"expiry": "Verloop",
|
||||||
"userExpiry": "Gebruikersverloop",
|
"userExpiry": "Gebruikersverloop",
|
||||||
"extendExpiry": "Verleng verloop",
|
"extendExpiry": "Verleng verloop",
|
||||||
"updates": "Updates",
|
"updates": "Updates",
|
||||||
@ -81,10 +89,13 @@
|
|||||||
"search": "Zoeken",
|
"search": "Zoeken",
|
||||||
"advancedSettings": "Geavanceerde instellingen",
|
"advancedSettings": "Geavanceerde instellingen",
|
||||||
"inviteMonths": "Maanden",
|
"inviteMonths": "Maanden",
|
||||||
|
"reEnable": "Opnieuw inschakelen",
|
||||||
|
"disable": "Uitschakelen",
|
||||||
"conditionals": "Voorwaarden",
|
"conditionals": "Voorwaarden",
|
||||||
"donate": "Doneer",
|
"donate": "Doneer",
|
||||||
"contactThrough": "Stuur bericht via:",
|
"contactThrough": "Stuur bericht via:",
|
||||||
"sendPIN": "Vraag de gebruiker om onderstaande pincode naar de bot te sturen.",
|
"sendPIN": "Vraag de gebruiker om onderstaande pincode naar de bot te sturen.",
|
||||||
|
"add": "Voeg toe",
|
||||||
"searchDiscordUser": "Begin de Discord gebruikersnaam te typen om de gebruiker te vinden.",
|
"searchDiscordUser": "Begin de Discord gebruikersnaam te typen om de gebruiker te vinden.",
|
||||||
"linkMatrixDescription": "Vul de gebruikersnaam en wachtwoord in van de gebruiker om als bot te gebruiken. De app start zodra ze zijn verstuurd.",
|
"linkMatrixDescription": "Vul de gebruikersnaam en wachtwoord in van de gebruiker om als bot te gebruiken. De app start zodra ze zijn verstuurd.",
|
||||||
"select": "Selecteer",
|
"select": "Selecteer",
|
||||||
@ -111,15 +122,20 @@
|
|||||||
"createProfile": "Profiel {n} aangemaakt.",
|
"createProfile": "Profiel {n} aangemaakt.",
|
||||||
"saveSettings": "De instellingen zijn opgeslagen",
|
"saveSettings": "De instellingen zijn opgeslagen",
|
||||||
"setOmbiDefaults": "De ombi standaardinstellingen zijn opgeslagen.",
|
"setOmbiDefaults": "De ombi standaardinstellingen zijn opgeslagen.",
|
||||||
|
"errorConnection": "Kon geen verbinding maken met jfa-go.",
|
||||||
|
"error401Unauthorized": "Geen toegang. Probeer de pagina te vernieuwen.",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "De instellingen zijn toegepast, maar wijzigen van de startpaginaindeling is misschien mislukt.",
|
"errorSettingsAppliedNoHomescreenLayout": "De instellingen zijn toegepast, maar wijzigen van de startpaginaindeling is misschien mislukt.",
|
||||||
"errorHomescreenAppliedNoSettings": "Startpaginaindeling toegepast, maar opslaan van instellingen is misschien mislukt.",
|
"errorHomescreenAppliedNoSettings": "Startpaginaindeling toegepast, maar opslaan van instellingen is misschien mislukt.",
|
||||||
"errorSettingsFailed": "Opslaan mislukt.",
|
"errorSettingsFailed": "Opslaan mislukt.",
|
||||||
|
"errorLoginBlank": "De gebruikersnaam en/of wachtwoord is leeg.",
|
||||||
|
"errorUnknown": "Onbekende fout.",
|
||||||
"errorBlankFields": "Velden leeggelaten",
|
"errorBlankFields": "Velden leeggelaten",
|
||||||
"errorDeleteProfile": "Verwijderen van profiel {n} mislukt",
|
"errorDeleteProfile": "Verwijderen van profiel {n} mislukt",
|
||||||
"errorLoadProfiles": "Fout bij het laden van profielen.",
|
"errorLoadProfiles": "Fout bij het laden van profielen.",
|
||||||
"errorCreateProfile": "Aanmaken van profile {n} mislukt",
|
"errorCreateProfile": "Aanmaken van profile {n} mislukt",
|
||||||
"errorSetDefaultProfile": "Fout bij instellen van standaardprofiel.",
|
"errorSetDefaultProfile": "Fout bij instellen van standaardprofiel.",
|
||||||
"errorLoadUsers": "Laden van gebruikers mislukt.",
|
"errorLoadUsers": "Laden van gebruikers mislukt.",
|
||||||
|
"errorSaveSettings": "Opslaan van instellingen mislukt.",
|
||||||
"errorLoadSettings": "Laden van instellingen mislukt.",
|
"errorLoadSettings": "Laden van instellingen mislukt.",
|
||||||
"errorSetOmbiDefaults": "Opslaan van ombi standaardinstellingen mislukt.",
|
"errorSetOmbiDefaults": "Opslaan van ombi standaardinstellingen mislukt.",
|
||||||
"errorLoadOmbiUsers": "Laden van ombi gebruikers mislukt.",
|
"errorLoadOmbiUsers": "Laden van ombi gebruikers mislukt.",
|
||||||
|
@ -15,12 +15,21 @@
|
|||||||
"warning": "Ostrzeżenie",
|
"warning": "Ostrzeżenie",
|
||||||
"inviteInfiniteUsesWarning": "",
|
"inviteInfiniteUsesWarning": "",
|
||||||
"inviteSendToEmail": "",
|
"inviteSendToEmail": "",
|
||||||
|
"login": "",
|
||||||
|
"logout": "",
|
||||||
"create": "",
|
"create": "",
|
||||||
"apply": "",
|
"apply": "",
|
||||||
|
"delete": "",
|
||||||
|
"add": "",
|
||||||
"select": "",
|
"select": "",
|
||||||
"name": "Imię",
|
"name": "Imię",
|
||||||
"date": "Data",
|
"date": "Data",
|
||||||
|
"enabled": "Włączone",
|
||||||
|
"disabled": "Wyłączone",
|
||||||
|
"reEnable": "",
|
||||||
"setExpiry": "",
|
"setExpiry": "",
|
||||||
|
"disable": "Wyłączone",
|
||||||
|
"admin": "Admin",
|
||||||
"updates": "Aktualizacje",
|
"updates": "Aktualizacje",
|
||||||
"update": "Aktualizacja",
|
"update": "Aktualizacja",
|
||||||
"download": "Pobierz",
|
"download": "Pobierz",
|
||||||
@ -29,6 +38,7 @@
|
|||||||
"lastActiveTime": "Ostatnia aktywność",
|
"lastActiveTime": "Ostatnia aktywność",
|
||||||
"from": "Od",
|
"from": "Od",
|
||||||
"user": "Użytkownik",
|
"user": "Użytkownik",
|
||||||
|
"expiry": "Wygasa",
|
||||||
"userExpiry": "Użytkownik wygasa",
|
"userExpiry": "Użytkownik wygasa",
|
||||||
"userExpiryDescription": "",
|
"userExpiryDescription": "",
|
||||||
"aboutProgram": "O",
|
"aboutProgram": "O",
|
||||||
@ -47,6 +57,7 @@
|
|||||||
"conditionals": "",
|
"conditionals": "",
|
||||||
"preview": "",
|
"preview": "",
|
||||||
"reset": "Zresetuj",
|
"reset": "Zresetuj",
|
||||||
|
"edit": "Edytuj",
|
||||||
"donate": "",
|
"donate": "",
|
||||||
"sendPWR": "",
|
"sendPWR": "",
|
||||||
"contactThrough": "",
|
"contactThrough": "",
|
||||||
@ -117,9 +128,13 @@
|
|||||||
"updateAppliedRefresh": "Aktualizacja zastosowana, odśwież.",
|
"updateAppliedRefresh": "Aktualizacja zastosowana, odśwież.",
|
||||||
"telegramVerified": "Konto telegramu zweryfikowane.",
|
"telegramVerified": "Konto telegramu zweryfikowane.",
|
||||||
"accountConnected": "Konto połączone.",
|
"accountConnected": "Konto połączone.",
|
||||||
|
"errorConnection": "Nie udało się połączyć z jfa-go.",
|
||||||
|
"error401Unauthorized": "Nieautoryzowany. Spróbuj odświeżyć stronę.",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "Zastosowano ustawienia, ale zastosowanie układu ekranu głównego mogło się nie powieść.",
|
"errorSettingsAppliedNoHomescreenLayout": "Zastosowano ustawienia, ale zastosowanie układu ekranu głównego mogło się nie powieść.",
|
||||||
"errorHomescreenAppliedNoSettings": "",
|
"errorHomescreenAppliedNoSettings": "",
|
||||||
"errorSettingsFailed": "",
|
"errorSettingsFailed": "",
|
||||||
|
"errorLoginBlank": "",
|
||||||
|
"errorUnknown": "Nieznany błąd.",
|
||||||
"errorSaveEmail": "",
|
"errorSaveEmail": "",
|
||||||
"errorBlankFields": "",
|
"errorBlankFields": "",
|
||||||
"errorDeleteProfile": "",
|
"errorDeleteProfile": "",
|
||||||
@ -127,6 +142,7 @@
|
|||||||
"errorCreateProfile": "",
|
"errorCreateProfile": "",
|
||||||
"errorSetDefaultProfile": "",
|
"errorSetDefaultProfile": "",
|
||||||
"errorLoadUsers": "",
|
"errorLoadUsers": "",
|
||||||
|
"errorSaveSettings": "",
|
||||||
"errorLoadSettings": "",
|
"errorLoadSettings": "",
|
||||||
"errorSetOmbiProfile": "",
|
"errorSetOmbiProfile": "",
|
||||||
"errorLoadOmbiUsers": "",
|
"errorLoadOmbiUsers": "",
|
@ -13,8 +13,11 @@
|
|||||||
"warning": "Aviso",
|
"warning": "Aviso",
|
||||||
"inviteInfiniteUsesWarning": "convites infinitos podem ser usados de forma abusiva",
|
"inviteInfiniteUsesWarning": "convites infinitos podem ser usados de forma abusiva",
|
||||||
"inviteSendToEmail": "Enviar para",
|
"inviteSendToEmail": "Enviar para",
|
||||||
|
"login": "Login",
|
||||||
|
"logout": "Sair",
|
||||||
"create": "Criar",
|
"create": "Criar",
|
||||||
"apply": "Aplicar",
|
"apply": "Aplicar",
|
||||||
|
"delete": "Deletar",
|
||||||
"name": "Nome",
|
"name": "Nome",
|
||||||
"date": "Data",
|
"date": "Data",
|
||||||
"lastActiveTime": "Ativo pela última vez",
|
"lastActiveTime": "Ativo pela última vez",
|
||||||
@ -70,9 +73,14 @@
|
|||||||
"variables": "Variáveis",
|
"variables": "Variáveis",
|
||||||
"preview": "Pre-visualizar",
|
"preview": "Pre-visualizar",
|
||||||
"reset": "Redefinir",
|
"reset": "Redefinir",
|
||||||
|
"edit": "Editar",
|
||||||
"customizeMessages": "Customizar Emails",
|
"customizeMessages": "Customizar Emails",
|
||||||
|
"disabled": "Desativado",
|
||||||
"userExpiryDescription": "Após um determinado período de tempo de cada inscrição, o jfa-go apagará/desabilitará a conta. Você pode alterar essa opção nas configurações.",
|
"userExpiryDescription": "Após um determinado período de tempo de cada inscrição, o jfa-go apagará/desabilitará a conta. Você pode alterar essa opção nas configurações.",
|
||||||
"inviteDuration": "Duração do Convite",
|
"inviteDuration": "Duração do Convite",
|
||||||
|
"enabled": "Habilitado",
|
||||||
|
"admin": "Admin",
|
||||||
|
"expiry": "Expira",
|
||||||
"userExpiry": "Vencimento do Usuário",
|
"userExpiry": "Vencimento do Usuário",
|
||||||
"extendExpiry": "Extender o vencimento",
|
"extendExpiry": "Extender o vencimento",
|
||||||
"updates": "Atualizações",
|
"updates": "Atualizações",
|
||||||
@ -81,12 +89,15 @@
|
|||||||
"search": "Procurar",
|
"search": "Procurar",
|
||||||
"advancedSettings": "Configurações Avançada",
|
"advancedSettings": "Configurações Avançada",
|
||||||
"inviteMonths": "Meses",
|
"inviteMonths": "Meses",
|
||||||
|
"reEnable": "Reativar",
|
||||||
|
"disable": "Desativar",
|
||||||
"conditionals": "Condicionais",
|
"conditionals": "Condicionais",
|
||||||
"donate": "Doar",
|
"donate": "Doar",
|
||||||
"contactThrough": "Contato através:",
|
"contactThrough": "Contato através:",
|
||||||
"sendPIN": "Peça que o usuário envie o PIN abaixo para o bot.",
|
"sendPIN": "Peça que o usuário envie o PIN abaixo para o bot.",
|
||||||
"searchDiscordUser": "Digite o nome de usuário do Discord.",
|
"searchDiscordUser": "Digite o nome de usuário do Discord.",
|
||||||
"findDiscordUser": "Encontrar usuário Discord",
|
"findDiscordUser": "Encontrar usuário Discord",
|
||||||
|
"add": "Adicionar",
|
||||||
"linkMatrixDescription": "Digite o nome de usuário e a senha para usar como bot. Depois de enviado, o aplicativo será reiniciado.",
|
"linkMatrixDescription": "Digite o nome de usuário e a senha para usar como bot. Depois de enviado, o aplicativo será reiniciado.",
|
||||||
"select": "Selecionar",
|
"select": "Selecionar",
|
||||||
"templates": "Modelos",
|
"templates": "Modelos",
|
||||||
@ -111,15 +122,20 @@
|
|||||||
"createProfile": "Perfil {n} criado.",
|
"createProfile": "Perfil {n} criado.",
|
||||||
"saveSettings": "As configurações foram salvas",
|
"saveSettings": "As configurações foram salvas",
|
||||||
"setOmbiDefaults": "Padrões do ombi armazenados.",
|
"setOmbiDefaults": "Padrões do ombi armazenados.",
|
||||||
|
"errorConnection": "Não foi possível conectar ao jfa-go.",
|
||||||
|
"error401Unauthorized": "Não autorizado. Tente atualizar a página.",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "As configurações foram aplicadas, mas a aplicação do layout da tela inicial pode ter falhado.",
|
"errorSettingsAppliedNoHomescreenLayout": "As configurações foram aplicadas, mas a aplicação do layout da tela inicial pode ter falhado.",
|
||||||
"errorHomescreenAppliedNoSettings": "O layout da tela inicial foi aplicado, mas a aplicação das configurações pode ter falhado.",
|
"errorHomescreenAppliedNoSettings": "O layout da tela inicial foi aplicado, mas a aplicação das configurações pode ter falhado.",
|
||||||
"errorSettingsFailed": "Falha na aplicação.",
|
"errorSettingsFailed": "Falha na aplicação.",
|
||||||
|
"errorLoginBlank": "O nome de usuário e/ou senha foram deixados em branco.",
|
||||||
|
"errorUnknown": "Erro desconhecido.",
|
||||||
"errorBlankFields": "Os campos foram deixados em branco",
|
"errorBlankFields": "Os campos foram deixados em branco",
|
||||||
"errorDeleteProfile": "Falha ao excluir perfil {n}",
|
"errorDeleteProfile": "Falha ao excluir perfil {n}",
|
||||||
"errorLoadProfiles": "Falha ao carregar perfis.",
|
"errorLoadProfiles": "Falha ao carregar perfis.",
|
||||||
"errorCreateProfile": "Falha ao criar perfil {n}",
|
"errorCreateProfile": "Falha ao criar perfil {n}",
|
||||||
"errorSetDefaultProfile": "Falha ao definir o perfil padrão.",
|
"errorSetDefaultProfile": "Falha ao definir o perfil padrão.",
|
||||||
"errorLoadUsers": "Falha ao carregar usuários.",
|
"errorLoadUsers": "Falha ao carregar usuários.",
|
||||||
|
"errorSaveSettings": "Não foi possível salvar as configurações.",
|
||||||
"errorLoadSettings": "Falha ao carregar as configurações.",
|
"errorLoadSettings": "Falha ao carregar as configurações.",
|
||||||
"errorSetOmbiDefaults": "Falha em armazenar os padrões ombi.",
|
"errorSetOmbiDefaults": "Falha em armazenar os padrões ombi.",
|
||||||
"errorLoadOmbiUsers": "Falha ao carregar usuários ombi.",
|
"errorLoadOmbiUsers": "Falha ao carregar usuários ombi.",
|
||||||
|
@ -13,8 +13,11 @@
|
|||||||
"warning": "Varning",
|
"warning": "Varning",
|
||||||
"inviteInfiniteUsesWarning": "inbjudningar med oändligt antal användningar kan missbrukas",
|
"inviteInfiniteUsesWarning": "inbjudningar med oändligt antal användningar kan missbrukas",
|
||||||
"inviteSendToEmail": "Skicka till",
|
"inviteSendToEmail": "Skicka till",
|
||||||
|
"login": "Logga in",
|
||||||
|
"logout": "Logga ut",
|
||||||
"create": "Skapa",
|
"create": "Skapa",
|
||||||
"apply": "Tillämpa",
|
"apply": "Tillämpa",
|
||||||
|
"delete": "Radera",
|
||||||
"name": "Namn",
|
"name": "Namn",
|
||||||
"date": "Datum",
|
"date": "Datum",
|
||||||
"lastActiveTime": "Senast aktiv",
|
"lastActiveTime": "Senast aktiv",
|
||||||
@ -33,6 +36,7 @@
|
|||||||
"variables": "Variabler",
|
"variables": "Variabler",
|
||||||
"preview": "Förhandsvisning",
|
"preview": "Förhandsvisning",
|
||||||
"reset": "Återställ",
|
"reset": "Återställ",
|
||||||
|
"edit": "Redigera",
|
||||||
"customizeMessages": "Anpassa e-post",
|
"customizeMessages": "Anpassa e-post",
|
||||||
"customizeMessagesDescription": "Om du inte vill använda jfa-go's e-postmallar, så kan du skapa dina egna med Markdown.",
|
"customizeMessagesDescription": "Om du inte vill använda jfa-go's e-postmallar, så kan du skapa dina egna med Markdown.",
|
||||||
"markdownSupported": "Markdown stöds.",
|
"markdownSupported": "Markdown stöds.",
|
||||||
@ -71,7 +75,11 @@
|
|||||||
"notifyEvent": "Meddela den:",
|
"notifyEvent": "Meddela den:",
|
||||||
"notifyInviteExpiry": "Vid utgång",
|
"notifyInviteExpiry": "Vid utgång",
|
||||||
"notifyUserCreation": "Vid användarskapande",
|
"notifyUserCreation": "Vid användarskapande",
|
||||||
|
"disabled": "Inaktiverad",
|
||||||
|
"enabled": "Aktiverad",
|
||||||
"inviteDuration": "Varaktighet för inbjudan",
|
"inviteDuration": "Varaktighet för inbjudan",
|
||||||
|
"admin": "Admin",
|
||||||
|
"expiry": "Löper ut",
|
||||||
"userExpiry": "Användarutgång",
|
"userExpiry": "Användarutgång",
|
||||||
"userExpiryDescription": "Efter en angiven tid efter varje registrering så tar jfa-go bort/inaktiverar kontot. Du kan ändra detta beteende i inställningarna.",
|
"userExpiryDescription": "Efter en angiven tid efter varje registrering så tar jfa-go bort/inaktiverar kontot. Du kan ändra detta beteende i inställningarna.",
|
||||||
"extendExpiry": "Förläng utgång"
|
"extendExpiry": "Förläng utgång"
|
||||||
@ -84,9 +92,13 @@
|
|||||||
"saveEmail": "E-post sparad.",
|
"saveEmail": "E-post sparad.",
|
||||||
"sentAnnouncement": "Meddelande skickat.",
|
"sentAnnouncement": "Meddelande skickat.",
|
||||||
"setOmbiDefaults": "Lagrade ombi-standardvärden.",
|
"setOmbiDefaults": "Lagrade ombi-standardvärden.",
|
||||||
|
"errorConnection": "Det gick inte att ansluta till jfa-go.",
|
||||||
|
"error401Unauthorized": "Obehörig. Prova att uppdatera sidan.",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "Inställningarna tillämpades, men tillämpningen av hemskärmslayout kan ha misslyckats.",
|
"errorSettingsAppliedNoHomescreenLayout": "Inställningarna tillämpades, men tillämpningen av hemskärmslayout kan ha misslyckats.",
|
||||||
"errorHomescreenAppliedNoSettings": "Hemskärmslayout tillämpades, men tillämpningen av inställningar kan ha misslyckats.",
|
"errorHomescreenAppliedNoSettings": "Hemskärmslayout tillämpades, men tillämpningen av inställningar kan ha misslyckats.",
|
||||||
"errorSettingsFailed": "Tillämpning misslyckades.",
|
"errorSettingsFailed": "Tillämpning misslyckades.",
|
||||||
|
"errorLoginBlank": "Användarnamnet och/eller lösenordet lämnades tomt.",
|
||||||
|
"errorUnknown": "Okänt fel.",
|
||||||
"errorSaveEmail": "Det gick inte att spara e-postmeddelandet.",
|
"errorSaveEmail": "Det gick inte att spara e-postmeddelandet.",
|
||||||
"errorBlankFields": "Fält lämnades tomma",
|
"errorBlankFields": "Fält lämnades tomma",
|
||||||
"errorDeleteProfile": "Det gick inte att ta bort profilen {n}",
|
"errorDeleteProfile": "Det gick inte att ta bort profilen {n}",
|
||||||
@ -94,6 +106,7 @@
|
|||||||
"errorCreateProfile": "Det gick inte att skapa profilen {n}",
|
"errorCreateProfile": "Det gick inte att skapa profilen {n}",
|
||||||
"errorSetDefaultProfile": "Det gick inte att ange standardprofil.",
|
"errorSetDefaultProfile": "Det gick inte att ange standardprofil.",
|
||||||
"errorLoadUsers": "Det gick inte att läsa in användare.",
|
"errorLoadUsers": "Det gick inte att läsa in användare.",
|
||||||
|
"errorSaveSettings": "Det gick inte att spara inställningarna.",
|
||||||
"errorLoadSettings": "Det gick inte att läsa in inställningarna.",
|
"errorLoadSettings": "Det gick inte att läsa in inställningarna.",
|
||||||
"errorSetOmbiDefaults": "Det gick inte att lagra ombi-standardvärden.",
|
"errorSetOmbiDefaults": "Det gick inte att lagra ombi-standardvärden.",
|
||||||
"errorLoadOmbiUsers": "Det gick inte att ladda ombi-användare.",
|
"errorLoadOmbiUsers": "Det gick inte att ladda ombi-användare.",
|
||||||
|
@ -15,12 +15,21 @@
|
|||||||
"warning": "Cảnh báo",
|
"warning": "Cảnh báo",
|
||||||
"inviteInfiniteUsesWarning": "các lời mời không giới hạn số lần sử dụng có thể bị lạm dụng",
|
"inviteInfiniteUsesWarning": "các lời mời không giới hạn số lần sử dụng có thể bị lạm dụng",
|
||||||
"inviteSendToEmail": "Gửi tới",
|
"inviteSendToEmail": "Gửi tới",
|
||||||
|
"login": "Đăng nhập",
|
||||||
|
"logout": "Đăng xuất",
|
||||||
"create": "Tạo mới",
|
"create": "Tạo mới",
|
||||||
"apply": "Áp dụng",
|
"apply": "Áp dụng",
|
||||||
|
"delete": "Xóa",
|
||||||
|
"add": "Thêm",
|
||||||
"select": "Chọn",
|
"select": "Chọn",
|
||||||
"name": "Tên",
|
"name": "Tên",
|
||||||
"date": "Ngày",
|
"date": "Ngày",
|
||||||
|
"enabled": "Mở",
|
||||||
|
"disabled": "Tắt",
|
||||||
|
"reEnable": "Mở lại",
|
||||||
"setExpiry": "Đặt hết hạn",
|
"setExpiry": "Đặt hết hạn",
|
||||||
|
"disable": "Tắt",
|
||||||
|
"admin": "Admin",
|
||||||
"updates": "Cập nhật",
|
"updates": "Cập nhật",
|
||||||
"update": "Cập nhật",
|
"update": "Cập nhật",
|
||||||
"download": "Tải về",
|
"download": "Tải về",
|
||||||
@ -29,6 +38,7 @@
|
|||||||
"lastActiveTime": "Lần cuối Hoạt động",
|
"lastActiveTime": "Lần cuối Hoạt động",
|
||||||
"from": "Từ",
|
"from": "Từ",
|
||||||
"user": "Người dùng",
|
"user": "Người dùng",
|
||||||
|
"expiry": "Hết hạn",
|
||||||
"userExpiry": "Hết hạn Người dùng",
|
"userExpiry": "Hết hạn Người dùng",
|
||||||
"userExpiryDescription": "Sau một khoảng thời gian nhất định sau khi mỗi đăng ký, jfa-go sẽ xóa/vô hiệu hóa tài khoản. Bạn có thể chỉnh sửa chế độ này trong cài đặt.",
|
"userExpiryDescription": "Sau một khoảng thời gian nhất định sau khi mỗi đăng ký, jfa-go sẽ xóa/vô hiệu hóa tài khoản. Bạn có thể chỉnh sửa chế độ này trong cài đặt.",
|
||||||
"aboutProgram": "Thông tin",
|
"aboutProgram": "Thông tin",
|
||||||
@ -46,6 +56,7 @@
|
|||||||
"conditionals": "Điều kiện",
|
"conditionals": "Điều kiện",
|
||||||
"preview": "Xem trước",
|
"preview": "Xem trước",
|
||||||
"reset": "Đặt lại",
|
"reset": "Đặt lại",
|
||||||
|
"edit": "Chỉnh sửa",
|
||||||
"donate": "Đóng góp",
|
"donate": "Đóng góp",
|
||||||
"sendPWR": "Gửi Đặt lại Mật khẩu",
|
"sendPWR": "Gửi Đặt lại Mật khẩu",
|
||||||
"contactThrough": "Liên lạc qua:",
|
"contactThrough": "Liên lạc qua:",
|
||||||
@ -116,9 +127,13 @@
|
|||||||
"updateAppliedRefresh": "Cập nhật mới đã được áp dụng, vui lòng làm mới lại trang.",
|
"updateAppliedRefresh": "Cập nhật mới đã được áp dụng, vui lòng làm mới lại trang.",
|
||||||
"telegramVerified": "Tài khoản Telegram đã được xác thực.",
|
"telegramVerified": "Tài khoản Telegram đã được xác thực.",
|
||||||
"accountConnected": "Tài khoản đã được kết nối.",
|
"accountConnected": "Tài khoản đã được kết nối.",
|
||||||
|
"errorConnection": "Không thể kết nối với jfa-go.",
|
||||||
|
"error401Unauthorized": "Không được phép. Hãy thử làm mới trang.",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "Cài đặt đã được áp dụng, nhưng việc áp dụng bố cục màn hình chính có thể không thành công.",
|
"errorSettingsAppliedNoHomescreenLayout": "Cài đặt đã được áp dụng, nhưng việc áp dụng bố cục màn hình chính có thể không thành công.",
|
||||||
"errorHomescreenAppliedNoSettings": "",
|
"errorHomescreenAppliedNoSettings": "",
|
||||||
"errorSettingsFailed": "",
|
"errorSettingsFailed": "",
|
||||||
|
"errorLoginBlank": "",
|
||||||
|
"errorUnknown": "",
|
||||||
"errorSaveEmail": "",
|
"errorSaveEmail": "",
|
||||||
"errorBlankFields": "",
|
"errorBlankFields": "",
|
||||||
"errorDeleteProfile": "",
|
"errorDeleteProfile": "",
|
||||||
@ -126,6 +141,7 @@
|
|||||||
"errorCreateProfile": "",
|
"errorCreateProfile": "",
|
||||||
"errorSetDefaultProfile": "",
|
"errorSetDefaultProfile": "",
|
||||||
"errorLoadUsers": "",
|
"errorLoadUsers": "",
|
||||||
|
"errorSaveSettings": "",
|
||||||
"errorLoadSettings": "",
|
"errorLoadSettings": "",
|
||||||
"errorSetOmbiProfile": "",
|
"errorSetOmbiProfile": "",
|
||||||
"errorLoadOmbiUsers": "",
|
"errorLoadOmbiUsers": "",
|
||||||
|
@ -15,12 +15,21 @@
|
|||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"inviteInfiniteUsesWarning": "無限使用次數的邀請碼可能被濫用",
|
"inviteInfiniteUsesWarning": "無限使用次數的邀請碼可能被濫用",
|
||||||
"inviteSendToEmail": "發送到",
|
"inviteSendToEmail": "發送到",
|
||||||
|
"login": "登錄",
|
||||||
|
"logout": "登出",
|
||||||
"create": "創建",
|
"create": "創建",
|
||||||
"apply": "應用",
|
"apply": "應用",
|
||||||
|
"delete": "刪除",
|
||||||
|
"add": "添加",
|
||||||
"select": "選擇",
|
"select": "選擇",
|
||||||
"name": "帳戶名稱",
|
"name": "帳戶名稱",
|
||||||
"date": "日期",
|
"date": "日期",
|
||||||
|
"enabled": "已啟用",
|
||||||
|
"disabled": "已禁用",
|
||||||
|
"reEnable": "重新啟用",
|
||||||
"setExpiry": "設置到期時間",
|
"setExpiry": "設置到期時間",
|
||||||
|
"disable": "禁用",
|
||||||
|
"admin": "管理員",
|
||||||
"updates": "更新",
|
"updates": "更新",
|
||||||
"update": "更新",
|
"update": "更新",
|
||||||
"download": "下載",
|
"download": "下載",
|
||||||
@ -29,6 +38,7 @@
|
|||||||
"lastActiveTime": "上次啟用時間",
|
"lastActiveTime": "上次啟用時間",
|
||||||
"from": "從",
|
"from": "從",
|
||||||
"user": "帳戶",
|
"user": "帳戶",
|
||||||
|
"expiry": "到期",
|
||||||
"userExpiry": "帳戶到期",
|
"userExpiry": "帳戶到期",
|
||||||
"userExpiryDescription": "每次註冊后指定的時間,jfa-go 將刪除/禁用該帳戶。您可以在設定中更改此行為。",
|
"userExpiryDescription": "每次註冊后指定的時間,jfa-go 將刪除/禁用該帳戶。您可以在設定中更改此行為。",
|
||||||
"aboutProgram": "關於",
|
"aboutProgram": "關於",
|
||||||
@ -47,6 +57,7 @@
|
|||||||
"conditionals": "條件",
|
"conditionals": "條件",
|
||||||
"preview": "預覽",
|
"preview": "預覽",
|
||||||
"reset": "重設",
|
"reset": "重設",
|
||||||
|
"edit": "編輯",
|
||||||
"donate": "捐贈",
|
"donate": "捐贈",
|
||||||
"sendPWR": "發送密碼重置",
|
"sendPWR": "發送密碼重置",
|
||||||
"contactThrough": "聯繫方式:",
|
"contactThrough": "聯繫方式:",
|
||||||
@ -117,9 +128,13 @@
|
|||||||
"updateAppliedRefresh": "更新已應用,請重新整理。",
|
"updateAppliedRefresh": "更新已應用,請重新整理。",
|
||||||
"telegramVerified": "Telegram 帳戶已驗證。",
|
"telegramVerified": "Telegram 帳戶已驗證。",
|
||||||
"accountConnected": "帳戶已連接。",
|
"accountConnected": "帳戶已連接。",
|
||||||
|
"errorConnection": "無法連接到 jfa-go。",
|
||||||
|
"error401Unauthorized": "未經授權。嘗試重新整理頁面。",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "已應用設置,但應用主螢幕佈局可能失敗。",
|
"errorSettingsAppliedNoHomescreenLayout": "已應用設置,但應用主螢幕佈局可能失敗。",
|
||||||
"errorHomescreenAppliedNoSettings": "已應用主螢幕佈局,但應用設置可能失敗。",
|
"errorHomescreenAppliedNoSettings": "已應用主螢幕佈局,但應用設置可能失敗。",
|
||||||
"errorSettingsFailed": "應用失敗。",
|
"errorSettingsFailed": "應用失敗。",
|
||||||
|
"errorLoginBlank": "帳戶名稱和/或密碼留空。",
|
||||||
|
"errorUnknown": "未知的錯誤。",
|
||||||
"errorSaveEmail": "無法儲存電子郵件。",
|
"errorSaveEmail": "無法儲存電子郵件。",
|
||||||
"errorBlankFields": "欄位留空",
|
"errorBlankFields": "欄位留空",
|
||||||
"errorDeleteProfile": "無法刪除設置文件 {n}",
|
"errorDeleteProfile": "無法刪除設置文件 {n}",
|
||||||
@ -127,6 +142,7 @@
|
|||||||
"errorCreateProfile": "無法創建設置文件 {n}",
|
"errorCreateProfile": "無法創建設置文件 {n}",
|
||||||
"errorSetDefaultProfile": "無法設置預設設置文件。",
|
"errorSetDefaultProfile": "無法設置預設設置文件。",
|
||||||
"errorLoadUsers": "無法讀取帳戶。",
|
"errorLoadUsers": "無法讀取帳戶。",
|
||||||
|
"errorSaveSettings": "無法儲存設置。",
|
||||||
"errorLoadSettings": "無法讀取設置。",
|
"errorLoadSettings": "無法讀取設置。",
|
||||||
"errorSetOmbiProfile": "無法儲存 ombi 設置文件。",
|
"errorSetOmbiProfile": "無法儲存 ombi 設置文件。",
|
||||||
"errorLoadOmbiUsers": "無法讀取 ombi 帳戶。",
|
"errorLoadOmbiUsers": "無法讀取 ombi 帳戶。",
|
@ -15,11 +15,20 @@
|
|||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"inviteInfiniteUsesWarning": "无限使用次数的邀请码可能被滥用",
|
"inviteInfiniteUsesWarning": "无限使用次数的邀请码可能被滥用",
|
||||||
"inviteSendToEmail": "发送到",
|
"inviteSendToEmail": "发送到",
|
||||||
|
"login": "登录",
|
||||||
|
"logout": "登出",
|
||||||
"create": "创建",
|
"create": "创建",
|
||||||
"apply": "申请",
|
"apply": "申请",
|
||||||
|
"delete": "删除",
|
||||||
|
"add": "添加",
|
||||||
"select": "选择",
|
"select": "选择",
|
||||||
"name": "名称",
|
"name": "名称",
|
||||||
"date": "日期",
|
"date": "日期",
|
||||||
|
"enabled": "已启用",
|
||||||
|
"disabled": "已禁用",
|
||||||
|
"reEnable": "重新启用",
|
||||||
|
"disable": "禁用",
|
||||||
|
"admin": "管理员",
|
||||||
"updates": "更新",
|
"updates": "更新",
|
||||||
"update": "更新",
|
"update": "更新",
|
||||||
"download": "下载",
|
"download": "下载",
|
||||||
@ -28,6 +37,7 @@
|
|||||||
"lastActiveTime": "上次活动",
|
"lastActiveTime": "上次活动",
|
||||||
"from": "从",
|
"from": "从",
|
||||||
"user": "用户",
|
"user": "用户",
|
||||||
|
"expiry": "到期",
|
||||||
"userExpiry": "用户到期",
|
"userExpiry": "用户到期",
|
||||||
"userExpiryDescription": "每次注册后的指定时间,jfa-go 将删除/禁用该帐户。您可以在设置中更改此行为。",
|
"userExpiryDescription": "每次注册后的指定时间,jfa-go 将删除/禁用该帐户。您可以在设置中更改此行为。",
|
||||||
"aboutProgram": "关于",
|
"aboutProgram": "关于",
|
||||||
@ -45,6 +55,7 @@
|
|||||||
"conditionals": "条件性条款",
|
"conditionals": "条件性条款",
|
||||||
"preview": "预览",
|
"preview": "预览",
|
||||||
"reset": "重设",
|
"reset": "重设",
|
||||||
|
"edit": "编辑",
|
||||||
"donate": "捐助",
|
"donate": "捐助",
|
||||||
"contactThrough": "联系方式:",
|
"contactThrough": "联系方式:",
|
||||||
"extendExpiry": "延长有效期",
|
"extendExpiry": "延长有效期",
|
||||||
@ -118,9 +129,13 @@
|
|||||||
"updateAppliedRefresh": "已应用更新,请刷新。",
|
"updateAppliedRefresh": "已应用更新,请刷新。",
|
||||||
"telegramVerified": "Telegram账户已验证。",
|
"telegramVerified": "Telegram账户已验证。",
|
||||||
"accountConnected": "帐户已连接。",
|
"accountConnected": "帐户已连接。",
|
||||||
|
"errorConnection": "无法连接到 jfa-go。",
|
||||||
|
"error401Unauthorized": "无授权。尝试刷新页面。",
|
||||||
"errorSettingsAppliedNoHomescreenLayout": "已应用设置,但应用主屏幕布局可能失败。",
|
"errorSettingsAppliedNoHomescreenLayout": "已应用设置,但应用主屏幕布局可能失败。",
|
||||||
"errorHomescreenAppliedNoSettings": "已应用主屏幕布局,但应用设置可能失败。",
|
"errorHomescreenAppliedNoSettings": "已应用主屏幕布局,但应用设置可能失败。",
|
||||||
"errorSettingsFailed": "应用失败。",
|
"errorSettingsFailed": "应用失败。",
|
||||||
|
"errorLoginBlank": "用户名/密码留空。",
|
||||||
|
"errorUnknown": "未知错误。",
|
||||||
"errorSaveEmail": "电子邮箱保存失败。",
|
"errorSaveEmail": "电子邮箱保存失败。",
|
||||||
"errorBlankFields": "字段留空",
|
"errorBlankFields": "字段留空",
|
||||||
"errorDeleteProfile": "删除配置文件{n}失败",
|
"errorDeleteProfile": "删除配置文件{n}失败",
|
||||||
@ -128,6 +143,7 @@
|
|||||||
"errorCreateProfile": "创建配置文件{n}失败",
|
"errorCreateProfile": "创建配置文件{n}失败",
|
||||||
"errorSetDefaultProfile": "设置默认配置文件失败。",
|
"errorSetDefaultProfile": "设置默认配置文件失败。",
|
||||||
"errorLoadUsers": "加载用户列表失败。",
|
"errorLoadUsers": "加载用户列表失败。",
|
||||||
|
"errorSaveSettings": "无法保存设置。",
|
||||||
"errorLoadSettings": "加载配置列表失败。",
|
"errorLoadSettings": "加载配置列表失败。",
|
||||||
"errorSetOmbiDefaults": "存储Ombi默认值失败。",
|
"errorSetOmbiDefaults": "存储Ombi默认值失败。",
|
||||||
"errorLoadOmbiUsers": "加载ombi用户列表失败。",
|
"errorLoadOmbiUsers": "加载ombi用户列表失败。",
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"meta": {
|
|
||||||
"name": "العربية (AR)"
|
|
||||||
},
|
|
||||||
"strings": {},
|
|
||||||
"notifications": {},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
|
@ -24,25 +24,6 @@
|
|||||||
"contactDiscord": "Kontakt gennem Discord",
|
"contactDiscord": "Kontakt gennem Discord",
|
||||||
"theme": "Tema",
|
"theme": "Tema",
|
||||||
"refresh": "Opdater",
|
"refresh": "Opdater",
|
||||||
"required": "Påkrævet",
|
"required": "Påkrævet"
|
||||||
"login": "Log på",
|
}
|
||||||
"logout": "Log ud",
|
|
||||||
"admin": "Administrator",
|
|
||||||
"enabled": "Aktiveret",
|
|
||||||
"disabled": "Deaktiveret",
|
|
||||||
"reEnable": "Genaktiver",
|
|
||||||
"disable": "Deaktiver",
|
|
||||||
"expiry": "Udløb",
|
|
||||||
"add": "Tilføj",
|
|
||||||
"edit": "Rediger",
|
|
||||||
"delete": "Slet"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorLoginBlank": "Brugernavnet og/eller adgangskoden blev efterladt tomme.",
|
|
||||||
"errorConnection": "Kunne ikke oprette forbindelse til jfa-go.",
|
|
||||||
"errorUnknown": "Ukendt fejl.",
|
|
||||||
"error401Unauthorized": "Adgang nægtet. Prøv at genindlæse siden.",
|
|
||||||
"errorSaveSettings": "Kunne ikke gemme indstillingerne."
|
|
||||||
},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
}
|
@ -4,45 +4,26 @@
|
|||||||
},
|
},
|
||||||
"strings": {
|
"strings": {
|
||||||
"username": "Benutzername",
|
"username": "Benutzername",
|
||||||
|
"name": "Name",
|
||||||
"password": "Passwort",
|
"password": "Passwort",
|
||||||
"emailAddress": "E-Mail Adresse",
|
"emailAddress": "E-Mail Adresse",
|
||||||
"name": "Name",
|
|
||||||
"submit": "Absenden",
|
"submit": "Absenden",
|
||||||
"send": "Senden",
|
|
||||||
"success": "Erfolgreich",
|
"success": "Erfolgreich",
|
||||||
"continue": "Weiter",
|
"continue": "Weiter",
|
||||||
"error": "Fehler",
|
"error": "Fehler",
|
||||||
"copy": "Kopieren",
|
"copy": "Kopieren",
|
||||||
"copied": "Kopiert",
|
"theme": "Thema",
|
||||||
"time24h": "24h-Format",
|
"time24h": "24h-Format",
|
||||||
"time12h": "12h-Format",
|
"time12h": "12h-Format",
|
||||||
|
"copied": "Kopiert",
|
||||||
"linkTelegram": "Link Telegram",
|
"linkTelegram": "Link Telegram",
|
||||||
"contactEmail": "Kontakt über E-Mail",
|
"contactEmail": "Kontakt über E-Mail",
|
||||||
"contactTelegram": "Kontakt über Telegram",
|
"contactTelegram": "Kontakt über Telegram",
|
||||||
"linkDiscord": "Link Discord",
|
"linkDiscord": "Link Discord",
|
||||||
"linkMatrix": "Link Matrix",
|
"linkMatrix": "Link Matrix",
|
||||||
|
"send": "Senden",
|
||||||
"contactDiscord": "Kontakt über Discord",
|
"contactDiscord": "Kontakt über Discord",
|
||||||
"theme": "Thema",
|
|
||||||
"refresh": "Aktualisieren",
|
"refresh": "Aktualisieren",
|
||||||
"required": "Erforderlich",
|
"required": "Erforderlich"
|
||||||
"login": "Anmelden",
|
}
|
||||||
"logout": "Abmelden",
|
|
||||||
"admin": "Admin",
|
|
||||||
"enabled": "Aktiviert",
|
|
||||||
"disabled": "Deaktiviert",
|
|
||||||
"reEnable": "Wieder aktivieren",
|
|
||||||
"disable": "Deaktivieren",
|
|
||||||
"expiry": "Ablaufdatum",
|
|
||||||
"add": "Hinzufügen",
|
|
||||||
"edit": "Bearbeiten",
|
|
||||||
"delete": "Löschen"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorLoginBlank": "Der Benutzername und/oder das Passwort wurden nicht ausgefüllt.",
|
|
||||||
"errorConnection": "Konnte keine Verbindung zu jfa-go herstellen.",
|
|
||||||
"errorUnknown": "Unbekannter Fehler.",
|
|
||||||
"error401Unauthorized": "Unberechtigt. Versuch, die Seite zu aktualisieren.",
|
|
||||||
"errorSaveSettings": "Einstellungen konnten nicht gespeichert werden."
|
|
||||||
},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
}
|
@ -12,27 +12,9 @@
|
|||||||
"continue": "Συνέχεια",
|
"continue": "Συνέχεια",
|
||||||
"error": "Σφάλμα",
|
"error": "Σφάλμα",
|
||||||
"copy": "Αντιγραφή",
|
"copy": "Αντιγραφή",
|
||||||
"copied": "Αντιγράφηκε",
|
"theme": "Θέμα",
|
||||||
"time24h": "24 Ώρες",
|
"time24h": "24 Ώρες",
|
||||||
"time12h": "12 Ώρες",
|
"time12h": "12 Ώρες",
|
||||||
"theme": "Θέμα",
|
"copied": "Αντιγράφηκε"
|
||||||
"login": "Σύνδεση",
|
}
|
||||||
"logout": "Αποσύνδεση",
|
|
||||||
"admin": "Διαχειριστής",
|
|
||||||
"enabled": "Ενεργοποιημένο",
|
|
||||||
"disabled": "Απενεργοποιημένο",
|
|
||||||
"reEnable": "Επανα-ενεργοποίηση",
|
|
||||||
"disable": "Απενεργοποίηση",
|
|
||||||
"expiry": "Λήξη",
|
|
||||||
"edit": "Επεξεργασία",
|
|
||||||
"delete": "Διαγραφή"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorLoginBlank": "Το όνομα χρήστη και/ή ο κωδικός ήταν κενά.",
|
|
||||||
"errorConnection": "Δεν μπόρεσε να συνδεθεί με το jfa-go.",
|
|
||||||
"errorUnknown": "Άγνωστο σφάλμα.",
|
|
||||||
"error401Unauthorized": "Ανεξουσιοδότητος. Προσπαθήστε να κάνετε επαναφόρτωση την σελίδα.",
|
|
||||||
"errorSaveSettings": "Αποτυχία αποθήκευσης ρυθμίσεων."
|
|
||||||
},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
}
|
@ -3,46 +3,27 @@
|
|||||||
"name": "English (GB)"
|
"name": "English (GB)"
|
||||||
},
|
},
|
||||||
"strings": {
|
"strings": {
|
||||||
"username": "Username",
|
|
||||||
"password": "Password",
|
|
||||||
"emailAddress": "Email Address",
|
|
||||||
"name": "Name",
|
|
||||||
"submit": "Submit",
|
|
||||||
"send": "Send",
|
|
||||||
"success": "Success",
|
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"error": "Error",
|
|
||||||
"copy": "Copy",
|
|
||||||
"copied": "Copied",
|
|
||||||
"time24h": "24h Time",
|
"time24h": "24h Time",
|
||||||
"time12h": "12h Time",
|
|
||||||
"linkTelegram": "Link Telegram",
|
"linkTelegram": "Link Telegram",
|
||||||
"contactEmail": "Contact through Email",
|
"send": "Send",
|
||||||
"contactTelegram": "Contact through Telegram",
|
|
||||||
"linkDiscord": "Link Discord",
|
"linkDiscord": "Link Discord",
|
||||||
"linkMatrix": "Link Matrix",
|
"linkMatrix": "Link Matrix",
|
||||||
"contactDiscord": "Contact through Discord",
|
"contactDiscord": "Contact through Discord",
|
||||||
|
"username": "Username",
|
||||||
|
"password": "Password",
|
||||||
|
"emailAddress": "Email Address",
|
||||||
|
"copy": "Copy",
|
||||||
|
"copied": "Copied",
|
||||||
|
"submit": "Submit",
|
||||||
|
"success": "Success",
|
||||||
|
"error": "Error",
|
||||||
|
"time12h": "12h Time",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
|
"contactEmail": "Contact through Email",
|
||||||
|
"contactTelegram": "Contact through Telegram",
|
||||||
|
"name": "Name",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"required": "Required",
|
"required": "Required"
|
||||||
"login": "Login",
|
}
|
||||||
"logout": "Logout",
|
|
||||||
"admin": "Admin",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"disabled": "Disabled",
|
|
||||||
"reEnable": "Re-enable",
|
|
||||||
"disable": "Disable",
|
|
||||||
"expiry": "Expiry",
|
|
||||||
"add": "Add",
|
|
||||||
"edit": "Edit",
|
|
||||||
"delete": "Delete"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorLoginBlank": "The username and/or password was left blank.",
|
|
||||||
"errorConnection": "Couldn't connect to jfa-go.",
|
|
||||||
"errorUnknown": "Unknown error.",
|
|
||||||
"error401Unauthorized": "Unauthorised. Try refreshing the page.",
|
|
||||||
"errorSaveSettings": "Couldn't save settings."
|
|
||||||
},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
}
|
@ -24,42 +24,6 @@
|
|||||||
"contactDiscord": "Contact through Discord",
|
"contactDiscord": "Contact through Discord",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"required": "Required",
|
"required": "Required"
|
||||||
"login": "Login",
|
|
||||||
"logout": "Logout",
|
|
||||||
"admin": "Admin",
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"disabled": "Disabled",
|
|
||||||
"reEnable": "Re-enable",
|
|
||||||
"disable": "Disable",
|
|
||||||
"contactMethods": "Contact Methods",
|
|
||||||
"accountStatus": "Account Status",
|
|
||||||
"notSet": "Not set",
|
|
||||||
"expiry": "Expiry",
|
|
||||||
"add": "Add",
|
|
||||||
"edit": "Edit",
|
|
||||||
"delete": "Delete",
|
|
||||||
"myAccount": "My Account"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorLoginBlank": "The username and/or password were left blank.",
|
|
||||||
"errorConnection": "Couldn't connect to jfa-go.",
|
|
||||||
"errorUnknown": "Unknown error.",
|
|
||||||
"error401Unauthorized": "Unauthorized. Try refreshing the page.",
|
|
||||||
"errorSaveSettings": "Couldn't save settings."
|
|
||||||
},
|
|
||||||
"quantityStrings": {
|
|
||||||
"year": {
|
|
||||||
"singular": "{n} Year",
|
|
||||||
"plural": "{n} Years"
|
|
||||||
},
|
|
||||||
"month": {
|
|
||||||
"singular": "{n} Month",
|
|
||||||
"plural": "{n} Months"
|
|
||||||
},
|
|
||||||
"day": {
|
|
||||||
"singular": "{n} Day",
|
|
||||||
"plural": "{n} Days"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
"emailAddress": "Correo electrónico",
|
"emailAddress": "Correo electrónico",
|
||||||
"name": "Nombre",
|
"name": "Nombre",
|
||||||
"submit": "Enviar",
|
"submit": "Enviar",
|
||||||
"send": "Enviar",
|
|
||||||
"success": "Éxito",
|
"success": "Éxito",
|
||||||
"continue": "Continuar",
|
"continue": "Continuar",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@ -16,33 +15,15 @@
|
|||||||
"copied": "Copiado",
|
"copied": "Copiado",
|
||||||
"time24h": "Formato de 24 horas",
|
"time24h": "Formato de 24 horas",
|
||||||
"time12h": "Formato de 12 horas",
|
"time12h": "Formato de 12 horas",
|
||||||
"linkTelegram": "Enlace Telegram",
|
"theme": "Tema",
|
||||||
|
"send": "Enviar",
|
||||||
|
"contactDiscord": "Contactar por Discord",
|
||||||
"contactEmail": "Contactar por correo electrónico",
|
"contactEmail": "Contactar por correo electrónico",
|
||||||
"contactTelegram": "Contactar por Telegram",
|
"contactTelegram": "Contactar por Telegram",
|
||||||
"linkDiscord": "Enlace Discord",
|
|
||||||
"linkMatrix": "Enlace Matrix",
|
"linkMatrix": "Enlace Matrix",
|
||||||
"contactDiscord": "Contactar por Discord",
|
"linkDiscord": "Enlace Discord",
|
||||||
"theme": "Tema",
|
"linkTelegram": "Enlace Telegram",
|
||||||
"refresh": "Refrescar",
|
"refresh": "Refrescar",
|
||||||
"required": "Requerido",
|
"required": "Requerido"
|
||||||
"login": "Acceso",
|
}
|
||||||
"logout": "Cerrar sesión",
|
|
||||||
"admin": "Administrador",
|
|
||||||
"enabled": "Activado",
|
|
||||||
"disabled": "Desactivado",
|
|
||||||
"reEnable": "Reactivar",
|
|
||||||
"disable": "Desactivar",
|
|
||||||
"expiry": "Expiración",
|
|
||||||
"add": "Agregar",
|
|
||||||
"edit": "Editar",
|
|
||||||
"delete": "Eliminar"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorLoginBlank": "El nombre de usuario y/o la contraseña se dejaron en blanco.",
|
|
||||||
"errorConnection": "No se pudo conectar a jfa-go.",
|
|
||||||
"errorUnknown": "Error desconocido.",
|
|
||||||
"error401Unauthorized": "No autorizado. Intente actualizar la página.",
|
|
||||||
"errorSaveSettings": "No se pudo guardar la configuración."
|
|
||||||
},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
}
|
@ -23,7 +23,5 @@
|
|||||||
"linkMatrix": "پیوند ماتریکس",
|
"linkMatrix": "پیوند ماتریکس",
|
||||||
"contactDiscord": "از طریق دیسکورد تماس بگیرید",
|
"contactDiscord": "از طریق دیسکورد تماس بگیرید",
|
||||||
"theme": "موضوع"
|
"theme": "موضوع"
|
||||||
},
|
}
|
||||||
"notifications": {},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
}
|
@ -1,48 +1,30 @@
|
|||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "Français (FR)"
|
"name": "Français (FR)",
|
||||||
|
"author": "https://github.com/Killianbe"
|
||||||
},
|
},
|
||||||
"strings": {
|
"strings": {
|
||||||
"username": "Nom d'utilisateur",
|
"username": "Nom d'utilisateur",
|
||||||
|
"name": "Nom",
|
||||||
"password": "Mot de passe",
|
"password": "Mot de passe",
|
||||||
"emailAddress": "Adresse e-mail",
|
"emailAddress": "Adresse e-mail",
|
||||||
"name": "Nom",
|
|
||||||
"submit": "Soumettre",
|
"submit": "Soumettre",
|
||||||
"send": "Envoyer",
|
|
||||||
"success": "Succès",
|
"success": "Succès",
|
||||||
"continue": "Continuer",
|
"continue": "Continuer",
|
||||||
"error": "Erreur",
|
"error": "Erreur",
|
||||||
"copy": "Copier",
|
"copy": "Copier",
|
||||||
"copied": "Copié",
|
|
||||||
"time24h": "Temps 24h",
|
"time24h": "Temps 24h",
|
||||||
"time12h": "Temps 12h",
|
"time12h": "Temps 12h",
|
||||||
|
"theme": "Thème",
|
||||||
|
"copied": "Copié",
|
||||||
"linkTelegram": "Lien Telegram",
|
"linkTelegram": "Lien Telegram",
|
||||||
"contactEmail": "Contact par e-mail",
|
"contactEmail": "Contact par e-mail",
|
||||||
"contactTelegram": "Contact par Telegram",
|
"contactTelegram": "Contact par Telegram",
|
||||||
"linkDiscord": "Lier Discord",
|
"linkDiscord": "Lier Discord",
|
||||||
"linkMatrix": "Lier Matrix",
|
"linkMatrix": "Lier Matrix",
|
||||||
|
"send": "Envoyer",
|
||||||
"contactDiscord": "Contacter par Discord",
|
"contactDiscord": "Contacter par Discord",
|
||||||
"theme": "Thème",
|
|
||||||
"refresh": "Actualiser",
|
"refresh": "Actualiser",
|
||||||
"required": "Requis",
|
"required": "Requis"
|
||||||
"login": "S'identifier",
|
}
|
||||||
"logout": "Se déconnecter",
|
|
||||||
"admin": "Administrateur",
|
|
||||||
"enabled": "Activé",
|
|
||||||
"disabled": "Désactivé",
|
|
||||||
"reEnable": "Ré-activé",
|
|
||||||
"disable": "Désactivé",
|
|
||||||
"expiry": "Expiration",
|
|
||||||
"add": "Ajouter",
|
|
||||||
"edit": "Éditer",
|
|
||||||
"delete": "Effacer"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorLoginBlank": "Le nom d'utilisateur et/ou le mot de passe sont vides.",
|
|
||||||
"errorConnection": "Impossible de se connecter à jfa-go.",
|
|
||||||
"errorUnknown": "Erreur inconnue.",
|
|
||||||
"error401Unauthorized": "Non autorisé. Essayez d'actualiser la page.",
|
|
||||||
"errorSaveSettings": "Impossible d'enregistrer les paramètres."
|
|
||||||
},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
}
|
@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"meta": {
|
|
||||||
"name": "English (US)"
|
|
||||||
},
|
|
||||||
"strings": {
|
|
||||||
"login": "Belépés",
|
|
||||||
"logout": "Kijelentkezés",
|
|
||||||
"admin": "Adminisztrátor",
|
|
||||||
"enabled": "Engedélyezve",
|
|
||||||
"disabled": "Tiltva",
|
|
||||||
"reEnable": "Újra engedélyezés",
|
|
||||||
"disable": "Letiltás",
|
|
||||||
"expiry": "Lejárat",
|
|
||||||
"add": "Hozzáadás",
|
|
||||||
"edit": "Szerkesztés",
|
|
||||||
"delete": "Törlés"
|
|
||||||
},
|
|
||||||
"notifications": {},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
|
@ -8,7 +8,6 @@
|
|||||||
"emailAddress": "Alamat Email",
|
"emailAddress": "Alamat Email",
|
||||||
"name": "Nama",
|
"name": "Nama",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"send": "Kirim",
|
|
||||||
"success": "Sukses",
|
"success": "Sukses",
|
||||||
"continue": "Lanjut",
|
"continue": "Lanjut",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@ -16,17 +15,6 @@
|
|||||||
"time24h": "Waktu 24 jam",
|
"time24h": "Waktu 24 jam",
|
||||||
"time12h": "Waktu 12 jam",
|
"time12h": "Waktu 12 jam",
|
||||||
"theme": "Tema",
|
"theme": "Tema",
|
||||||
"login": "Masuk",
|
"send": "Kirim"
|
||||||
"logout": "Keluar",
|
}
|
||||||
"edit": "Edit",
|
|
||||||
"delete": "Hapus"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorLoginBlank": "Nama pengguna dan / atau sandi kosong.",
|
|
||||||
"errorConnection": "Tidak dapat terhubung ke jfa-go.",
|
|
||||||
"errorUnknown": "Kesalahan yang tidak diketahui.",
|
|
||||||
"error401Unauthorized": "Tidak ter-otorisasi. Coba segarkan halaman.",
|
|
||||||
"errorSaveSettings": "Tidak dapat menyimpan pengaturan."
|
|
||||||
},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
}
|
@ -25,7 +25,5 @@
|
|||||||
"theme": "Tema",
|
"theme": "Tema",
|
||||||
"refresh": "Aggiorna",
|
"refresh": "Aggiorna",
|
||||||
"required": "Richiesto"
|
"required": "Richiesto"
|
||||||
},
|
}
|
||||||
"notifications": {},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
}
|
@ -4,45 +4,26 @@
|
|||||||
},
|
},
|
||||||
"strings": {
|
"strings": {
|
||||||
"username": "Gebruikersnaam",
|
"username": "Gebruikersnaam",
|
||||||
|
"name": "Naam",
|
||||||
"password": "Wachtwoord",
|
"password": "Wachtwoord",
|
||||||
"emailAddress": "E-mailadres",
|
"emailAddress": "E-mailadres",
|
||||||
"name": "Naam",
|
|
||||||
"submit": "Verstuur",
|
"submit": "Verstuur",
|
||||||
"send": "Verstuur",
|
|
||||||
"success": "Succes",
|
"success": "Succes",
|
||||||
"continue": "Doorgaan",
|
"continue": "Doorgaan",
|
||||||
"error": "Fout",
|
"error": "Fout",
|
||||||
"copy": "Kopiëer",
|
"copy": "Kopiëer",
|
||||||
"copied": "Gekopieerd",
|
"theme": "Thema",
|
||||||
"time24h": "24u-formaat",
|
"time24h": "24u-formaat",
|
||||||
"time12h": "12u-formaat",
|
"time12h": "12u-formaat",
|
||||||
|
"copied": "Gekopieerd",
|
||||||
"linkTelegram": "Koppel Telegram",
|
"linkTelegram": "Koppel Telegram",
|
||||||
"contactEmail": "Stuur e-mailbericht",
|
"contactEmail": "Stuur e-mailbericht",
|
||||||
"contactTelegram": "Stuur Telegram-bericht",
|
"contactTelegram": "Stuur Telegram-bericht",
|
||||||
|
"send": "Verstuur",
|
||||||
"linkDiscord": "Koppel Discord",
|
"linkDiscord": "Koppel Discord",
|
||||||
"linkMatrix": "Koppel Matrix",
|
"linkMatrix": "Koppel Matrix",
|
||||||
"contactDiscord": "Stuur Discord bericht",
|
"contactDiscord": "Stuur Discord bericht",
|
||||||
"theme": "Thema",
|
|
||||||
"refresh": "Ververs",
|
"refresh": "Ververs",
|
||||||
"required": "Verplicht",
|
"required": "Verplicht"
|
||||||
"login": "Inloggen",
|
}
|
||||||
"logout": "Uitloggen",
|
|
||||||
"admin": "Beheerder",
|
|
||||||
"enabled": "Ingeschakeld",
|
|
||||||
"disabled": "Uitgeschakeld",
|
|
||||||
"reEnable": "Opnieuw inschakelen",
|
|
||||||
"disable": "Uitschakelen",
|
|
||||||
"expiry": "Verloop",
|
|
||||||
"add": "Voeg toe",
|
|
||||||
"edit": "Bewerken",
|
|
||||||
"delete": "Verwijderen"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorLoginBlank": "De gebruikersnaam en/of wachtwoord is leeg.",
|
|
||||||
"errorConnection": "Kon geen verbinding maken met jfa-go.",
|
|
||||||
"errorUnknown": "Onbekende fout.",
|
|
||||||
"error401Unauthorized": "Geen toegang. Probeer de pagina te vernieuwen.",
|
|
||||||
"errorSaveSettings": "Opslaan van instellingen mislukt."
|
|
||||||
},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
}
|
@ -24,18 +24,6 @@
|
|||||||
"contactDiscord": "Kontakt przez Discord",
|
"contactDiscord": "Kontakt przez Discord",
|
||||||
"theme": "Motyw",
|
"theme": "Motyw",
|
||||||
"refresh": "Odśwież",
|
"refresh": "Odśwież",
|
||||||
"required": "Wymagane",
|
"required": "Wymagane"
|
||||||
"admin": "Admin",
|
}
|
||||||
"enabled": "Włączone",
|
|
||||||
"disabled": "Wyłączone",
|
|
||||||
"disable": "Wyłączone",
|
|
||||||
"expiry": "Wygasa",
|
|
||||||
"edit": "Edytuj"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorConnection": "Nie udało się połączyć z jfa-go.",
|
|
||||||
"errorUnknown": "Nieznany błąd.",
|
|
||||||
"error401Unauthorized": "Nieautoryzowany. Spróbuj odświeżyć stronę."
|
|
||||||
},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
}
|
@ -4,45 +4,26 @@
|
|||||||
},
|
},
|
||||||
"strings": {
|
"strings": {
|
||||||
"username": "Nome do Usuário",
|
"username": "Nome do Usuário",
|
||||||
|
"name": "Nome",
|
||||||
"password": "Senha",
|
"password": "Senha",
|
||||||
"emailAddress": "Endereço de e-mail",
|
"emailAddress": "Endereço de e-mail",
|
||||||
"name": "Nome",
|
|
||||||
"submit": "Enviar",
|
"submit": "Enviar",
|
||||||
"send": "Enviar",
|
|
||||||
"success": "Sucesso",
|
"success": "Sucesso",
|
||||||
"continue": "Continuar",
|
"continue": "Continuar",
|
||||||
"error": "Erro",
|
"error": "Erro",
|
||||||
"copy": "Copiar",
|
"copy": "Copiar",
|
||||||
"copied": "Copiado",
|
"theme": "Tema",
|
||||||
"time24h": "Horário 24h",
|
"time24h": "Horário 24h",
|
||||||
"time12h": "Horário 12h",
|
"time12h": "Horário 12h",
|
||||||
|
"copied": "Copiado",
|
||||||
"linkTelegram": "Link do Telegram",
|
"linkTelegram": "Link do Telegram",
|
||||||
"contactEmail": "Contato por Email",
|
"contactEmail": "Contato por Email",
|
||||||
"contactTelegram": "Contato pelo Telegram",
|
"contactTelegram": "Contato pelo Telegram",
|
||||||
|
"send": "Enviar",
|
||||||
"linkDiscord": "Link do Discord",
|
"linkDiscord": "Link do Discord",
|
||||||
"linkMatrix": "Link do Matrix",
|
"linkMatrix": "Link do Matrix",
|
||||||
"contactDiscord": "Contato através do Discord",
|
"contactDiscord": "Contato através do Discord",
|
||||||
"theme": "Tema",
|
|
||||||
"refresh": "Atualizar",
|
"refresh": "Atualizar",
|
||||||
"required": "Requeridos",
|
"required": "Requeridos"
|
||||||
"login": "Login",
|
}
|
||||||
"logout": "Sair",
|
|
||||||
"admin": "Admin",
|
|
||||||
"enabled": "Habilitado",
|
|
||||||
"disabled": "Desativado",
|
|
||||||
"reEnable": "Reativar",
|
|
||||||
"disable": "Desativar",
|
|
||||||
"expiry": "Expira",
|
|
||||||
"add": "Adicionar",
|
|
||||||
"edit": "Editar",
|
|
||||||
"delete": "Deletar"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorLoginBlank": "O nome de usuário e/ou senha foram deixados em branco.",
|
|
||||||
"errorConnection": "Não foi possível conectar ao jfa-go.",
|
|
||||||
"errorUnknown": "Erro desconhecido.",
|
|
||||||
"error401Unauthorized": "Não autorizado. Tente atualizar a página.",
|
|
||||||
"errorSaveSettings": "Não foi possível salvar as configurações."
|
|
||||||
},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
}
|
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"meta": {
|
|
||||||
"name": "Română (ROU)"
|
|
||||||
},
|
|
||||||
"strings": {},
|
|
||||||
"notifications": {},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
|
@ -25,7 +25,5 @@
|
|||||||
"theme": "Tema",
|
"theme": "Tema",
|
||||||
"refresh": "Osveži",
|
"refresh": "Osveži",
|
||||||
"required": "Obvezno"
|
"required": "Obvezno"
|
||||||
},
|
}
|
||||||
"notifications": {},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
}
|
@ -14,22 +14,6 @@
|
|||||||
"copy": "Kopiera",
|
"copy": "Kopiera",
|
||||||
"time24h": "24 timmarsklocka",
|
"time24h": "24 timmarsklocka",
|
||||||
"time12h": "12 timmarsklocka",
|
"time12h": "12 timmarsklocka",
|
||||||
"theme": "Tema",
|
"theme": "Tema"
|
||||||
"login": "Logga in",
|
}
|
||||||
"logout": "Logga ut",
|
|
||||||
"admin": "Admin",
|
|
||||||
"enabled": "Aktiverad",
|
|
||||||
"disabled": "Inaktiverad",
|
|
||||||
"expiry": "Löper ut",
|
|
||||||
"edit": "Redigera",
|
|
||||||
"delete": "Radera"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorLoginBlank": "Användarnamnet och/eller lösenordet lämnades tomt.",
|
|
||||||
"errorConnection": "Det gick inte att ansluta till jfa-go.",
|
|
||||||
"errorUnknown": "Okänt fel.",
|
|
||||||
"error401Unauthorized": "Obehörig. Prova att uppdatera sidan.",
|
|
||||||
"errorSaveSettings": "Det gick inte att spara inställningarna."
|
|
||||||
},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
}
|
@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"meta": {
|
|
||||||
"name": "Tiếng Anh (Mỹ)"
|
|
||||||
},
|
|
||||||
"strings": {
|
|
||||||
"login": "Đăng nhập",
|
|
||||||
"logout": "Đăng xuất",
|
|
||||||
"admin": "Admin",
|
|
||||||
"enabled": "Mở",
|
|
||||||
"disabled": "Tắt",
|
|
||||||
"reEnable": "Mở lại",
|
|
||||||
"disable": "Tắt",
|
|
||||||
"expiry": "Hết hạn",
|
|
||||||
"add": "Thêm",
|
|
||||||
"edit": "Chỉnh sửa",
|
|
||||||
"delete": "Xóa"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorConnection": "Không thể kết nối với jfa-go.",
|
|
||||||
"error401Unauthorized": "Không được phép. Hãy thử làm mới trang."
|
|
||||||
},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
|
@ -24,25 +24,6 @@
|
|||||||
"contactDiscord": "通過 Discord 聯繫",
|
"contactDiscord": "通過 Discord 聯繫",
|
||||||
"theme": "主題",
|
"theme": "主題",
|
||||||
"refresh": "重新整理",
|
"refresh": "重新整理",
|
||||||
"required": "必填",
|
"required": "必填"
|
||||||
"login": "登錄",
|
}
|
||||||
"logout": "登出",
|
|
||||||
"admin": "管理員",
|
|
||||||
"enabled": "已啟用",
|
|
||||||
"disabled": "已禁用",
|
|
||||||
"reEnable": "重新啟用",
|
|
||||||
"disable": "禁用",
|
|
||||||
"expiry": "到期",
|
|
||||||
"add": "添加",
|
|
||||||
"edit": "編輯",
|
|
||||||
"delete": "刪除"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorLoginBlank": "帳戶名稱和/或密碼留空。",
|
|
||||||
"errorConnection": "無法連接到 jfa-go。",
|
|
||||||
"errorUnknown": "未知的錯誤。",
|
|
||||||
"error401Unauthorized": "未經授權。嘗試重新整理頁面。",
|
|
||||||
"errorSaveSettings": "無法儲存設置。"
|
|
||||||
},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
}
|
@ -24,25 +24,6 @@
|
|||||||
"contactDiscord": "通过Discord联系",
|
"contactDiscord": "通过Discord联系",
|
||||||
"theme": "主题",
|
"theme": "主题",
|
||||||
"refresh": "刷新",
|
"refresh": "刷新",
|
||||||
"required": "必需的",
|
"required": "必需的"
|
||||||
"login": "登录",
|
}
|
||||||
"logout": "登出",
|
|
||||||
"admin": "管理员",
|
|
||||||
"enabled": "已启用",
|
|
||||||
"disabled": "已禁用",
|
|
||||||
"reEnable": "重新启用",
|
|
||||||
"disable": "禁用",
|
|
||||||
"expiry": "到期",
|
|
||||||
"add": "添加",
|
|
||||||
"edit": "编辑",
|
|
||||||
"delete": "删除"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorLoginBlank": "用户名/密码留空。",
|
|
||||||
"errorConnection": "无法连接到 jfa-go。",
|
|
||||||
"errorUnknown": "未知错误。",
|
|
||||||
"error401Unauthorized": "无授权。尝试刷新页面。",
|
|
||||||
"errorSaveSettings": "无法保存设置。"
|
|
||||||
},
|
|
||||||
"quantityStrings": {}
|
|
||||||
}
|
}
|
@ -8,8 +8,6 @@
|
|||||||
"accountDetails": "Details",
|
"accountDetails": "Details",
|
||||||
"emailAddress": "Email",
|
"emailAddress": "Email",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"oldPassword": "Old Password",
|
|
||||||
"newPassword": "New Password",
|
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"reEnterPassword": "Re-enter Password",
|
"reEnterPassword": "Re-enter Password",
|
||||||
"reEnterPasswordInvalid": "Passwords are not the same.",
|
"reEnterPasswordInvalid": "Passwords are not the same.",
|
||||||
@ -21,20 +19,7 @@
|
|||||||
"yourAccountIsValidUntil": "Your account will be valid until {date}.",
|
"yourAccountIsValidUntil": "Your account will be valid until {date}.",
|
||||||
"sendPIN": "Send the PIN below to the bot, then come back here to link your account.",
|
"sendPIN": "Send the PIN below to the bot, then come back here to link your account.",
|
||||||
"sendPINDiscord": "Type {command} in {server_channel} on Discord, then send the PIN below.",
|
"sendPINDiscord": "Type {command} in {server_channel} on Discord, then send the PIN below.",
|
||||||
"matrixEnterUser": "Enter your User ID, press submit, and a PIN will be sent to you. Enter it here to continue.",
|
"matrixEnterUser": "Enter your User ID, press submit, and a PIN will be sent to you. Enter it here to continue."
|
||||||
"welcomeUser": "Welcome, {user}!",
|
|
||||||
"addContactMethod": "Add Contact Method",
|
|
||||||
"editContactMethod": "Edit Contact Method",
|
|
||||||
"joinTheServer": "Join the server:",
|
|
||||||
"customMessagePlaceholderHeader": "Customize this card",
|
|
||||||
"customMessagePlaceholderContent": "Click the user page edit button in settings to customize this card, or show one on the login screen, and don't worry, the user can't see this.",
|
|
||||||
"userPageSuccessMessage": "You can see and change details about your account later on the {myAccount} page.",
|
|
||||||
"resetPassword": "Reset Password",
|
|
||||||
"resetPasswordThroughJellyfin": "To reset your password, visit {jfLink} and press the \"Forgot Password\" button.",
|
|
||||||
"resetPasswordThroughLink": "To reset your password, enter your username, email address or a linked contact method username, and submit. A link will be sent to reset your password.",
|
|
||||||
"resetSent": "Reset Sent.",
|
|
||||||
"resetSentDescription": "If an account with the given username/contact method exists, a password reset link has been sent via all contact methods available. The code will expire in 30 minutes.",
|
|
||||||
"changePassword": "Change Password"
|
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorUserExists": "User already exists.",
|
"errorUserExists": "User already exists.",
|
||||||
@ -50,8 +35,6 @@
|
|||||||
"errorCaptcha": "Captcha incorrect.",
|
"errorCaptcha": "Captcha incorrect.",
|
||||||
"errorPassword": "Check password requirements.",
|
"errorPassword": "Check password requirements.",
|
||||||
"errorNoMatch": "Passwords don't match.",
|
"errorNoMatch": "Passwords don't match.",
|
||||||
"errorOldPassword": "Old password incorrect.",
|
|
||||||
"passwordChanged": "Password Changed.",
|
|
||||||
"verified": "Account verified."
|
"verified": "Account verified."
|
||||||
},
|
},
|
||||||
"validationStrings": {
|
"validationStrings": {
|
||||||
|
@ -1,152 +0,0 @@
|
|||||||
{
|
|
||||||
"meta": {
|
|
||||||
"name": ""
|
|
||||||
},
|
|
||||||
"strings": {
|
|
||||||
"pageTitle": "",
|
|
||||||
"next": "",
|
|
||||||
"back": "",
|
|
||||||
"optional": "",
|
|
||||||
"serverType": "",
|
|
||||||
"disabled": "",
|
|
||||||
"enabled": "",
|
|
||||||
"port": "",
|
|
||||||
"message": "",
|
|
||||||
"serverAddress": "",
|
|
||||||
"emailSubject": "",
|
|
||||||
"URL": "",
|
|
||||||
"apiKey": "",
|
|
||||||
"error": "",
|
|
||||||
"errorInvalidUserPass": "",
|
|
||||||
"errorNotAdmin": "",
|
|
||||||
"errorUserDisabled": "",
|
|
||||||
"error404": "",
|
|
||||||
"errorConnectionRefused": "",
|
|
||||||
"errorUnknown": ""
|
|
||||||
},
|
|
||||||
"startPage": {
|
|
||||||
"welcome": "",
|
|
||||||
"pressStart": "",
|
|
||||||
"httpsNotice": "",
|
|
||||||
"start": ""
|
|
||||||
},
|
|
||||||
"endPage": {
|
|
||||||
"finished": "",
|
|
||||||
"restartMessage": "",
|
|
||||||
"refreshPage": ""
|
|
||||||
},
|
|
||||||
"language": {
|
|
||||||
"title": "",
|
|
||||||
"description": "",
|
|
||||||
"defaultAdminLang": "",
|
|
||||||
"defaultFormLang": "",
|
|
||||||
"defaultEmailLang": ""
|
|
||||||
},
|
|
||||||
"general": {
|
|
||||||
"title": "",
|
|
||||||
"listenAddress": "",
|
|
||||||
"urlBase": "",
|
|
||||||
"urlBaseNotice": "",
|
|
||||||
"lightTheme": "",
|
|
||||||
"darkTheme": "",
|
|
||||||
"useHTTPS": "",
|
|
||||||
"httpsPort": "",
|
|
||||||
"useHTTPSNotice": "",
|
|
||||||
"pathToCertificate": "",
|
|
||||||
"pathToKeyFile": ""
|
|
||||||
},
|
|
||||||
"updates": {
|
|
||||||
"title": "",
|
|
||||||
"description": "",
|
|
||||||
"updateChannel": "",
|
|
||||||
"stable": "",
|
|
||||||
"unstable": ""
|
|
||||||
},
|
|
||||||
"login": {
|
|
||||||
"title": "",
|
|
||||||
"description": "",
|
|
||||||
"authorizeWithJellyfin": "",
|
|
||||||
"authorizeManual": "",
|
|
||||||
"adminOnly": "",
|
|
||||||
"allowAll": "",
|
|
||||||
"allowAllDescription": "",
|
|
||||||
"emailNotice": ""
|
|
||||||
},
|
|
||||||
"jellyfinEmby": {
|
|
||||||
"title": "",
|
|
||||||
"description": "",
|
|
||||||
"embyNotice": "",
|
|
||||||
"internal": "",
|
|
||||||
"external": "",
|
|
||||||
"replaceJellyfin": "",
|
|
||||||
"replaceJellyfinNotice": "",
|
|
||||||
"addressExternalNotice": "",
|
|
||||||
"testConnection": ""
|
|
||||||
},
|
|
||||||
"ombi": {
|
|
||||||
"title": "",
|
|
||||||
"description": "",
|
|
||||||
"apiKeyNotice": ""
|
|
||||||
},
|
|
||||||
"messages": {
|
|
||||||
"title": "",
|
|
||||||
"description": ""
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"title": "",
|
|
||||||
"description": "",
|
|
||||||
"method": "",
|
|
||||||
"useEmailAsUsername": "",
|
|
||||||
"useEmailAsUsernameNotice": "",
|
|
||||||
"fromAddress": "",
|
|
||||||
"senderName": "",
|
|
||||||
"dateFormat": "",
|
|
||||||
"dateFormatNotice": "",
|
|
||||||
"encryption": "",
|
|
||||||
"mailgunApiURL": ""
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"title": "",
|
|
||||||
"description": ""
|
|
||||||
},
|
|
||||||
"welcomeEmails": {
|
|
||||||
"title": "",
|
|
||||||
"description": ""
|
|
||||||
},
|
|
||||||
"inviteEmails": {
|
|
||||||
"title": "",
|
|
||||||
"description": ""
|
|
||||||
},
|
|
||||||
"passwordResets": {
|
|
||||||
"title": "",
|
|
||||||
"description": "",
|
|
||||||
"pathToJellyfin": "",
|
|
||||||
"pathToJellyfinNotice": "",
|
|
||||||
"resetLinks": "",
|
|
||||||
"resetLinksNotice": "",
|
|
||||||
"resetLinksLanguage": "",
|
|
||||||
"setPassword": "",
|
|
||||||
"setPasswordNotice": ""
|
|
||||||
},
|
|
||||||
"passwordValidation": {
|
|
||||||
"title": "",
|
|
||||||
"description": "",
|
|
||||||
"length": "",
|
|
||||||
"uppercase": "",
|
|
||||||
"lowercase": "",
|
|
||||||
"numbers": "",
|
|
||||||
"special": ""
|
|
||||||
},
|
|
||||||
"helpMessages": {
|
|
||||||
"title": "",
|
|
||||||
"description": "",
|
|
||||||
"contactMessage": "",
|
|
||||||
"contactMessageNotice": "",
|
|
||||||
"helpMessage": "",
|
|
||||||
"helpMessageNotice": "",
|
|
||||||
"successMessage": "",
|
|
||||||
"successMessageNotice": "",
|
|
||||||
"emailMessage": "",
|
|
||||||
"emailMessageNotice": ""
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"meta": {
|
|
||||||
"name": ""
|
|
||||||
},
|
|
||||||
"strings": {
|
|
||||||
"startMessage": "",
|
|
||||||
"discordStartMessage": "",
|
|
||||||
"matrixStartMessage": "",
|
|
||||||
"invalidPIN": "",
|
|
||||||
"pinSuccess": "",
|
|
||||||
"languageMessage": "",
|
|
||||||
"languageMessageDiscord": "",
|
|
||||||
"languageSet": "",
|
|
||||||
"discordDMs": ""
|
|
||||||
}
|
|
||||||
}
|
|
15
main.go
15
main.go
@ -17,7 +17,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -88,7 +87,7 @@ type appContext struct {
|
|||||||
webFS httpFS
|
webFS httpFS
|
||||||
cssClass string // Default theme, "light"|"dark".
|
cssClass string // Default theme, "light"|"dark".
|
||||||
jellyfinLogin bool
|
jellyfinLogin bool
|
||||||
adminUsers []User
|
users []User
|
||||||
invalidTokens []string
|
invalidTokens []string
|
||||||
// Keeping jf name because I can't think of a better one
|
// Keeping jf name because I can't think of a better one
|
||||||
jf *mediabrowser.MediaBrowser
|
jf *mediabrowser.MediaBrowser
|
||||||
@ -112,8 +111,6 @@ type appContext struct {
|
|||||||
tag Tag
|
tag Tag
|
||||||
update Update
|
update Update
|
||||||
internalPWRs map[string]InternalPWR
|
internalPWRs map[string]InternalPWR
|
||||||
ConfirmationKeys map[string]map[string]newUserDTO // Map of invite code to jwt to request
|
|
||||||
confirmationKeysLock sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateSecret(length int) (string, error) {
|
func generateSecret(length int) (string, error) {
|
||||||
@ -287,7 +284,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.storage.lang.CommonPath = "common"
|
app.storage.lang.CommonPath = "common"
|
||||||
app.storage.lang.UserPath = "form"
|
app.storage.lang.FormPath = "form"
|
||||||
app.storage.lang.AdminPath = "admin"
|
app.storage.lang.AdminPath = "admin"
|
||||||
app.storage.lang.EmailPath = "email"
|
app.storage.lang.EmailPath = "email"
|
||||||
app.storage.lang.TelegramPath = "telegram"
|
app.storage.lang.TelegramPath = "telegram"
|
||||||
@ -453,7 +450,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
user.UserID = shortuuid.New()
|
user.UserID = shortuuid.New()
|
||||||
user.Username = app.config.Section("ui").Key("username").String()
|
user.Username = app.config.Section("ui").Key("username").String()
|
||||||
user.Password = app.config.Section("ui").Key("password").String()
|
user.Password = app.config.Section("ui").Key("password").String()
|
||||||
app.adminUsers = append(app.adminUsers, user)
|
app.users = append(app.users, user)
|
||||||
} else {
|
} else {
|
||||||
app.debug.Println("Using Jellyfin for authentication")
|
app.debug.Println("Using Jellyfin for authentication")
|
||||||
app.authJf, _ = mediabrowser.NewServer(serverType, server, "jfa-go", app.version, "auth", "auth", timeoutHandler, cacheTimeout)
|
app.authJf, _ = mediabrowser.NewServer(serverType, server, "jfa-go", app.version, "auth", "auth", timeoutHandler, cacheTimeout)
|
||||||
@ -648,15 +645,9 @@ func flagPassed(name string) (found bool) {
|
|||||||
// @securityDefinitions.basic getTokenAuth
|
// @securityDefinitions.basic getTokenAuth
|
||||||
// @name getTokenAuth
|
// @name getTokenAuth
|
||||||
|
|
||||||
// @securityDefinitions.basic getUserTokenAuth
|
|
||||||
// @name getUserTokenAuth
|
|
||||||
|
|
||||||
// @tag.name Auth
|
// @tag.name Auth
|
||||||
// @tag.description -Get a token here if running swagger UI locally.-
|
// @tag.description -Get a token here if running swagger UI locally.-
|
||||||
|
|
||||||
// @tag.name User Page
|
|
||||||
// @tag.description User-page related routes.
|
|
||||||
|
|
||||||
// @tag.name Users
|
// @tag.name Users
|
||||||
// @tag.description Jellyfin user related operations.
|
// @tag.description Jellyfin user related operations.
|
||||||
|
|
||||||
|
11
matrix.go
11
matrix.go
@ -83,7 +83,7 @@ func newMatrixDaemon(app *appContext) (d *MatrixDaemon, err error) {
|
|||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
// d.bot.Store.SaveFilterID(d.userID, resp.FilterID)
|
// d.bot.Store.SaveFilterID(d.userID, resp.FilterID)
|
||||||
for _, user := range app.storage.GetMatrix() {
|
for _, user := range app.storage.matrix {
|
||||||
if user.Lang != "" {
|
if user.Lang != "" {
|
||||||
d.languages[id.RoomID(user.RoomID)] = user.Lang
|
d.languages[id.RoomID(user.RoomID)] = user.Lang
|
||||||
}
|
}
|
||||||
@ -176,9 +176,12 @@ func (d *MatrixDaemon) commandLang(evt *event.Event, code, lang string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
d.languages[evt.RoomID] = code
|
d.languages[evt.RoomID] = code
|
||||||
if u, ok := d.app.storage.GetMatrixKey(string(evt.RoomID)); ok {
|
if u, ok := d.app.storage.matrix[string(evt.RoomID)]; ok {
|
||||||
u.Lang = code
|
u.Lang = code
|
||||||
d.app.storage.SetMatrixKey(string(evt.RoomID), u)
|
d.app.storage.matrix[string(evt.RoomID)] = u
|
||||||
|
if err := d.app.storage.storeMatrixUsers(); err != nil {
|
||||||
|
d.app.err.Printf("Matrix: Failed to store Matrix users: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,7 +252,7 @@ func (d *MatrixDaemon) Send(message *Message, users ...MatrixUser) (err error) {
|
|||||||
md := ""
|
md := ""
|
||||||
if message.Markdown != "" {
|
if message.Markdown != "" {
|
||||||
// Convert images to links
|
// Convert images to links
|
||||||
md = string(markdown.ToHTML([]byte(strings.ReplaceAll(message.Markdown, "![", "[")), nil, markdownRenderer))
|
md = string(markdown.ToHTML([]byte(strings.ReplaceAll(message.Markdown, "![", "[")), nil, renderer))
|
||||||
}
|
}
|
||||||
content := &event.MessageEventContent{
|
content := &event.MessageEventContent{
|
||||||
MsgType: "m.text",
|
MsgType: "m.text",
|
||||||
|
@ -139,7 +139,7 @@ func migrateNotificationMethods(app *appContext) error {
|
|||||||
if !strings.Contains(address, "@") {
|
if !strings.Contains(address, "@") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for id, email := range app.storage.GetEmails() {
|
for id, email := range app.storage.emails {
|
||||||
if email.Addr == address {
|
if email.Addr == address {
|
||||||
invite.Notify[id] = notifyPrefs
|
invite.Notify[id] = notifyPrefs
|
||||||
delete(invite.Notify, address)
|
delete(invite.Notify, address)
|
||||||
@ -168,10 +168,10 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
idList := map[string][2]string{}
|
idList := map[string][2]string{}
|
||||||
for jfID, user := range app.storage.GetDiscord() {
|
for jfID, user := range app.storage.discord {
|
||||||
idList[jfID] = [2]string{user.ID, ""}
|
idList[jfID] = [2]string{user.ID, ""}
|
||||||
}
|
}
|
||||||
for jfID, user := range app.storage.GetTelegram() {
|
for jfID, user := range app.storage.telegram {
|
||||||
vals, ok := idList[jfID]
|
vals, ok := idList[jfID]
|
||||||
if !ok {
|
if !ok {
|
||||||
vals = [2]string{"", ""}
|
vals = [2]string{"", ""}
|
||||||
@ -212,7 +212,7 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
|
|||||||
// app.jf.GetUsers(false)
|
// app.jf.GetUsers(false)
|
||||||
//
|
//
|
||||||
// noHyphens := true
|
// noHyphens := true
|
||||||
// for id := range app.storage.GetEmails() {
|
// for id := range app.storage.emails {
|
||||||
// if strings.Contains(id, "-") {
|
// if strings.Contains(id, "-") {
|
||||||
// noHyphens = false
|
// noHyphens = false
|
||||||
// break
|
// break
|
||||||
|
40
models.go
40
models.go
@ -215,7 +215,6 @@ type setting struct {
|
|||||||
Options [][2]string `json:"options,omitempty"`
|
Options [][2]string `json:"options,omitempty"`
|
||||||
DependsTrue string `json:"depends_true,omitempty"` // If specified, this field is enabled when the specified bool setting is enabled.
|
DependsTrue string `json:"depends_true,omitempty"` // If specified, this field is enabled when the specified bool setting is enabled.
|
||||||
DependsFalse string `json:"depends_false,omitempty"` // If specified, opposite behaviour of DependsTrue.
|
DependsFalse string `json:"depends_false,omitempty"` // If specified, opposite behaviour of DependsTrue.
|
||||||
Style string `json:"style,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type section struct {
|
type section struct {
|
||||||
@ -374,42 +373,3 @@ type ReCaptchaResponseDTO struct {
|
|||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
ErrorCodes []string `json:"error-codes"`
|
ErrorCodes []string `json:"error-codes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MyDetailsDTO is sent to the user page to personalize it for the user.
|
|
||||||
type MyDetailsDTO struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Expiry int64 `json:"expiry"`
|
|
||||||
Admin bool `json:"admin"`
|
|
||||||
AccountsAdmin bool `json:"accounts_admin"`
|
|
||||||
Disabled bool `json:"disabled"`
|
|
||||||
Email *MyDetailsContactMethodsDTO `json:"email,omitempty"`
|
|
||||||
Discord *MyDetailsContactMethodsDTO `json:"discord,omitempty"`
|
|
||||||
Telegram *MyDetailsContactMethodsDTO `json:"telegram,omitempty"`
|
|
||||||
Matrix *MyDetailsContactMethodsDTO `json:"matrix,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MyDetailsContactMethodsDTO struct {
|
|
||||||
Value string `json:"value"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ModifyMyEmailDTO struct {
|
|
||||||
Email string `json:"email"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfirmationTarget int
|
|
||||||
|
|
||||||
const (
|
|
||||||
UserEmailChange ConfirmationTarget = iota
|
|
||||||
NoOp
|
|
||||||
)
|
|
||||||
|
|
||||||
type GetMyPINDTO struct {
|
|
||||||
PIN string `json:"pin"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChangeMyPasswordDTO struct {
|
|
||||||
Old string `json:"old"`
|
|
||||||
New string `json:"new"`
|
|
||||||
}
|
|
||||||
|
366
package-lock.json
generated
366
package-lock.json
generated
@ -15,7 +15,7 @@
|
|||||||
"any-date-parser": "^1.5.4",
|
"any-date-parser": "^1.5.4",
|
||||||
"browserslist": "^4.21.7",
|
"browserslist": "^4.21.7",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"esbuild": "^0.18.6",
|
"esbuild": "^0.18.3",
|
||||||
"fs-cheerio": "^3.0.0",
|
"fs-cheerio": "^3.0.0",
|
||||||
"inline-source": "^8.0.2",
|
"inline-source": "^8.0.2",
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^22.1.0",
|
||||||
@ -57,9 +57,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.3.tgz",
|
||||||
"integrity": "sha512-J3lwhDSXBBppSzm/LC1uZ8yKSIpExc+5T8MxrYD9KNVZG81FOAu2VF2gXi/6A/LwDDQQ+b6DpQbYlo3VwxFepQ==",
|
"integrity": "sha512-QOn3VIlL6Qv1eHBpQB/s7simaZgGss2ASyxDOwYSLmc6vD0uuizZkuYawHmuLjWEm5wPwp0JQWhbpaYwwGevYw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@ -72,9 +72,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm64": {
|
"node_modules/@esbuild/android-arm64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.3.tgz",
|
||||||
"integrity": "sha512-pL0Ci8P9q1sWbtPx8CXbc8JvPvvYdJJQ+LO09PLFsbz3aYNdFBGWJjiHU+CaObO4Ames+GOFpXRAJZS2L3ZK/A==",
|
"integrity": "sha512-PgabCsoaEEnnOiF6rUhOBXgYoLFIrHWP6mfLOzuQ1oZ1lwBdTL0hp5ivC4K3Kvz3BD8EipjeQo6l0aty3nr4qQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -87,9 +87,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-x64": {
|
"node_modules/@esbuild/android-x64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.3.tgz",
|
||||||
"integrity": "sha512-hE2vZxOlJ05aY28lUpB0y0RokngtZtcUB+TVl9vnLEnY0z/8BicSvrkThg5/iI1rbf8TwXrbr2heEjl9fLf+EA==",
|
"integrity": "sha512-1OkJf8wNX1W5ucbp5HrK+z42b9DINb4ix59oJH/PIsh9cyFMqjgRKtCBXg0zEWhkmP1k3egdfrnS7cDTpLH43g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -102,9 +102,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.3.tgz",
|
||||||
"integrity": "sha512-/tuyl4R+QhhoROQtuQj9E/yfJtZNdv2HKaHwYhhHGQDN1Teziem2Kh7BWQMumfiY7Lu9g5rO7scWdGE4OsQ6MQ==",
|
"integrity": "sha512-57aofORpY7wDAuMs6DeqpmgSnVfZ63RgGbR/BHdOSTqJgYvHDCMY7/o1myFntl3k0YxtLE3WAm56nMf4qy3UDw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -117,9 +117,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.3.tgz",
|
||||||
"integrity": "sha512-L7IQga2pDT+14Ti8HZwsVfbCjuKP4U213T3tuPggOzyK/p4KaUJxQFXJgfUFHKzU0zOXx8QcYRYZf0hSQtppkw==",
|
"integrity": "sha512-NVBqMnxT9qvgu7Z322LUDlwjh4GDk6wEePyAQnHF9noxik/WvLFmr5v3Vgz5LSvqFducLCxsdmLztKhdpFW0Gg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -132,9 +132,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.3.tgz",
|
||||||
"integrity": "sha512-bq10jFv42V20Kk77NvmO+WEZaLHBKuXcvEowixnBOMkaBgS7kQaqTc77ZJDbsUpXU3KKNLQFZctfaeINmeTsZA==",
|
"integrity": "sha512-XiLK1AsCk2wKxN7j8h9GXXCs8FPZhp07U0rnpwRkAVSVGgLaIWYSqpTRzKjAfqJiZlp+XKo1HwsmDdICEKB3Dg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -147,9 +147,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.3.tgz",
|
||||||
"integrity": "sha512-HbDLlkDZqUMBQaiday0pJzB6/8Xx/10dI3xRebJBReOEeDSeS+7GzTtW9h8ZnfB7/wBCqvtAjGtWQLTNPbR2+g==",
|
"integrity": "sha512-xyITfrF0G3l1gwR79hvNCCWKQ/16uK14xNNPFgzjbIqF4EpBvhO6l3jrWxXFUW51z6dVIl2Szh3x3uIbBWzH1Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -162,9 +162,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm": {
|
"node_modules/@esbuild/linux-arm": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.3.tgz",
|
||||||
"integrity": "sha512-C+5kb6rgsGMmvIdUI7v1PPgC98A6BMv233e97aXZ5AE03iMdlILFD/20HlHrOi0x2CzbspXn9HOnlE4/Ijn5Kw==",
|
"integrity": "sha512-fc/T0QHMzvmnlF+kfD6bHLB8u+17gg13260p/E86yYjVoKNFjonL/+Y0GGQjMbFUas9QijqOa7pcR00a9RNkwg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@ -177,9 +177,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.3.tgz",
|
||||||
"integrity": "sha512-NMY9yg/88MskEZH2s4i6biz/3av+M8xY5ua4HE7CCz5DBz542cr7REe317+v7oKjnYBCijHpkzo5vU85bkXQmQ==",
|
"integrity": "sha512-lsKUYVd8L/j2uNs8dhMjMsKC5MHYh77gR9EThu7YCeeFz1XpIkx1I4a7mhoVfPS2VPVD1pMCh+PgxuAHUcEmXw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -192,9 +192,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.3.tgz",
|
||||||
"integrity": "sha512-AXazA0ljvQEp7cA9jscABNXsjodKbEcqPcAE3rDzKN82Vb3lYOq6INd+HOCA7hk8IegEyHW4T72Z7QGIhyCQEA==",
|
"integrity": "sha512-EyfGWeOwRqK5Xj18vok0qv8IFBZ1/+hKV+cqD44oVhGsxHo9TmPtoSiDrWn8Sa2swq/VuO5Aiog6YPDj81oIkA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@ -207,9 +207,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.3.tgz",
|
||||||
"integrity": "sha512-JjBf7TwY7ldcPgHYt9UcrjZB03+WZqg/jSwMAfzOzM5ZG+tu5umUqzy5ugH/crGI4eoDIhSOTDp1NL3Uo/05Fw==",
|
"integrity": "sha512-PwXkcl3t0kSeYH5RuJIeh/fHOzKZd+ZdifAWzpVO+9TLWArutTFBJvOSkTZ3CcqQqNrTj1Qyo6nqE8MQj/a7cQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@ -222,9 +222,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.3.tgz",
|
||||||
"integrity": "sha512-kATNsslryVxcH1sO3KP2nnyUWtZZVkgyhAUnyTVVa0OQQ9pmDRjTpHaE+2EQHoCM5wt/uav2edrAUqbwn3tkKQ==",
|
"integrity": "sha512-CRVkkSXf5GQcq7Am2a2tdIn85oqi/bkjuPvhNqcdeTgI0xgNbqLnEPRy2AEGkRuaJWB5uCX1IC4sqnY8ET14Yg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
@ -237,9 +237,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.3.tgz",
|
||||||
"integrity": "sha512-B+wTKz+8pi7mcWXFQV0LA79dJ+qhiut5uK9q0omoKnq8yRIwQJwfg3/vclXoqqcX89Ri5Y5538V0Se2v5qlcLA==",
|
"integrity": "sha512-t7zK1Cheh0xvzfZbimztiE0wGnpV+YRsBg3tefcEBN3O4GzgLu6fFpA5HxEyVm3hHZW1jAC4OhoGEp7C5Ii6Eg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@ -252,9 +252,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.3.tgz",
|
||||||
"integrity": "sha512-h44RBLVXFUSjvhOfseE+5UxQ/r9LVeqK2S8JziJKOm9W7SePYRPDyn7MhzhNCCFPkcjIy+soCxfhlJXHXXCR0A==",
|
"integrity": "sha512-fUZPtyCYih6y4lDYdSM4Yoax4nS7aH0/XixJStys+9tfp5cAlIAZhEVKOOdeGXmQn0IEyiUtlIsPnfObbeDQfQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@ -267,9 +267,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.3.tgz",
|
||||||
"integrity": "sha512-FlYpyr2Xc2AUePoAbc84NRV+mj7xpsISeQ36HGf9etrY5rTBEA+IU9HzWVmw5mDFtC62EQxzkLRj8h5Hq85yOQ==",
|
"integrity": "sha512-oIcK2LqHWqfMERqjvaKJ3QJmycHn723HsXIv5gH4iGfmePfSj+gi0ZQv2h4bHUg2bs2gJtV0DlIjGhEuvdgxLw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@ -282,9 +282,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.3.tgz",
|
||||||
"integrity": "sha512-Mc4EUSYwzLci77u0Kao6ajB2WbTe5fNc7+lHwS3a+vJISC/oprwURezUYu1SdWAYoczbsyOvKAJwuNftoAdjjg==",
|
"integrity": "sha512-RW9lpfZ6XZ6f5to2DJPvt0f/4RXEW229Xf++quVoW+YbnPrcapIJChtD/AmZ8cK3hglO/hXxJjs21pV0/l7L5w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -297,9 +297,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.3.tgz",
|
||||||
"integrity": "sha512-3hgZlp7NqIM5lNG3fpdhBI5rUnPmdahraSmwAi+YX/bp7iZ7mpTv2NkypGs/XngdMtpzljICxnUG3uPfqLFd3w==",
|
"integrity": "sha512-piZ2oBoaq58pKZvhgdV6PemlL30Uhd9GmmOkIGZYgChwNcyVSSl6iMEJxMzU7x44Lk9q+hJ6a343M/iVEMEvxA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -312,9 +312,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.3.tgz",
|
||||||
"integrity": "sha512-aEWTdZQHtSRROlDYn7ygB8yAqtnall/UnmoVIJVqccKitkAWVVSYocQUWrBOxLEFk8XdlRouVrLZe6WXszyviA==",
|
"integrity": "sha512-vaMfouYTz/4tKdQsXDccqhV6wgPEr+hfuxdNU5Pl/vQxYTsqcXv5DYEa5Z1RAxCoua5aEB+Uj5V7VT/bM92wxw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -327,9 +327,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.3.tgz",
|
||||||
"integrity": "sha512-uxk/5yAGpjKZUHOECtI9W+9IcLjKj+2m0qf+RG7f7eRBHr8wP6wsr3XbNbgtOD1qSpPapd6R2ZfSeXTkCcAo5g==",
|
"integrity": "sha512-Fa3rNQQ9q1qwy9u2cdDvuGKy3jmPnPPMDdyy/qbn5d395Pb9hjLYiPzX9BozXMPJDlCNofSY7jN3miM9gyAdHA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -342,9 +342,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.3.tgz",
|
||||||
"integrity": "sha512-oXlXGS9zvNCGoAT/tLHAsFKrIKye1JaIIP0anCdpaI+Dc10ftaNZcqfLzEwyhdzFAYInXYH4V7kEdH4hPyo9GA==",
|
"integrity": "sha512-LPJC8ub+9uzyC6ygVmp00dAqet1q1DsZ/OldGIIBt+y+Ctd1OfnKNlzQgXK8nxwY1G8fAhklFSeSRRgAUJnR0w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -357,9 +357,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.3.tgz",
|
||||||
"integrity": "sha512-qh7IcAHUvvmMBmoIG+V+BbE9ZWSR0ohF51e5g8JZvU08kZF58uDFL5tHs0eoYz31H6Finv17te3W3QB042GqVA==",
|
"integrity": "sha512-WtUyRspyxZR6NTc2HG4xd9Wvz8lP4C6OUY1gAqisrf151HvXIxsK0mfAacFJNS7EN2wvPTgjP+SM8vgBOx5+zA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@ -372,9 +372,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.3.tgz",
|
||||||
"integrity": "sha512-9UDwkz7Wlm4N9jnv+4NL7F8vxLhSZfEkRArz2gD33HesAFfMLGIGNVXRoIHtWNw8feKsnGly9Hq1EUuRkWl0zA==",
|
"integrity": "sha512-Z8qCK4BkBm40j5KUM4NrkxYQS0R12cBO1NBVtI4vws6uwh1n/VaNu31Hm+n2cJUWdFbfH57PBghkhm9yLgmPfw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -1647,9 +1647,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.3.tgz",
|
||||||
"integrity": "sha512-5QgxWaAhU/tPBpvkxUmnFv2YINHuZzjbk0LeUUnC2i3aJHjfi5yR49lgKgF7cb98bclOp/kans8M5TGbGFfJlQ==",
|
"integrity": "sha512-eadWJC4CRpj93+miO5ZBlvCv+m2x6pzyNBznTvUeLFObMmxs1IMd8cCf6qiDVEZuDL6W8W7u+ZNW3GKEfOdDsA==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"esbuild": "bin/esbuild"
|
"esbuild": "bin/esbuild"
|
||||||
@ -1658,28 +1658,28 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@esbuild/android-arm": "0.18.6",
|
"@esbuild/android-arm": "0.18.3",
|
||||||
"@esbuild/android-arm64": "0.18.6",
|
"@esbuild/android-arm64": "0.18.3",
|
||||||
"@esbuild/android-x64": "0.18.6",
|
"@esbuild/android-x64": "0.18.3",
|
||||||
"@esbuild/darwin-arm64": "0.18.6",
|
"@esbuild/darwin-arm64": "0.18.3",
|
||||||
"@esbuild/darwin-x64": "0.18.6",
|
"@esbuild/darwin-x64": "0.18.3",
|
||||||
"@esbuild/freebsd-arm64": "0.18.6",
|
"@esbuild/freebsd-arm64": "0.18.3",
|
||||||
"@esbuild/freebsd-x64": "0.18.6",
|
"@esbuild/freebsd-x64": "0.18.3",
|
||||||
"@esbuild/linux-arm": "0.18.6",
|
"@esbuild/linux-arm": "0.18.3",
|
||||||
"@esbuild/linux-arm64": "0.18.6",
|
"@esbuild/linux-arm64": "0.18.3",
|
||||||
"@esbuild/linux-ia32": "0.18.6",
|
"@esbuild/linux-ia32": "0.18.3",
|
||||||
"@esbuild/linux-loong64": "0.18.6",
|
"@esbuild/linux-loong64": "0.18.3",
|
||||||
"@esbuild/linux-mips64el": "0.18.6",
|
"@esbuild/linux-mips64el": "0.18.3",
|
||||||
"@esbuild/linux-ppc64": "0.18.6",
|
"@esbuild/linux-ppc64": "0.18.3",
|
||||||
"@esbuild/linux-riscv64": "0.18.6",
|
"@esbuild/linux-riscv64": "0.18.3",
|
||||||
"@esbuild/linux-s390x": "0.18.6",
|
"@esbuild/linux-s390x": "0.18.3",
|
||||||
"@esbuild/linux-x64": "0.18.6",
|
"@esbuild/linux-x64": "0.18.3",
|
||||||
"@esbuild/netbsd-x64": "0.18.6",
|
"@esbuild/netbsd-x64": "0.18.3",
|
||||||
"@esbuild/openbsd-x64": "0.18.6",
|
"@esbuild/openbsd-x64": "0.18.3",
|
||||||
"@esbuild/sunos-x64": "0.18.6",
|
"@esbuild/sunos-x64": "0.18.3",
|
||||||
"@esbuild/win32-arm64": "0.18.6",
|
"@esbuild/win32-arm64": "0.18.3",
|
||||||
"@esbuild/win32-ia32": "0.18.6",
|
"@esbuild/win32-ia32": "0.18.3",
|
||||||
"@esbuild/win32-x64": "0.18.6"
|
"@esbuild/win32-x64": "0.18.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
@ -6797,135 +6797,135 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@esbuild/android-arm": {
|
"@esbuild/android-arm": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.3.tgz",
|
||||||
"integrity": "sha512-J3lwhDSXBBppSzm/LC1uZ8yKSIpExc+5T8MxrYD9KNVZG81FOAu2VF2gXi/6A/LwDDQQ+b6DpQbYlo3VwxFepQ==",
|
"integrity": "sha512-QOn3VIlL6Qv1eHBpQB/s7simaZgGss2ASyxDOwYSLmc6vD0uuizZkuYawHmuLjWEm5wPwp0JQWhbpaYwwGevYw==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/android-arm64": {
|
"@esbuild/android-arm64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.3.tgz",
|
||||||
"integrity": "sha512-pL0Ci8P9q1sWbtPx8CXbc8JvPvvYdJJQ+LO09PLFsbz3aYNdFBGWJjiHU+CaObO4Ames+GOFpXRAJZS2L3ZK/A==",
|
"integrity": "sha512-PgabCsoaEEnnOiF6rUhOBXgYoLFIrHWP6mfLOzuQ1oZ1lwBdTL0hp5ivC4K3Kvz3BD8EipjeQo6l0aty3nr4qQ==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/android-x64": {
|
"@esbuild/android-x64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.3.tgz",
|
||||||
"integrity": "sha512-hE2vZxOlJ05aY28lUpB0y0RokngtZtcUB+TVl9vnLEnY0z/8BicSvrkThg5/iI1rbf8TwXrbr2heEjl9fLf+EA==",
|
"integrity": "sha512-1OkJf8wNX1W5ucbp5HrK+z42b9DINb4ix59oJH/PIsh9cyFMqjgRKtCBXg0zEWhkmP1k3egdfrnS7cDTpLH43g==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/darwin-arm64": {
|
"@esbuild/darwin-arm64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.3.tgz",
|
||||||
"integrity": "sha512-/tuyl4R+QhhoROQtuQj9E/yfJtZNdv2HKaHwYhhHGQDN1Teziem2Kh7BWQMumfiY7Lu9g5rO7scWdGE4OsQ6MQ==",
|
"integrity": "sha512-57aofORpY7wDAuMs6DeqpmgSnVfZ63RgGbR/BHdOSTqJgYvHDCMY7/o1myFntl3k0YxtLE3WAm56nMf4qy3UDw==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/darwin-x64": {
|
"@esbuild/darwin-x64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.3.tgz",
|
||||||
"integrity": "sha512-L7IQga2pDT+14Ti8HZwsVfbCjuKP4U213T3tuPggOzyK/p4KaUJxQFXJgfUFHKzU0zOXx8QcYRYZf0hSQtppkw==",
|
"integrity": "sha512-NVBqMnxT9qvgu7Z322LUDlwjh4GDk6wEePyAQnHF9noxik/WvLFmr5v3Vgz5LSvqFducLCxsdmLztKhdpFW0Gg==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/freebsd-arm64": {
|
"@esbuild/freebsd-arm64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.3.tgz",
|
||||||
"integrity": "sha512-bq10jFv42V20Kk77NvmO+WEZaLHBKuXcvEowixnBOMkaBgS7kQaqTc77ZJDbsUpXU3KKNLQFZctfaeINmeTsZA==",
|
"integrity": "sha512-XiLK1AsCk2wKxN7j8h9GXXCs8FPZhp07U0rnpwRkAVSVGgLaIWYSqpTRzKjAfqJiZlp+XKo1HwsmDdICEKB3Dg==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/freebsd-x64": {
|
"@esbuild/freebsd-x64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.3.tgz",
|
||||||
"integrity": "sha512-HbDLlkDZqUMBQaiday0pJzB6/8Xx/10dI3xRebJBReOEeDSeS+7GzTtW9h8ZnfB7/wBCqvtAjGtWQLTNPbR2+g==",
|
"integrity": "sha512-xyITfrF0G3l1gwR79hvNCCWKQ/16uK14xNNPFgzjbIqF4EpBvhO6l3jrWxXFUW51z6dVIl2Szh3x3uIbBWzH1Q==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-arm": {
|
"@esbuild/linux-arm": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.3.tgz",
|
||||||
"integrity": "sha512-C+5kb6rgsGMmvIdUI7v1PPgC98A6BMv233e97aXZ5AE03iMdlILFD/20HlHrOi0x2CzbspXn9HOnlE4/Ijn5Kw==",
|
"integrity": "sha512-fc/T0QHMzvmnlF+kfD6bHLB8u+17gg13260p/E86yYjVoKNFjonL/+Y0GGQjMbFUas9QijqOa7pcR00a9RNkwg==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-arm64": {
|
"@esbuild/linux-arm64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.3.tgz",
|
||||||
"integrity": "sha512-NMY9yg/88MskEZH2s4i6biz/3av+M8xY5ua4HE7CCz5DBz542cr7REe317+v7oKjnYBCijHpkzo5vU85bkXQmQ==",
|
"integrity": "sha512-lsKUYVd8L/j2uNs8dhMjMsKC5MHYh77gR9EThu7YCeeFz1XpIkx1I4a7mhoVfPS2VPVD1pMCh+PgxuAHUcEmXw==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-ia32": {
|
"@esbuild/linux-ia32": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.3.tgz",
|
||||||
"integrity": "sha512-AXazA0ljvQEp7cA9jscABNXsjodKbEcqPcAE3rDzKN82Vb3lYOq6INd+HOCA7hk8IegEyHW4T72Z7QGIhyCQEA==",
|
"integrity": "sha512-EyfGWeOwRqK5Xj18vok0qv8IFBZ1/+hKV+cqD44oVhGsxHo9TmPtoSiDrWn8Sa2swq/VuO5Aiog6YPDj81oIkA==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-loong64": {
|
"@esbuild/linux-loong64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.3.tgz",
|
||||||
"integrity": "sha512-JjBf7TwY7ldcPgHYt9UcrjZB03+WZqg/jSwMAfzOzM5ZG+tu5umUqzy5ugH/crGI4eoDIhSOTDp1NL3Uo/05Fw==",
|
"integrity": "sha512-PwXkcl3t0kSeYH5RuJIeh/fHOzKZd+ZdifAWzpVO+9TLWArutTFBJvOSkTZ3CcqQqNrTj1Qyo6nqE8MQj/a7cQ==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-mips64el": {
|
"@esbuild/linux-mips64el": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.3.tgz",
|
||||||
"integrity": "sha512-kATNsslryVxcH1sO3KP2nnyUWtZZVkgyhAUnyTVVa0OQQ9pmDRjTpHaE+2EQHoCM5wt/uav2edrAUqbwn3tkKQ==",
|
"integrity": "sha512-CRVkkSXf5GQcq7Am2a2tdIn85oqi/bkjuPvhNqcdeTgI0xgNbqLnEPRy2AEGkRuaJWB5uCX1IC4sqnY8ET14Yg==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-ppc64": {
|
"@esbuild/linux-ppc64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.3.tgz",
|
||||||
"integrity": "sha512-B+wTKz+8pi7mcWXFQV0LA79dJ+qhiut5uK9q0omoKnq8yRIwQJwfg3/vclXoqqcX89Ri5Y5538V0Se2v5qlcLA==",
|
"integrity": "sha512-t7zK1Cheh0xvzfZbimztiE0wGnpV+YRsBg3tefcEBN3O4GzgLu6fFpA5HxEyVm3hHZW1jAC4OhoGEp7C5Ii6Eg==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-riscv64": {
|
"@esbuild/linux-riscv64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.3.tgz",
|
||||||
"integrity": "sha512-h44RBLVXFUSjvhOfseE+5UxQ/r9LVeqK2S8JziJKOm9W7SePYRPDyn7MhzhNCCFPkcjIy+soCxfhlJXHXXCR0A==",
|
"integrity": "sha512-fUZPtyCYih6y4lDYdSM4Yoax4nS7aH0/XixJStys+9tfp5cAlIAZhEVKOOdeGXmQn0IEyiUtlIsPnfObbeDQfQ==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-s390x": {
|
"@esbuild/linux-s390x": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.3.tgz",
|
||||||
"integrity": "sha512-FlYpyr2Xc2AUePoAbc84NRV+mj7xpsISeQ36HGf9etrY5rTBEA+IU9HzWVmw5mDFtC62EQxzkLRj8h5Hq85yOQ==",
|
"integrity": "sha512-oIcK2LqHWqfMERqjvaKJ3QJmycHn723HsXIv5gH4iGfmePfSj+gi0ZQv2h4bHUg2bs2gJtV0DlIjGhEuvdgxLw==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/linux-x64": {
|
"@esbuild/linux-x64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.3.tgz",
|
||||||
"integrity": "sha512-Mc4EUSYwzLci77u0Kao6ajB2WbTe5fNc7+lHwS3a+vJISC/oprwURezUYu1SdWAYoczbsyOvKAJwuNftoAdjjg==",
|
"integrity": "sha512-RW9lpfZ6XZ6f5to2DJPvt0f/4RXEW229Xf++quVoW+YbnPrcapIJChtD/AmZ8cK3hglO/hXxJjs21pV0/l7L5w==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/netbsd-x64": {
|
"@esbuild/netbsd-x64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.3.tgz",
|
||||||
"integrity": "sha512-3hgZlp7NqIM5lNG3fpdhBI5rUnPmdahraSmwAi+YX/bp7iZ7mpTv2NkypGs/XngdMtpzljICxnUG3uPfqLFd3w==",
|
"integrity": "sha512-piZ2oBoaq58pKZvhgdV6PemlL30Uhd9GmmOkIGZYgChwNcyVSSl6iMEJxMzU7x44Lk9q+hJ6a343M/iVEMEvxA==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/openbsd-x64": {
|
"@esbuild/openbsd-x64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.3.tgz",
|
||||||
"integrity": "sha512-aEWTdZQHtSRROlDYn7ygB8yAqtnall/UnmoVIJVqccKitkAWVVSYocQUWrBOxLEFk8XdlRouVrLZe6WXszyviA==",
|
"integrity": "sha512-vaMfouYTz/4tKdQsXDccqhV6wgPEr+hfuxdNU5Pl/vQxYTsqcXv5DYEa5Z1RAxCoua5aEB+Uj5V7VT/bM92wxw==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/sunos-x64": {
|
"@esbuild/sunos-x64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.3.tgz",
|
||||||
"integrity": "sha512-uxk/5yAGpjKZUHOECtI9W+9IcLjKj+2m0qf+RG7f7eRBHr8wP6wsr3XbNbgtOD1qSpPapd6R2ZfSeXTkCcAo5g==",
|
"integrity": "sha512-Fa3rNQQ9q1qwy9u2cdDvuGKy3jmPnPPMDdyy/qbn5d395Pb9hjLYiPzX9BozXMPJDlCNofSY7jN3miM9gyAdHA==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/win32-arm64": {
|
"@esbuild/win32-arm64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.3.tgz",
|
||||||
"integrity": "sha512-oXlXGS9zvNCGoAT/tLHAsFKrIKye1JaIIP0anCdpaI+Dc10ftaNZcqfLzEwyhdzFAYInXYH4V7kEdH4hPyo9GA==",
|
"integrity": "sha512-LPJC8ub+9uzyC6ygVmp00dAqet1q1DsZ/OldGIIBt+y+Ctd1OfnKNlzQgXK8nxwY1G8fAhklFSeSRRgAUJnR0w==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/win32-ia32": {
|
"@esbuild/win32-ia32": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.3.tgz",
|
||||||
"integrity": "sha512-qh7IcAHUvvmMBmoIG+V+BbE9ZWSR0ohF51e5g8JZvU08kZF58uDFL5tHs0eoYz31H6Finv17te3W3QB042GqVA==",
|
"integrity": "sha512-WtUyRspyxZR6NTc2HG4xd9Wvz8lP4C6OUY1gAqisrf151HvXIxsK0mfAacFJNS7EN2wvPTgjP+SM8vgBOx5+zA==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@esbuild/win32-x64": {
|
"@esbuild/win32-x64": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.3.tgz",
|
||||||
"integrity": "sha512-9UDwkz7Wlm4N9jnv+4NL7F8vxLhSZfEkRArz2gD33HesAFfMLGIGNVXRoIHtWNw8feKsnGly9Hq1EUuRkWl0zA==",
|
"integrity": "sha512-Z8qCK4BkBm40j5KUM4NrkxYQS0R12cBO1NBVtI4vws6uwh1n/VaNu31Hm+n2cJUWdFbfH57PBghkhm9yLgmPfw==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@jridgewell/gen-mapping": {
|
"@jridgewell/gen-mapping": {
|
||||||
@ -7896,32 +7896,32 @@
|
|||||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
|
||||||
},
|
},
|
||||||
"esbuild": {
|
"esbuild": {
|
||||||
"version": "0.18.6",
|
"version": "0.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.3.tgz",
|
||||||
"integrity": "sha512-5QgxWaAhU/tPBpvkxUmnFv2YINHuZzjbk0LeUUnC2i3aJHjfi5yR49lgKgF7cb98bclOp/kans8M5TGbGFfJlQ==",
|
"integrity": "sha512-eadWJC4CRpj93+miO5ZBlvCv+m2x6pzyNBznTvUeLFObMmxs1IMd8cCf6qiDVEZuDL6W8W7u+ZNW3GKEfOdDsA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@esbuild/android-arm": "0.18.6",
|
"@esbuild/android-arm": "0.18.3",
|
||||||
"@esbuild/android-arm64": "0.18.6",
|
"@esbuild/android-arm64": "0.18.3",
|
||||||
"@esbuild/android-x64": "0.18.6",
|
"@esbuild/android-x64": "0.18.3",
|
||||||
"@esbuild/darwin-arm64": "0.18.6",
|
"@esbuild/darwin-arm64": "0.18.3",
|
||||||
"@esbuild/darwin-x64": "0.18.6",
|
"@esbuild/darwin-x64": "0.18.3",
|
||||||
"@esbuild/freebsd-arm64": "0.18.6",
|
"@esbuild/freebsd-arm64": "0.18.3",
|
||||||
"@esbuild/freebsd-x64": "0.18.6",
|
"@esbuild/freebsd-x64": "0.18.3",
|
||||||
"@esbuild/linux-arm": "0.18.6",
|
"@esbuild/linux-arm": "0.18.3",
|
||||||
"@esbuild/linux-arm64": "0.18.6",
|
"@esbuild/linux-arm64": "0.18.3",
|
||||||
"@esbuild/linux-ia32": "0.18.6",
|
"@esbuild/linux-ia32": "0.18.3",
|
||||||
"@esbuild/linux-loong64": "0.18.6",
|
"@esbuild/linux-loong64": "0.18.3",
|
||||||
"@esbuild/linux-mips64el": "0.18.6",
|
"@esbuild/linux-mips64el": "0.18.3",
|
||||||
"@esbuild/linux-ppc64": "0.18.6",
|
"@esbuild/linux-ppc64": "0.18.3",
|
||||||
"@esbuild/linux-riscv64": "0.18.6",
|
"@esbuild/linux-riscv64": "0.18.3",
|
||||||
"@esbuild/linux-s390x": "0.18.6",
|
"@esbuild/linux-s390x": "0.18.3",
|
||||||
"@esbuild/linux-x64": "0.18.6",
|
"@esbuild/linux-x64": "0.18.3",
|
||||||
"@esbuild/netbsd-x64": "0.18.6",
|
"@esbuild/netbsd-x64": "0.18.3",
|
||||||
"@esbuild/openbsd-x64": "0.18.6",
|
"@esbuild/openbsd-x64": "0.18.3",
|
||||||
"@esbuild/sunos-x64": "0.18.6",
|
"@esbuild/sunos-x64": "0.18.3",
|
||||||
"@esbuild/win32-arm64": "0.18.6",
|
"@esbuild/win32-arm64": "0.18.3",
|
||||||
"@esbuild/win32-ia32": "0.18.6",
|
"@esbuild/win32-ia32": "0.18.3",
|
||||||
"@esbuild/win32-x64": "0.18.6"
|
"@esbuild/win32-x64": "0.18.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"escalade": {
|
"escalade": {
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
"any-date-parser": "^1.5.4",
|
"any-date-parser": "^1.5.4",
|
||||||
"browserslist": "^4.21.7",
|
"browserslist": "^4.21.7",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"esbuild": "^0.18.6",
|
"esbuild": "^0.18.3",
|
||||||
"fs-cheerio": "^3.0.0",
|
"fs-cheerio": "^3.0.0",
|
||||||
"inline-source": "^8.0.2",
|
"inline-source": "^8.0.2",
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^22.1.0",
|
||||||
|
13
pwreset.go
13
pwreset.go
@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -26,18 +25,6 @@ func (app *appContext) GenInternalReset(userID string) (InternalPWR, error) {
|
|||||||
return pwr, nil
|
return pwr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenResetLink generates and returns a password reset link.
|
|
||||||
func (app *appContext) GenResetLink(pin string) (string, error) {
|
|
||||||
url := app.config.Section("password_resets").Key("url_base").String()
|
|
||||||
var pinLink string
|
|
||||||
if url == "" {
|
|
||||||
return pinLink, fmt.Errorf("disabled as no URL Base provided. Set in Settings > Password Resets.")
|
|
||||||
}
|
|
||||||
// Strip /invite from end of this URL, ik it's ugly.
|
|
||||||
pinLink = fmt.Sprintf("%s/reset?pin=%s", url, pin)
|
|
||||||
return pinLink, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *appContext) StartPWR() {
|
func (app *appContext) StartPWR() {
|
||||||
app.info.Println("Starting password reset daemon")
|
app.info.Println("Starting password reset daemon")
|
||||||
path := app.config.Section("password_resets").Key("watch_directory").String()
|
path := app.config.Section("password_resets").Key("watch_directory").String()
|
||||||
|
41
router.go
41
router.go
@ -101,9 +101,6 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
if app.URLBase != "" {
|
if app.URLBase != "" {
|
||||||
routePrefixes = append(routePrefixes, "")
|
routePrefixes = append(routePrefixes, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
userPageEnabled := app.config.Section("user_page").Key("enabled").MustBool(true) && app.config.Section("ui").Key("jellyfin_login").MustBool(true)
|
|
||||||
|
|
||||||
for _, p := range routePrefixes {
|
for _, p := range routePrefixes {
|
||||||
router.GET(p+"/lang/:page", app.GetLanguages)
|
router.GET(p+"/lang/:page", app.GetLanguages)
|
||||||
router.Use(static.Serve(p+"/", app.webFS))
|
router.Use(static.Serve(p+"/", app.webFS))
|
||||||
@ -143,13 +140,6 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
router.POST(p+"/invite/:invCode/matrix/user", app.MatrixSendPIN)
|
router.POST(p+"/invite/:invCode/matrix/user", app.MatrixSendPIN)
|
||||||
router.POST(p+"/users/matrix", app.MatrixConnect)
|
router.POST(p+"/users/matrix", app.MatrixConnect)
|
||||||
}
|
}
|
||||||
if userPageEnabled {
|
|
||||||
router.GET(p+"/my/account", app.MyUserPage)
|
|
||||||
router.GET(p+"/my/token/login", app.getUserTokenLogin)
|
|
||||||
router.GET(p+"/my/token/refresh", app.getUserTokenRefresh)
|
|
||||||
router.GET(p+"/my/confirm/:jwt", app.ConfirmMyAction)
|
|
||||||
router.POST(p+"/my/password/reset/:address", app.ResetMyPassword)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if *SWAGGER {
|
if *SWAGGER {
|
||||||
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
|
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
|
||||||
@ -157,14 +147,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
api := router.Group("/", app.webAuth())
|
api := router.Group("/", app.webAuth())
|
||||||
|
|
||||||
var user *gin.RouterGroup
|
|
||||||
if userPageEnabled {
|
|
||||||
user = router.Group("/my", app.userAuth())
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range routePrefixes {
|
for _, p := range routePrefixes {
|
||||||
router.POST(p+"/logout", app.Logout)
|
router.POST(p+"/logout", app.Logout)
|
||||||
api.DELETE(p+"/users", app.DeleteUsers)
|
api.DELETE(p+"/users", app.DeleteUsers)
|
||||||
@ -197,10 +180,10 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
|
|
||||||
api.GET(p+"/config/update", app.CheckUpdate)
|
api.GET(p+"/config/update", app.CheckUpdate)
|
||||||
api.POST(p+"/config/update", app.ApplyUpdate)
|
api.POST(p+"/config/update", app.ApplyUpdate)
|
||||||
api.GET(p+"/config/emails", app.GetCustomContent)
|
api.GET(p+"/config/emails", app.GetCustomEmails)
|
||||||
api.GET(p+"/config/emails/:id", app.GetCustomMessageTemplate)
|
api.GET(p+"/config/emails/:id", app.GetCustomEmailTemplate)
|
||||||
api.POST(p+"/config/emails/:id", app.SetCustomMessage)
|
api.POST(p+"/config/emails/:id", app.SetCustomEmail)
|
||||||
api.POST(p+"/config/emails/:id/state/:state", app.SetCustomMessageState)
|
api.POST(p+"/config/emails/:id/state/:state", app.SetCustomEmailState)
|
||||||
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)
|
||||||
@ -227,22 +210,6 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
}
|
}
|
||||||
api.POST(p+"/matrix/login", app.MatrixLogin)
|
api.POST(p+"/matrix/login", app.MatrixLogin)
|
||||||
|
|
||||||
if userPageEnabled {
|
|
||||||
user.GET(p+"/details", app.MyDetails)
|
|
||||||
user.POST(p+"/contact", app.SetMyContactMethods)
|
|
||||||
user.POST(p+"/logout", app.LogoutUser)
|
|
||||||
user.POST(p+"/email", app.ModifyMyEmail)
|
|
||||||
user.GET(p+"/discord/invite", app.MyDiscordServerInvite)
|
|
||||||
user.GET(p+"/pin/:service", app.GetMyPIN)
|
|
||||||
user.GET(p+"/discord/verified/:pin", app.MyDiscordVerifiedInvite)
|
|
||||||
user.GET(p+"/telegram/verified/:pin", app.MyTelegramVerifiedInvite)
|
|
||||||
user.POST(p+"/matrix/user", app.MatrixSendMyPIN)
|
|
||||||
user.GET(p+"/matrix/verified/:userID/:pin", app.MatrixCheckMyPIN)
|
|
||||||
user.DELETE(p+"/discord", app.UnlinkMyDiscord)
|
|
||||||
user.DELETE(p+"/telegram", app.UnlinkMyTelegram)
|
|
||||||
user.DELETE(p+"/matrix", app.UnlinkMyMatrix)
|
|
||||||
user.POST(p+"/password", app.ChangeMyPassword)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,8 +21,6 @@ def generate_ini(base_file, ini_file):
|
|||||||
if "meta" in config_base["sections"][section]:
|
if "meta" in config_base["sections"][section]:
|
||||||
ini.set(section, fix_description(config_base["sections"][section]["meta"]["description"]))
|
ini.set(section, fix_description(config_base["sections"][section]["meta"]["description"]))
|
||||||
for entry in config_base["sections"][section]["settings"]:
|
for entry in config_base["sections"][section]["settings"]:
|
||||||
if config_base["sections"][section]["settings"][entry]["type"] == "note":
|
|
||||||
continue
|
|
||||||
if "description" in config_base["sections"][section]["settings"][entry]:
|
if "description" in config_base["sections"][section]["settings"][entry]:
|
||||||
ini.set(section, fix_description(config_base["sections"][section]["settings"][entry]["description"]))
|
ini.set(section, fix_description(config_base["sections"][section]["settings"][entry]["description"]))
|
||||||
value = config_base["sections"][section]["settings"][entry]["value"]
|
value = config_base["sections"][section]["settings"][entry]["value"]
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
# *langmover*
|
|
||||||
|
|
||||||
* Makes moving strings between language files a little easier.
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
|
|
||||||
You'll need to create a template file. See example `template.json`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"meta": {
|
|
||||||
"explanation": "values here can either be folder, folder:section, or folder:section:subkey, and then either nothing, or /keyname. It all depends on whether the sections and keys match up, or if you want to pull a plural/singular only or not."
|
|
||||||
},
|
|
||||||
"strings": {
|
|
||||||
"inviteInfiniteUsesWarning": "admin", // Resolves to admin/strings/inviteInfiniteUsesWarning
|
|
||||||
"emailAddress": "form:strings/emailAddress", // Resolves to form/strings/emailAddress
|
|
||||||
"modifySettingsFor": "admin:quantityStrings:plural/", // Resolves to admin/quantityStrings/modifySettingsFor/plural
|
|
||||||
"deleteNUsers": "admin:quantityStrings:singular/deleteNUsers" // Resolves to admin/quantityStrings/deleteNUsers/singular
|
|
||||||
},
|
|
||||||
"quantityStrings": {
|
|
||||||
"reEnableUsers": "admin" // Resolves to admin/quantityStrings/reEnableUsers
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Args:
|
|
||||||
* `--source`: Source `lang/` directory. **Always run on a copy, to avoid data loss**
|
|
||||||
* `--template`: Template JSON file.
|
|
||||||
* `--output`: Output directory. Will be filled with lang files (e.g. "en-us.json", "fa-ir.json", ...).
|
|
||||||
* `--extract`: Passing will remove the templated strings from their source file. **Modifies the source directory**.
|
|
||||||
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
|||||||
{
|
|
||||||
"meta": {
|
|
||||||
"name": "English (US)"
|
|
||||||
},
|
|
||||||
"strings": {
|
|
||||||
"username": "common",
|
|
||||||
"password": "common",
|
|
||||||
"emailAddress": "common",
|
|
||||||
"name": "common",
|
|
||||||
"submit": "common",
|
|
||||||
"send": "common",
|
|
||||||
"success": "common",
|
|
||||||
"continue": "common",
|
|
||||||
"error": "common",
|
|
||||||
"copy": "common",
|
|
||||||
"copied": "common",
|
|
||||||
"time24h": "common",
|
|
||||||
"time12h": "common",
|
|
||||||
"linkTelegram": "common",
|
|
||||||
"contactEmail": "common",
|
|
||||||
"contactTelegram": "common",
|
|
||||||
"linkDiscord": "common",
|
|
||||||
"linkMatrix": "common",
|
|
||||||
"contactDiscord": "common",
|
|
||||||
"theme": "common",
|
|
||||||
"refresh": "common",
|
|
||||||
"required": "common",
|
|
||||||
"login": "common",
|
|
||||||
"logout": "common",
|
|
||||||
"admin": "common",
|
|
||||||
"enabled": "common",
|
|
||||||
"disabled": "common",
|
|
||||||
"reEnable": "common",
|
|
||||||
"disable": "common",
|
|
||||||
"contactMethods": "common",
|
|
||||||
"accountStatus": "common",
|
|
||||||
"notSet": "common",
|
|
||||||
"expiry": "common",
|
|
||||||
"add": "common",
|
|
||||||
"edit": "common",
|
|
||||||
"delete": "admin"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorLoginBlank": "common",
|
|
||||||
"errorConnection": "common",
|
|
||||||
"errorUnknown": "common",
|
|
||||||
"error401Unauthorized": "common",
|
|
||||||
"errorSaveSettings": "common"
|
|
||||||
},
|
|
||||||
"quantityStrings": {
|
|
||||||
"year": "common",
|
|
||||||
"month": "common",
|
|
||||||
"day": "common"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,138 +0,0 @@
|
|||||||
import json, argparse, os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = "en-us.json"
|
|
||||||
|
|
||||||
# Tree structure: <lang-code.json>/<folder>/<json content>
|
|
||||||
def generateTree(src: Path):
|
|
||||||
tree = {}
|
|
||||||
langs = {}
|
|
||||||
directories = []
|
|
||||||
|
|
||||||
def readLangFile(path: Path):
|
|
||||||
with open(path, 'r') as f:
|
|
||||||
content = json.load(f)
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
for directory in os.scandir(src):
|
|
||||||
if not directory.is_dir(): continue
|
|
||||||
directories.append(directory.name)
|
|
||||||
|
|
||||||
# tree[directory.name] = {}
|
|
||||||
|
|
||||||
for lang in os.scandir(directory.path):
|
|
||||||
if not lang.is_file(): continue
|
|
||||||
if not ".json" in lang.name: continue
|
|
||||||
if lang.name not in langs:
|
|
||||||
langs[lang.name] = True
|
|
||||||
if lang.name.lower() not in tree:
|
|
||||||
tree[lang.name.lower()] = {}
|
|
||||||
|
|
||||||
for lang in langs:
|
|
||||||
for directory in directories:
|
|
||||||
filepath = Path(src) / Path(directory) / Path(lang)
|
|
||||||
if not filepath.exists(): continue
|
|
||||||
tree[lang.lower()][directory] = readLangFile(filepath)
|
|
||||||
|
|
||||||
return tree
|
|
||||||
|
|
||||||
def parseKey(langTree, currentSection: str, fieldName: str, key: str, extract=False):
|
|
||||||
temp = key.split("/")
|
|
||||||
loc = temp[0]
|
|
||||||
k = ""
|
|
||||||
if len(temp) > 1:
|
|
||||||
k = temp[1]
|
|
||||||
|
|
||||||
sections = loc.split(":")
|
|
||||||
|
|
||||||
# folder, folder:section or folder:section:subkey
|
|
||||||
folder = sections[0]
|
|
||||||
section = currentSection
|
|
||||||
subkey = None
|
|
||||||
if len(sections) > 1:
|
|
||||||
section = sections[1]
|
|
||||||
if len(sections) > 2:
|
|
||||||
subkey = sections[2]
|
|
||||||
|
|
||||||
|
|
||||||
if k == '':
|
|
||||||
k = fieldName
|
|
||||||
|
|
||||||
value = ""
|
|
||||||
if folder in langTree and section in langTree[folder] and k in langTree[folder][section]:
|
|
||||||
value = langTree[folder][section][k]
|
|
||||||
if extract:
|
|
||||||
s = langTree[folder][section]
|
|
||||||
del s[k]
|
|
||||||
langTree[folder][section] = s
|
|
||||||
|
|
||||||
if subkey is not None and subkey in value:
|
|
||||||
value = value[subkey]
|
|
||||||
|
|
||||||
return (langTree, folder, value)
|
|
||||||
|
|
||||||
|
|
||||||
def generate(templ: Path, source: Path, output: Path, extract: str, tree):
|
|
||||||
with open(templ, "r") as f:
|
|
||||||
template = json.load(f)
|
|
||||||
|
|
||||||
if not output.exists():
|
|
||||||
output.mkdir()
|
|
||||||
|
|
||||||
for lang in tree:
|
|
||||||
out = {}
|
|
||||||
for section in template:
|
|
||||||
if section == "meta":
|
|
||||||
# grab a meta section from the first file we find
|
|
||||||
for file in tree[lang]:
|
|
||||||
out["meta"] = tree[lang][file]["meta"]
|
|
||||||
break
|
|
||||||
|
|
||||||
continue
|
|
||||||
|
|
||||||
folder = ""
|
|
||||||
out[section] = {}
|
|
||||||
for key in template[section]:
|
|
||||||
(tree[lang], folder, val) = parseKey(tree[lang], section, key, template[section][key], extract)
|
|
||||||
if val != "":
|
|
||||||
out[section][key] = val
|
|
||||||
|
|
||||||
# if extract and val != "":
|
|
||||||
# with open(source / folder / lang, "w") as f:
|
|
||||||
# json.dump(modifiedTree[folder], f, indent=4, ensure_ascii=False)
|
|
||||||
|
|
||||||
with open(output / Path(lang), "w") as f:
|
|
||||||
json.dump(out, f, indent=4, ensure_ascii=False)
|
|
||||||
|
|
||||||
if extract and extract != "":
|
|
||||||
ex = Path(extract)
|
|
||||||
if not ex.exists():
|
|
||||||
ex.mkdir()
|
|
||||||
|
|
||||||
for lang in tree:
|
|
||||||
for folder in tree[lang]:
|
|
||||||
if not (ex / folder).exists():
|
|
||||||
(ex / folder).mkdir()
|
|
||||||
with open(ex / folder / lang, "w") as f:
|
|
||||||
json.dump(tree[lang][folder], f, indent=4, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
|
|
||||||
parser.add_argument("--source", help="source \"lang/\" folder.")
|
|
||||||
parser.add_argument("--template", help="template file. see template.json for an example of how it works.")
|
|
||||||
parser.add_argument("--output", help="output directory for new files.")
|
|
||||||
parser.add_argument("--extract", help="put copies of original files with strings removed in this directory")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
source = Path(args.source)
|
|
||||||
|
|
||||||
tree = generateTree(source)
|
|
||||||
|
|
||||||
generate(Path(args.template), source, Path(args.output), args.extract, tree)
|
|
||||||
|
|
||||||
# print(json.dumps(tree, sort_keys=True, indent=4))
|
|
@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"meta": {},
|
|
||||||
"strings": {
|
|
||||||
"username": "common",
|
|
||||||
"password": "common",
|
|
||||||
"emailAddress": "common",
|
|
||||||
"name": "common",
|
|
||||||
"submit": "common",
|
|
||||||
"send": "common",
|
|
||||||
"success": "common",
|
|
||||||
"continue": "common",
|
|
||||||
"error": "common",
|
|
||||||
"copy": "common",
|
|
||||||
"copied": "common",
|
|
||||||
"time24h": "common",
|
|
||||||
"time12h": "common",
|
|
||||||
"linkTelegram": "common",
|
|
||||||
"contactEmail": "common",
|
|
||||||
"contactTelegram": "common",
|
|
||||||
"linkDiscord": "common",
|
|
||||||
"linkMatrix": "common",
|
|
||||||
"contactDiscord": "common",
|
|
||||||
"theme": "common",
|
|
||||||
"refresh": "common",
|
|
||||||
"required": "common",
|
|
||||||
"login": "common",
|
|
||||||
"logout": "common",
|
|
||||||
"admin": "common",
|
|
||||||
"enabled": "common",
|
|
||||||
"disabled": "common",
|
|
||||||
"reEnable": "common",
|
|
||||||
"disable": "common"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorLoginBlank": "common",
|
|
||||||
"errorConnection": "common",
|
|
||||||
"errorUnknown": "common",
|
|
||||||
"error401Unauthorized": "common",
|
|
||||||
"errorSaveSettings": "admin"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"meta": {
|
|
||||||
"explanation": "values here can either be folder, folder:section, or folder:section:subkey, and then either nothing, or /keyname. It all depends on whether the sections and keys match up, or if you want to pull a plural/singular only or not."
|
|
||||||
},
|
|
||||||
"strings": {
|
|
||||||
"inviteInfiniteUsesWarning": "admin",
|
|
||||||
"emailAddress": "form:strings/emailAddress",
|
|
||||||
"modifySettingsFor": "admin:quantityStrings:plural/",
|
|
||||||
"deleteNUsers": "admin:quantityStrings:singular/deleteNUsers"
|
|
||||||
},
|
|
||||||
"quantityStrings": {
|
|
||||||
"reEnableUsers": "admin"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
3
setup.go
3
setup.go
@ -111,8 +111,7 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
st.lang.Common.patchCommonStrings(&lang.Strings, index)
|
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||||
st.lang.Common.patchCommonNotifications(&lang.Notifications, index)
|
|
||||||
if fname != "en-us.json" {
|
if fname != "en-us.json" {
|
||||||
if lang.Meta.Fallback != "" {
|
if lang.Meta.Fallback != "" {
|
||||||
fallback, ok := st.lang.Setup[lang.Meta.Fallback]
|
fallback, ok := st.lang.Setup[lang.Meta.Fallback]
|
||||||
|
333
storage.go
333
storage.go
@ -15,189 +15,24 @@ import (
|
|||||||
"github.com/steambap/captcha"
|
"github.com/steambap/captcha"
|
||||||
)
|
)
|
||||||
|
|
||||||
type discordStore map[string]DiscordUser
|
|
||||||
type telegramStore map[string]TelegramUser
|
|
||||||
type matrixStore map[string]MatrixUser
|
|
||||||
type emailStore map[string]EmailAddress
|
|
||||||
|
|
||||||
type Storage struct {
|
type Storage struct {
|
||||||
timePattern string
|
timePattern string
|
||||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path, announcements_path, matrix_sql_path, userPage_path string
|
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path, announcements_path, matrix_sql_path string
|
||||||
users map[string]time.Time // Map of Jellyfin User IDs to their expiry times.
|
users map[string]time.Time // Map of Jellyfin User IDs to their expiry times.
|
||||||
invites Invites
|
invites Invites
|
||||||
profiles map[string]Profile
|
profiles map[string]Profile
|
||||||
defaultProfile string
|
defaultProfile string
|
||||||
displayprefs, ombi_template map[string]interface{}
|
displayprefs, ombi_template map[string]interface{}
|
||||||
emails emailStore
|
emails map[string]EmailAddress
|
||||||
telegram telegramStore // Map of Jellyfin User IDs to telegram users.
|
telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
|
||||||
discord discordStore // Map of Jellyfin user IDs to discord users.
|
discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users.
|
||||||
matrix matrixStore // Map of Jellyfin user IDs to Matrix users.
|
matrix map[string]MatrixUser // Map of Jellyfin user IDs to Matrix users.
|
||||||
customEmails customEmails
|
customEmails customEmails
|
||||||
userPage userPageContent
|
|
||||||
policy mediabrowser.Policy
|
policy mediabrowser.Policy
|
||||||
configuration mediabrowser.Configuration
|
configuration mediabrowser.Configuration
|
||||||
lang Lang
|
lang Lang
|
||||||
announcements map[string]announcementTemplate
|
announcements map[string]announcementTemplate
|
||||||
invitesLock, usersLock, discordLock, telegramLock, matrixLock, emailsLock sync.Mutex
|
invitesLock, usersLock sync.Mutex
|
||||||
}
|
|
||||||
|
|
||||||
// GetEmails returns a copy of the store.
|
|
||||||
func (st *Storage) GetEmails() emailStore {
|
|
||||||
return st.emails
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEmailsKey returns the value stored in the store's key.
|
|
||||||
func (st *Storage) GetEmailsKey(k string) (EmailAddress, bool) {
|
|
||||||
v, ok := st.emails[k]
|
|
||||||
return v, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetEmailsKey stores value v in key k.
|
|
||||||
func (st *Storage) SetEmailsKey(k string, v EmailAddress) {
|
|
||||||
st.emailsLock.Lock()
|
|
||||||
st.emails[k] = v
|
|
||||||
st.storeEmails()
|
|
||||||
st.emailsLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteEmailKey deletes value at key k.
|
|
||||||
func (st *Storage) DeleteEmailsKey(k string) {
|
|
||||||
st.emailsLock.Lock()
|
|
||||||
delete(st.emails, k)
|
|
||||||
st.storeEmails()
|
|
||||||
st.emailsLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDiscord returns a copy of the store.
|
|
||||||
func (st *Storage) GetDiscord() discordStore {
|
|
||||||
if st.discord == nil {
|
|
||||||
st.discord = discordStore{}
|
|
||||||
}
|
|
||||||
return st.discord
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDiscordKey returns the value stored in the store's key.
|
|
||||||
func (st *Storage) GetDiscordKey(k string) (DiscordUser, bool) {
|
|
||||||
v, ok := st.discord[k]
|
|
||||||
return v, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDiscordKey stores value v in key k.
|
|
||||||
func (st *Storage) SetDiscordKey(k string, v DiscordUser) {
|
|
||||||
st.discordLock.Lock()
|
|
||||||
if st.discord == nil {
|
|
||||||
st.discord = discordStore{}
|
|
||||||
}
|
|
||||||
st.discord[k] = v
|
|
||||||
st.storeDiscordUsers()
|
|
||||||
st.discordLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteDiscordKey deletes value at key k.
|
|
||||||
func (st *Storage) DeleteDiscordKey(k string) {
|
|
||||||
st.discordLock.Lock()
|
|
||||||
delete(st.discord, k)
|
|
||||||
st.storeDiscordUsers()
|
|
||||||
st.discordLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTelegram returns a copy of the store.
|
|
||||||
func (st *Storage) GetTelegram() telegramStore {
|
|
||||||
if st.telegram == nil {
|
|
||||||
st.telegram = telegramStore{}
|
|
||||||
}
|
|
||||||
return st.telegram
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTelegramKey returns the value stored in the store's key.
|
|
||||||
func (st *Storage) GetTelegramKey(k string) (TelegramUser, bool) {
|
|
||||||
v, ok := st.telegram[k]
|
|
||||||
return v, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetTelegramKey stores value v in key k.
|
|
||||||
func (st *Storage) SetTelegramKey(k string, v TelegramUser) {
|
|
||||||
st.telegramLock.Lock()
|
|
||||||
if st.telegram == nil {
|
|
||||||
st.telegram = telegramStore{}
|
|
||||||
}
|
|
||||||
st.telegram[k] = v
|
|
||||||
st.storeTelegramUsers()
|
|
||||||
st.telegramLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteTelegramKey deletes value at key k.
|
|
||||||
func (st *Storage) DeleteTelegramKey(k string) {
|
|
||||||
st.telegramLock.Lock()
|
|
||||||
delete(st.telegram, k)
|
|
||||||
st.storeTelegramUsers()
|
|
||||||
st.telegramLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMatrix returns a copy of the store.
|
|
||||||
func (st *Storage) GetMatrix() matrixStore {
|
|
||||||
if st.matrix == nil {
|
|
||||||
st.matrix = matrixStore{}
|
|
||||||
}
|
|
||||||
return st.matrix
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMatrixKey returns the value stored in the store's key.
|
|
||||||
func (st *Storage) GetMatrixKey(k string) (MatrixUser, bool) {
|
|
||||||
v, ok := st.matrix[k]
|
|
||||||
return v, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMatrixKey stores value v in key k.
|
|
||||||
func (st *Storage) SetMatrixKey(k string, v MatrixUser) {
|
|
||||||
st.matrixLock.Lock()
|
|
||||||
if st.matrix == nil {
|
|
||||||
st.matrix = matrixStore{}
|
|
||||||
}
|
|
||||||
st.matrix[k] = v
|
|
||||||
st.storeMatrixUsers()
|
|
||||||
st.matrixLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteMatrixKey deletes value at key k.
|
|
||||||
func (st *Storage) DeleteMatrixKey(k string) {
|
|
||||||
st.matrixLock.Lock()
|
|
||||||
delete(st.matrix, k)
|
|
||||||
st.storeMatrixUsers()
|
|
||||||
st.matrixLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInvites returns a copy of the store.
|
|
||||||
func (st *Storage) GetInvites() Invites {
|
|
||||||
if st.invites == nil {
|
|
||||||
st.invites = Invites{}
|
|
||||||
}
|
|
||||||
return st.invites
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInvitesKey returns the value stored in the store's key.
|
|
||||||
func (st *Storage) GetInvitesKey(k string) (Invite, bool) {
|
|
||||||
v, ok := st.invites[k]
|
|
||||||
return v, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetInvitesKey stores value v in key k.
|
|
||||||
func (st *Storage) SetInvitesKey(k string, v Invite) {
|
|
||||||
st.invitesLock.Lock()
|
|
||||||
if st.invites == nil {
|
|
||||||
st.invites = Invites{}
|
|
||||||
}
|
|
||||||
st.invites[k] = v
|
|
||||||
st.storeInvites()
|
|
||||||
st.invitesLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteInvitesKey deletes value at key k.
|
|
||||||
func (st *Storage) DeleteInvitesKey(k string) {
|
|
||||||
st.invitesLock.Lock()
|
|
||||||
delete(st.invites, k)
|
|
||||||
st.storeInvites()
|
|
||||||
st.invitesLock.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TelegramUser struct {
|
type TelegramUser struct {
|
||||||
@ -214,7 +49,6 @@ type DiscordUser struct {
|
|||||||
Discriminator string
|
Discriminator string
|
||||||
Lang string
|
Lang string
|
||||||
Contact bool
|
Contact bool
|
||||||
JellyfinID string `json:"-"` // Used internally in discord.go
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmailAddress struct {
|
type EmailAddress struct {
|
||||||
@ -225,30 +59,25 @@ type EmailAddress struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type customEmails struct {
|
type customEmails struct {
|
||||||
UserCreated customContent `json:"userCreated"`
|
UserCreated customEmail `json:"userCreated"`
|
||||||
InviteExpiry customContent `json:"inviteExpiry"`
|
InviteExpiry customEmail `json:"inviteExpiry"`
|
||||||
PasswordReset customContent `json:"passwordReset"`
|
PasswordReset customEmail `json:"passwordReset"`
|
||||||
UserDeleted customContent `json:"userDeleted"`
|
UserDeleted customEmail `json:"userDeleted"`
|
||||||
UserDisabled customContent `json:"userDisabled"`
|
UserDisabled customEmail `json:"userDisabled"`
|
||||||
UserEnabled customContent `json:"userEnabled"`
|
UserEnabled customEmail `json:"userEnabled"`
|
||||||
InviteEmail customContent `json:"inviteEmail"`
|
InviteEmail customEmail `json:"inviteEmail"`
|
||||||
WelcomeEmail customContent `json:"welcomeEmail"`
|
WelcomeEmail customEmail `json:"welcomeEmail"`
|
||||||
EmailConfirmation customContent `json:"emailConfirmation"`
|
EmailConfirmation customEmail `json:"emailConfirmation"`
|
||||||
UserExpired customContent `json:"userExpired"`
|
UserExpired customEmail `json:"userExpired"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type customContent struct {
|
type customEmail struct {
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Variables []string `json:"variables,omitempty"`
|
Variables []string `json:"variables,omitempty"`
|
||||||
Conditionals []string `json:"conditionals,omitempty"`
|
Conditionals []string `json:"conditionals,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type userPageContent struct {
|
|
||||||
Login customContent `json:"login"`
|
|
||||||
Page customContent `json:"page"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// timePattern: %Y-%m-%dT%H:%M:%S.%f
|
// timePattern: %Y-%m-%dT%H:%M:%S.%f
|
||||||
|
|
||||||
type Profile struct {
|
type Profile struct {
|
||||||
@ -278,6 +107,7 @@ type Invite struct {
|
|||||||
Notify map[string]map[string]bool `json:"notify"`
|
Notify map[string]map[string]bool `json:"notify"`
|
||||||
Profile string `json:"profile"`
|
Profile string `json:"profile"`
|
||||||
Label string `json:"label,omitempty"`
|
Label string `json:"label,omitempty"`
|
||||||
|
Keys []string `json:"keys,omitempty"`
|
||||||
Captchas map[string]*captcha.Data // Map of Captcha IDs to answers
|
Captchas map[string]*captcha.Data // Map of Captcha IDs to answers
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,9 +116,9 @@ type Lang struct {
|
|||||||
chosenAdminLang string
|
chosenAdminLang string
|
||||||
Admin adminLangs
|
Admin adminLangs
|
||||||
AdminJSON map[string]string
|
AdminJSON map[string]string
|
||||||
UserPath string
|
FormPath string
|
||||||
chosenUserLang string
|
chosenFormLang string
|
||||||
User userLangs
|
Form formLangs
|
||||||
PasswordResetPath string
|
PasswordResetPath string
|
||||||
chosenPWRLang string
|
chosenPWRLang string
|
||||||
PasswordReset pwrLangs
|
PasswordReset pwrLangs
|
||||||
@ -314,11 +144,7 @@ func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = st.loadLangEmail(filesystems...)
|
err = st.loadLangForm(filesystems...)
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = st.loadLangUser(filesystems...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -326,6 +152,10 @@ func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
err = st.loadLangEmail(filesystems...)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
err = st.loadLangTelegram(filesystems...)
|
err = st.loadLangTelegram(filesystems...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -334,7 +164,7 @@ func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
|
|||||||
// from a list of other sources in a preferred order.
|
// from a list of other sources in a preferred order.
|
||||||
// languages to patch from should be in decreasing priority,
|
// languages to patch from should be in decreasing priority,
|
||||||
// E.g: If to = fr-be, from = [fr-fr, en-us].
|
// E.g: If to = fr-be, from = [fr-fr, en-us].
|
||||||
func (common *commonLangs) patchCommonStrings(to *langSection, from ...string) {
|
func (common *commonLangs) patchCommon(to *langSection, from ...string) {
|
||||||
if *to == nil {
|
if *to == nil {
|
||||||
*to = langSection{}
|
*to = langSection{}
|
||||||
}
|
}
|
||||||
@ -353,44 +183,6 @@ func (common *commonLangs) patchCommonStrings(to *langSection, from ...string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (common *commonLangs) patchCommonNotifications(to *langSection, from ...string) {
|
|
||||||
if *to == nil {
|
|
||||||
*to = langSection{}
|
|
||||||
}
|
|
||||||
for n, ev := range (*common)[from[len(from)-1]].Notifications {
|
|
||||||
if v, ok := (*to)[n]; !ok || v == "" {
|
|
||||||
i := 0
|
|
||||||
for i < len(from)-1 {
|
|
||||||
ev, ok = (*common)[from[i]].Notifications[n]
|
|
||||||
if ok && ev != "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
(*to)[n] = ev
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (common *commonLangs) patchCommonQuantityStrings(to *map[string]quantityString, from ...string) {
|
|
||||||
if *to == nil {
|
|
||||||
*to = map[string]quantityString{}
|
|
||||||
}
|
|
||||||
for n, ev := range (*common)[from[len(from)-1]].QuantityStrings {
|
|
||||||
if v, ok := (*to)[n]; !ok || (v.Singular == "" && v.Plural == "") {
|
|
||||||
i := 0
|
|
||||||
for i < len(from)-1 {
|
|
||||||
ev, ok = (*common)[from[i]].QuantityStrings[n]
|
|
||||||
if ok && ev.Singular != "" && ev.Plural != "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
(*to)[n] = ev
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func patchLang(to *langSection, from ...*langSection) {
|
func patchLang(to *langSection, from ...*langSection) {
|
||||||
if *to == nil {
|
if *to == nil {
|
||||||
*to = langSection{}
|
*to = langSection{}
|
||||||
@ -472,14 +264,10 @@ func (st *Storage) loadLangCommon(filesystems ...fs.FS) error {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
||||||
patchLang(&lang.Strings, &fallback.Strings, &english.Strings)
|
patchLang(&lang.Strings, &fallback.Strings, &english.Strings)
|
||||||
patchLang(&lang.Notifications, &fallback.Notifications, &english.Notifications)
|
|
||||||
patchQuantityStrings(&lang.QuantityStrings, &fallback.QuantityStrings, &english.QuantityStrings)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lang.Meta.Fallback != "" && err != nil) || lang.Meta.Fallback == "" {
|
if (lang.Meta.Fallback != "" && err != nil) || lang.Meta.Fallback == "" {
|
||||||
patchLang(&lang.Strings, &english.Strings)
|
patchLang(&lang.Strings, &english.Strings)
|
||||||
patchLang(&lang.Notifications, &english.Notifications)
|
|
||||||
patchQuantityStrings(&lang.QuantityStrings, &english.QuantityStrings)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
st.lang.Common[index] = lang
|
st.lang.Common[index] = lang
|
||||||
@ -541,8 +329,7 @@ func (st *Storage) loadLangAdmin(filesystems ...fs.FS) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
st.lang.Common.patchCommonStrings(&lang.Strings, index)
|
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||||
st.lang.Common.patchCommonNotifications(&lang.Notifications, index)
|
|
||||||
if fname != "en-us.json" {
|
if fname != "en-us.json" {
|
||||||
if lang.Meta.Fallback != "" {
|
if lang.Meta.Fallback != "" {
|
||||||
fallback, ok := st.lang.Admin[lang.Meta.Fallback]
|
fallback, ok := st.lang.Admin[lang.Meta.Fallback]
|
||||||
@ -608,16 +395,16 @@ func (st *Storage) loadLangAdmin(filesystems ...fs.FS) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (st *Storage) loadLangUser(filesystems ...fs.FS) error {
|
func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
|
||||||
st.lang.User = map[string]userLang{}
|
st.lang.Form = map[string]formLang{}
|
||||||
var english userLang
|
var english formLang
|
||||||
loadedLangs := make([]map[string]bool, len(filesystems))
|
loadedLangs := make([]map[string]bool, len(filesystems))
|
||||||
var load loadLangFunc
|
var load loadLangFunc
|
||||||
load = func(fsIndex int, fname string) error {
|
load = func(fsIndex int, fname string) error {
|
||||||
filesystem := filesystems[fsIndex]
|
filesystem := filesystems[fsIndex]
|
||||||
index := strings.TrimSuffix(fname, filepath.Ext(fname))
|
index := strings.TrimSuffix(fname, filepath.Ext(fname))
|
||||||
lang := userLang{}
|
lang := formLang{}
|
||||||
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.UserPath, fname))
|
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.FormPath, fname))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -628,21 +415,14 @@ func (st *Storage) loadLangUser(filesystems ...fs.FS) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
st.lang.Common.patchCommonStrings(&lang.Strings, index)
|
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||||
st.lang.Common.patchCommonNotifications(&lang.Notifications, index)
|
|
||||||
st.lang.Common.patchCommonQuantityStrings(&lang.QuantityStrings, index)
|
|
||||||
// turns out, a lot of email strings are useful on the user page.
|
|
||||||
emailLang := []langSection{st.lang.Email[index].WelcomeEmail, st.lang.Email[index].UserDisabled, st.lang.Email[index].UserExpired}
|
|
||||||
for _, v := range emailLang {
|
|
||||||
patchLang(&lang.Strings, &v)
|
|
||||||
}
|
|
||||||
if fname != "en-us.json" {
|
if fname != "en-us.json" {
|
||||||
if lang.Meta.Fallback != "" {
|
if lang.Meta.Fallback != "" {
|
||||||
fallback, ok := st.lang.User[lang.Meta.Fallback]
|
fallback, ok := st.lang.Form[lang.Meta.Fallback]
|
||||||
err = nil
|
err = nil
|
||||||
if !ok {
|
if !ok {
|
||||||
err = load(fsIndex, lang.Meta.Fallback+".json")
|
err = load(fsIndex, lang.Meta.Fallback+".json")
|
||||||
fallback = st.lang.User[lang.Meta.Fallback]
|
fallback = st.lang.Form[lang.Meta.Fallback]
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
||||||
@ -665,14 +445,9 @@ func (st *Storage) loadLangUser(filesystems ...fs.FS) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
userJSON, err := json.Marshal(lang)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
lang.notificationsJSON = string(notifications)
|
lang.notificationsJSON = string(notifications)
|
||||||
lang.validationStringsJSON = string(validationStrings)
|
lang.validationStringsJSON = string(validationStrings)
|
||||||
lang.JSON = string(userJSON)
|
st.lang.Form[index] = lang
|
||||||
st.lang.User[index] = lang
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
engFound := false
|
engFound := false
|
||||||
@ -688,10 +463,10 @@ func (st *Storage) loadLangUser(filesystems ...fs.FS) error {
|
|||||||
if !engFound {
|
if !engFound {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
english = st.lang.User["en-us"]
|
english = st.lang.Form["en-us"]
|
||||||
userLoaded := false
|
formLoaded := false
|
||||||
for i := range filesystems {
|
for i := range filesystems {
|
||||||
files, err := fs.ReadDir(filesystems[i], st.lang.UserPath)
|
files, err := fs.ReadDir(filesystems[i], st.lang.FormPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -699,13 +474,13 @@ func (st *Storage) loadLangUser(filesystems ...fs.FS) error {
|
|||||||
if !loadedLangs[i][f.Name()] {
|
if !loadedLangs[i][f.Name()] {
|
||||||
err = load(i, f.Name())
|
err = load(i, f.Name())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
userLoaded = true
|
formLoaded = true
|
||||||
loadedLangs[i][f.Name()] = true
|
loadedLangs[i][f.Name()] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !userLoaded {
|
if !formLoaded {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -731,7 +506,7 @@ func (st *Storage) loadLangPWR(filesystems ...fs.FS) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
st.lang.Common.patchCommonStrings(&lang.Strings, index)
|
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||||
if fname != "en-us.json" {
|
if fname != "en-us.json" {
|
||||||
if lang.Meta.Fallback != "" {
|
if lang.Meta.Fallback != "" {
|
||||||
fallback, ok := st.lang.PasswordReset[lang.Meta.Fallback]
|
fallback, ok := st.lang.PasswordReset[lang.Meta.Fallback]
|
||||||
@ -765,7 +540,7 @@ func (st *Storage) loadLangPWR(filesystems ...fs.FS) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
english = st.lang.PasswordReset["en-us"]
|
english = st.lang.PasswordReset["en-us"]
|
||||||
userLoaded := false
|
formLoaded := false
|
||||||
for i := range filesystems {
|
for i := range filesystems {
|
||||||
files, err := fs.ReadDir(filesystems[i], st.lang.PasswordResetPath)
|
files, err := fs.ReadDir(filesystems[i], st.lang.PasswordResetPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -775,13 +550,13 @@ func (st *Storage) loadLangPWR(filesystems ...fs.FS) error {
|
|||||||
if !loadedLangs[i][f.Name()] {
|
if !loadedLangs[i][f.Name()] {
|
||||||
err = load(i, f.Name())
|
err = load(i, f.Name())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
userLoaded = true
|
formLoaded = true
|
||||||
loadedLangs[i][f.Name()] = true
|
loadedLangs[i][f.Name()] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !userLoaded {
|
if !formLoaded {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -807,7 +582,7 @@ func (st *Storage) loadLangEmail(filesystems ...fs.FS) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
st.lang.Common.patchCommonStrings(&lang.Strings, index)
|
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||||
if fname != "en-us.json" {
|
if fname != "en-us.json" {
|
||||||
if lang.Meta.Fallback != "" {
|
if lang.Meta.Fallback != "" {
|
||||||
fallback, ok := st.lang.Email[lang.Meta.Fallback]
|
fallback, ok := st.lang.Email[lang.Meta.Fallback]
|
||||||
@ -904,7 +679,7 @@ func (st *Storage) loadLangTelegram(filesystems ...fs.FS) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
st.lang.Common.patchCommonStrings(&lang.Strings, index)
|
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||||
if fname != "en-us.json" {
|
if fname != "en-us.json" {
|
||||||
if lang.Meta.Fallback != "" {
|
if lang.Meta.Fallback != "" {
|
||||||
fallback, ok := st.lang.Telegram[lang.Meta.Fallback]
|
fallback, ok := st.lang.Telegram[lang.Meta.Fallback]
|
||||||
@ -964,10 +739,14 @@ func (st *Storage) loadLangTelegram(filesystems ...fs.FS) error {
|
|||||||
type Invites map[string]Invite
|
type Invites map[string]Invite
|
||||||
|
|
||||||
func (st *Storage) loadInvites() error {
|
func (st *Storage) loadInvites() error {
|
||||||
|
st.invitesLock.Lock()
|
||||||
|
defer st.invitesLock.Unlock()
|
||||||
return loadJSON(st.invite_path, &st.invites)
|
return loadJSON(st.invite_path, &st.invites)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (st *Storage) storeInvites() error {
|
func (st *Storage) storeInvites() error {
|
||||||
|
st.invitesLock.Lock()
|
||||||
|
defer st.invitesLock.Unlock()
|
||||||
return storeJSON(st.invite_path, st.invites)
|
return storeJSON(st.invite_path, st.invites)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1034,14 +813,6 @@ func (st *Storage) storeCustomEmails() error {
|
|||||||
return storeJSON(st.customEmails_path, st.customEmails)
|
return storeJSON(st.customEmails_path, st.customEmails)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (st *Storage) loadUserPageContent() error {
|
|
||||||
return loadJSON(st.userPage_path, &st.userPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (st *Storage) storeUserPageContent() error {
|
|
||||||
return storeJSON(st.userPage_path, st.userPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (st *Storage) loadPolicy() error {
|
func (st *Storage) loadPolicy() error {
|
||||||
return loadJSON(st.policy_path, &st.policy)
|
return loadJSON(st.policy_path, &st.policy)
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user