mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 17:10:10 +00:00
Merge "My Account"
User Page/My Account
This commit is contained in:
commit
ef5d89f323
3
.gitignore
vendored
3
.gitignore
vendored
@ -21,3 +21,6 @@ cl.md
|
||||
mautrix/
|
||||
tempts/
|
||||
matacc.txt
|
||||
scripts/langmover/lang
|
||||
scripts/langmover/lang2
|
||||
scripts/langmover/out
|
||||
|
@ -26,11 +26,11 @@ before:
|
||||
- cp -r ts tempts
|
||||
- scripts/dark-variant.sh tempts
|
||||
- scripts/dark-variant.sh tempts/modules
|
||||
- npx esbuild --bundle tempts/admin.ts --outfile=./data/web/js/admin.js --minify
|
||||
- npx esbuild --bundle tempts/pwr.ts --outfile=./data/web/js/pwr.js --minify
|
||||
- npx esbuild --bundle tempts/form.ts --outfile=./data/web/js/form.js --minify
|
||||
- npx esbuild --bundle tempts/setup.ts --outfile=./data/web/js/setup.js --minify
|
||||
- npx esbuild --bundle tempts/crash.ts --outfile=./data/crash.js --minify
|
||||
- npx esbuild --target=es6 --format=esm --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 --target=es6 --format=esm --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 --target=es6 --format=esm --bundle tempts/crash.ts --outfile=./data/crash.js --minify
|
||||
- rm -r tempts
|
||||
- npx esbuild --bundle css/base.css --outfile=./data/web/css/bundle.css --external:remixicon.css --minify
|
||||
- cp html/crash.html data/
|
||||
|
3
Makefile
3
Makefile
@ -104,6 +104,7 @@ typescript:
|
||||
$(info compiling typescript)
|
||||
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/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/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
|
||||
@ -177,4 +178,6 @@ clean:
|
||||
-rm docs/docs.go docs/swagger.json docs/swagger.yaml
|
||||
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
|
||||
|
@ -16,7 +16,7 @@ func (app *appContext) checkInvites() {
|
||||
currentTime := time.Now()
|
||||
app.storage.loadInvites()
|
||||
changed := false
|
||||
for code, data := range app.storage.invites {
|
||||
for code, data := range app.storage.GetInvites() {
|
||||
expiry := data.ValidTill
|
||||
if !currentTime.After(expiry) {
|
||||
continue
|
||||
@ -54,7 +54,7 @@ func (app *appContext) checkInvites() {
|
||||
wait.Wait()
|
||||
}
|
||||
changed = true
|
||||
delete(app.storage.invites, code)
|
||||
app.storage.DeleteInvitesKey(code)
|
||||
}
|
||||
if changed {
|
||||
app.storage.storeInvites()
|
||||
@ -65,7 +65,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
||||
currentTime := time.Now()
|
||||
app.storage.loadInvites()
|
||||
changed := false
|
||||
inv, match := app.storage.invites[code]
|
||||
inv, match := app.storage.GetInvitesKey(code)
|
||||
if !match {
|
||||
return false
|
||||
}
|
||||
@ -105,21 +105,21 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
||||
}
|
||||
changed = true
|
||||
match = false
|
||||
delete(app.storage.invites, code)
|
||||
app.storage.DeleteInvitesKey(code)
|
||||
} else if used {
|
||||
changed = true
|
||||
del := false
|
||||
newInv := inv
|
||||
if newInv.RemainingUses == 1 {
|
||||
del = true
|
||||
delete(app.storage.invites, code)
|
||||
app.storage.DeleteInvitesKey(code)
|
||||
} else if newInv.RemainingUses != 0 {
|
||||
// 0 means infinite i guess?
|
||||
newInv.RemainingUses--
|
||||
}
|
||||
newInv.UsedBy = append(newInv.UsedBy, []string{username, strconv.FormatInt(currentTime.Unix(), 10)})
|
||||
if !del {
|
||||
app.storage.invites[code] = newInv
|
||||
app.storage.SetInvitesKey(code, newInv)
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
@ -219,7 +219,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
invite.Profile = "Default"
|
||||
}
|
||||
}
|
||||
app.storage.invites[inviteCode] = invite
|
||||
app.storage.SetInvitesKey(inviteCode, invite)
|
||||
app.storage.storeInvites()
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
@ -236,7 +236,7 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
app.storage.loadInvites()
|
||||
app.checkInvites()
|
||||
var invites []inviteDTO
|
||||
for code, inv := range app.storage.invites {
|
||||
for code, inv := range app.storage.GetInvites() {
|
||||
_, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
|
||||
invite := inviteDTO{
|
||||
Code: code,
|
||||
@ -280,7 +280,7 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
var address string
|
||||
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||
app.storage.loadEmails()
|
||||
if addr, ok := app.storage.emails[gc.GetString("jfId")]; ok && addr.Addr != "" {
|
||||
if addr, ok := app.storage.GetEmailsKey(gc.GetString("jfId")); ok && addr.Addr != "" {
|
||||
address = addr.Addr
|
||||
}
|
||||
} else {
|
||||
@ -335,9 +335,9 @@ func (app *appContext) SetProfile(gc *gin.Context) {
|
||||
respond(500, "Profile not found", gc)
|
||||
return
|
||||
}
|
||||
inv := app.storage.invites[req.Invite]
|
||||
inv, _ := app.storage.GetInvitesKey(req.Invite)
|
||||
inv.Profile = req.Profile
|
||||
app.storage.invites[req.Invite] = inv
|
||||
app.storage.SetInvitesKey(req.Invite, inv)
|
||||
app.storage.storeInvites()
|
||||
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.storage.loadInvites()
|
||||
app.storage.loadEmails()
|
||||
invite, ok := app.storage.invites[code]
|
||||
invite, ok := app.storage.GetInvitesKey(code)
|
||||
if !ok {
|
||||
app.err.Printf("%s Notification setting change failed: Invalid code", code)
|
||||
respond(400, "Invalid invite code", gc)
|
||||
@ -398,7 +398,7 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
app.storage.invites[code] = invite
|
||||
app.storage.SetInvitesKey(code, invite)
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
@ -419,9 +419,9 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
|
||||
gc.BindJSON(&req)
|
||||
app.debug.Printf("%s: Deletion requested", req.Code)
|
||||
var ok bool
|
||||
_, ok = app.storage.invites[req.Code]
|
||||
_, ok = app.storage.GetInvitesKey(req.Code)
|
||||
if ok {
|
||||
delete(app.storage.invites, req.Code)
|
||||
app.storage.DeleteInvitesKey(req.Code)
|
||||
app.storage.storeInvites()
|
||||
app.info.Printf("%s: Invite deleted", req.Code)
|
||||
respondBool(200, true, gc)
|
||||
|
226
api-messages.go
226
api-messages.go
@ -15,12 +15,16 @@ import (
|
||||
// @Router /config/emails [get]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) GetCustomEmails(gc *gin.Context) {
|
||||
func (app *appContext) GetCustomContent(gc *gin.Context) {
|
||||
lang := gc.Query("lang")
|
||||
if _, ok := app.storage.lang.Email[lang]; !ok {
|
||||
lang = app.storage.lang.chosenEmailLang
|
||||
}
|
||||
gc.JSON(200, emailListDTO{
|
||||
adminLang := lang
|
||||
if _, ok := app.storage.lang.Admin[lang]; !ok {
|
||||
adminLang = app.storage.lang.chosenAdminLang
|
||||
}
|
||||
list := emailListDTO{
|
||||
"UserCreated": {Name: app.storage.lang.Email[lang].UserCreated["name"], Enabled: app.storage.customEmails.UserCreated.Enabled},
|
||||
"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},
|
||||
@ -31,13 +35,25 @@ func (app *appContext) GetCustomEmails(gc *gin.Context) {
|
||||
"WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.customEmails.WelcomeEmail.Enabled},
|
||||
"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},
|
||||
})
|
||||
"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")
|
||||
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) getCustomEmail(id string) *customEmail {
|
||||
func (app *appContext) getCustomMessage(id string) *customContent {
|
||||
switch id {
|
||||
case "Announcement":
|
||||
return &customEmail{}
|
||||
return &customContent{}
|
||||
case "UserCreated":
|
||||
return &app.storage.customEmails.UserCreated
|
||||
case "InviteExpiry":
|
||||
@ -58,13 +74,17 @@ func (app *appContext) getCustomEmail(id string) *customEmail {
|
||||
return &app.storage.customEmails.EmailConfirmation
|
||||
case "UserExpired":
|
||||
return &app.storage.customEmails.UserExpired
|
||||
case "UserLogin":
|
||||
return &app.storage.userPage.Login
|
||||
case "UserPage":
|
||||
return &app.storage.userPage.Page
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// @Summary Sets the corresponding custom email.
|
||||
// @Produce json
|
||||
// @Param customEmail body customEmail true "Content = email (in markdown)."
|
||||
// @Param customEmails body customEmails true "Content = email (in markdown)."
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
@ -72,25 +92,29 @@ func (app *appContext) getCustomEmail(id string) *customEmail {
|
||||
// @Router /config/emails/{id} [post]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) SetCustomEmail(gc *gin.Context) {
|
||||
var req customEmail
|
||||
func (app *appContext) SetCustomMessage(gc *gin.Context) {
|
||||
var req customContent
|
||||
gc.BindJSON(&req)
|
||||
id := gc.Param("id")
|
||||
if req.Content == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
email := app.getCustomEmail(id)
|
||||
if email == nil {
|
||||
message := app.getCustomMessage(id)
|
||||
if message == nil {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
email.Content = req.Content
|
||||
email.Enabled = true
|
||||
message.Content = req.Content
|
||||
message.Enabled = true
|
||||
if app.storage.storeCustomEmails() != nil {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
if app.storage.storeUserPageContent() != nil {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
@ -104,7 +128,7 @@ func (app *appContext) SetCustomEmail(gc *gin.Context) {
|
||||
// @Router /config/emails/{id}/state/{enable/disable} [post]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) SetCustomEmailState(gc *gin.Context) {
|
||||
func (app *appContext) SetCustomMessageState(gc *gin.Context) {
|
||||
id := gc.Param("id")
|
||||
s := gc.Param("state")
|
||||
enabled := false
|
||||
@ -113,20 +137,24 @@ func (app *appContext) SetCustomEmailState(gc *gin.Context) {
|
||||
} else if s != "disable" {
|
||||
respondBool(400, false, gc)
|
||||
}
|
||||
email := app.getCustomEmail(id)
|
||||
if email == nil {
|
||||
message := app.getCustomMessage(id)
|
||||
if message == nil {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
email.Enabled = enabled
|
||||
message.Enabled = enabled
|
||||
if app.storage.storeCustomEmails() != nil {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
if app.storage.storeUserPageContent() != nil {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns the custom email (generating it if not set) and list of used variables in it.
|
||||
// @Summary Returns the custom email/message (generating it if not set) and list of used variables in it.
|
||||
// @Produce json
|
||||
// @Success 200 {object} customEmailDTO
|
||||
// @Failure 400 {object} boolResponse
|
||||
@ -135,7 +163,7 @@ func (app *appContext) SetCustomEmailState(gc *gin.Context) {
|
||||
// @Router /config/emails/{id} [get]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
|
||||
func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
|
||||
lang := app.storage.lang.chosenEmailLang
|
||||
id := gc.Param("id")
|
||||
var content string
|
||||
@ -146,20 +174,26 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
|
||||
var values map[string]interface{}
|
||||
username := app.storage.lang.Email[lang].Strings.get("username")
|
||||
emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress")
|
||||
email := app.getCustomEmail(id)
|
||||
if email == nil {
|
||||
app.err.Printf("Failed to get custom email with ID \"%s\"", id)
|
||||
customMessage := app.getCustomMessage(id)
|
||||
if customMessage == nil {
|
||||
app.err.Printf("Failed to get custom message with ID \"%s\"", id)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
if id == "WelcomeEmail" {
|
||||
conditionals = []string{"{yourAccountWillExpire}"}
|
||||
email.Conditionals = conditionals
|
||||
customMessage.Conditionals = conditionals
|
||||
} else if id == "UserPage" {
|
||||
variables = []string{"{username}"}
|
||||
customMessage.Variables = variables
|
||||
} else if id == "UserLogin" {
|
||||
variables = []string{}
|
||||
customMessage.Variables = variables
|
||||
}
|
||||
content = email.Content
|
||||
content = customMessage.Content
|
||||
noContent := content == ""
|
||||
if !noContent {
|
||||
variables = email.Variables
|
||||
variables = customMessage.Variables
|
||||
}
|
||||
switch id {
|
||||
case "Announcement":
|
||||
@ -215,12 +249,14 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
|
||||
msg, err = app.email.constructUserExpired(app, true)
|
||||
}
|
||||
values = app.email.userExpiredValues(app, false)
|
||||
case "UserLogin", "UserPage":
|
||||
values = map[string]interface{}{}
|
||||
}
|
||||
if err != nil {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
if noContent && id != "Announcement" {
|
||||
if noContent && id != "Announcement" && id != "UserPage" && id != "UserLogin" {
|
||||
content = msg.Text
|
||||
variables = make([]string, strings.Count(content, "{"))
|
||||
i := 0
|
||||
@ -239,7 +275,7 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
|
||||
i++
|
||||
}
|
||||
}
|
||||
email.Variables = variables
|
||||
customMessage.Variables = variables
|
||||
}
|
||||
if variables == nil {
|
||||
variables = []string{}
|
||||
@ -248,11 +284,22 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
mail, err := app.email.constructTemplate("", "<div class=\"preview-content\"></div>", app)
|
||||
if app.storage.storeUserPageContent() != nil {
|
||||
respondBool(500, false, gc)
|
||||
}
|
||||
var mail *Message
|
||||
if id != "UserLogin" && id != "UserPage" {
|
||||
mail, err = app.email.constructTemplate("", "<div class=\"preview-content\"></div>", app)
|
||||
if err != nil {
|
||||
respondBool(500, false, gc)
|
||||
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})
|
||||
}
|
||||
|
||||
@ -285,18 +332,12 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
tokenIndex := -1
|
||||
for i, v := range app.telegram.verifiedTokens {
|
||||
if v.Token == req.Token {
|
||||
tokenIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if tokenIndex == -1 {
|
||||
tgToken, ok := app.telegram.TokenVerified(req.Token)
|
||||
app.telegram.DeleteVerifiedToken(req.Token)
|
||||
if !ok {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
tgToken := app.telegram.verifiedTokens[tokenIndex]
|
||||
tgUser := TelegramUser{
|
||||
ChatID: tgToken.ChatID,
|
||||
Username: tgToken.Username,
|
||||
@ -305,17 +346,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
|
||||
if lang, ok := app.telegram.languages[tgToken.ChatID]; ok {
|
||||
tgUser.Lang = lang
|
||||
}
|
||||
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]
|
||||
}
|
||||
app.storage.SetTelegramKey(req.ID, tgUser)
|
||||
linkExistingOmbiDiscordTelegram(app)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
@ -336,15 +367,14 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
if tgUser, ok := app.storage.telegram[req.ID]; ok {
|
||||
app.setContactMethods(req, gc)
|
||||
}
|
||||
|
||||
func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Context) {
|
||||
if tgUser, ok := app.storage.GetTelegramKey(req.ID); ok {
|
||||
change := tgUser.Contact != req.Telegram
|
||||
tgUser.Contact = req.Telegram
|
||||
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
|
||||
}
|
||||
app.storage.SetTelegramKey(req.ID, tgUser)
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Telegram {
|
||||
@ -353,10 +383,10 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
|
||||
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
|
||||
}
|
||||
}
|
||||
if dcUser, ok := app.storage.discord[req.ID]; ok {
|
||||
if dcUser, ok := app.storage.GetDiscordKey(req.ID); ok {
|
||||
change := dcUser.Contact != req.Discord
|
||||
dcUser.Contact = req.Discord
|
||||
app.storage.discord[req.ID] = dcUser
|
||||
app.storage.SetDiscordKey(req.ID, dcUser)
|
||||
if err := app.storage.storeDiscordUsers(); err != nil {
|
||||
respondBool(500, false, gc)
|
||||
app.err.Printf("Discord: Failed to store users: %v", err)
|
||||
@ -370,10 +400,10 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
|
||||
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
|
||||
}
|
||||
}
|
||||
if mxUser, ok := app.storage.matrix[req.ID]; ok {
|
||||
if mxUser, ok := app.storage.GetMatrixKey(req.ID); ok {
|
||||
change := mxUser.Contact != req.Matrix
|
||||
mxUser.Contact = req.Matrix
|
||||
app.storage.matrix[req.ID] = mxUser
|
||||
app.storage.SetMatrixKey(req.ID, mxUser)
|
||||
if err := app.storage.storeMatrixUsers(); err != nil {
|
||||
respondBool(500, false, gc)
|
||||
app.err.Printf("Matrix: Failed to store users: %v", err)
|
||||
@ -387,10 +417,10 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
|
||||
app.debug.Printf("Matrix: User \"%s\" will%s be notified through Matrix.", mxUser.UserID, msg)
|
||||
}
|
||||
}
|
||||
if email, ok := app.storage.emails[req.ID]; ok {
|
||||
if email, ok := app.storage.GetEmailsKey(req.ID); ok {
|
||||
change := email.Contact != req.Email
|
||||
email.Contact = req.Email
|
||||
app.storage.emails[req.ID] = email
|
||||
app.storage.SetEmailsKey(req.ID, email)
|
||||
if err := app.storage.storeEmails(); err != nil {
|
||||
respondBool(500, false, gc)
|
||||
app.err.Printf("Failed to store emails: %v", err)
|
||||
@ -416,19 +446,8 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
|
||||
// @tags Other
|
||||
func (app *appContext) TelegramVerified(gc *gin.Context) {
|
||||
pin := gc.Param("pin")
|
||||
tokenIndex := -1
|
||||
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)
|
||||
_, ok := app.telegram.TokenVerified(pin)
|
||||
respondBool(200, ok, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires invite code.
|
||||
@ -441,32 +460,18 @@ func (app *appContext) TelegramVerified(gc *gin.Context) {
|
||||
// @tags Other
|
||||
func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.invites[code]; !ok {
|
||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
pin := gc.Param("pin")
|
||||
tokenIndex := -1
|
||||
for i, v := range app.telegram.verifiedTokens {
|
||||
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 {
|
||||
token, ok := app.telegram.TokenVerified(pin)
|
||||
if ok && app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(token.Username) {
|
||||
app.discord.DeleteVerifiedUser(pin)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
respondBool(200, ok, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code.
|
||||
@ -479,21 +484,17 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
|
||||
// @tags Other
|
||||
func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.invites[code]; !ok {
|
||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
pin := gc.Param("pin")
|
||||
_, ok := app.discord.verifiedTokens[pin]
|
||||
if app.config.Section("discord").Key("require_unique").MustBool(false) {
|
||||
for _, u := range app.storage.discord {
|
||||
if app.discord.verifiedTokens[pin].ID == u.ID {
|
||||
user, ok := app.discord.UserVerified(pin)
|
||||
if ok && app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(user.ID) {
|
||||
delete(app.discord.verifiedTokens, pin)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
respondBool(200, ok, gc)
|
||||
}
|
||||
|
||||
@ -512,7 +513,7 @@ func (app *appContext) DiscordServerInvite(gc *gin.Context) {
|
||||
return
|
||||
}
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.invites[code]; !ok {
|
||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
@ -536,7 +537,7 @@ func (app *appContext) DiscordServerInvite(gc *gin.Context) {
|
||||
// @tags Other
|
||||
func (app *appContext) MatrixSendPIN(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.invites[code]; !ok {
|
||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
@ -547,7 +548,7 @@ func (app *appContext) MatrixSendPIN(gc *gin.Context) {
|
||||
return
|
||||
}
|
||||
if app.config.Section("matrix").Key("require_unique").MustBool(false) {
|
||||
for _, u := range app.storage.matrix {
|
||||
for _, u := range app.storage.GetMatrix() {
|
||||
if req.UserID == u.UserID {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
@ -574,7 +575,7 @@ func (app *appContext) MatrixSendPIN(gc *gin.Context) {
|
||||
// @tags Other
|
||||
func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.invites[code]; !ok {
|
||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
||||
app.debug.Println("Matrix: Invite code was invalid")
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
@ -644,8 +645,8 @@ func (app *appContext) MatrixLogin(gc *gin.Context) {
|
||||
func (app *appContext) MatrixConnect(gc *gin.Context) {
|
||||
var req MatrixConnectUserDTO
|
||||
gc.BindJSON(&req)
|
||||
if app.storage.matrix == nil {
|
||||
app.storage.matrix = map[string]MatrixUser{}
|
||||
if app.storage.GetMatrix() == nil {
|
||||
app.storage.matrix = matrixStore{}
|
||||
}
|
||||
roomID, encrypted, err := app.matrix.CreateRoom(req.UserID)
|
||||
if err != nil {
|
||||
@ -653,13 +654,13 @@ func (app *appContext) MatrixConnect(gc *gin.Context) {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
app.storage.matrix[req.JellyfinID] = MatrixUser{
|
||||
app.storage.SetMatrixKey(req.JellyfinID, MatrixUser{
|
||||
UserID: req.UserID,
|
||||
RoomID: string(roomID),
|
||||
Lang: "en-us",
|
||||
Contact: true,
|
||||
Encrypted: encrypted,
|
||||
}
|
||||
})
|
||||
app.matrix.isEncrypted[roomID] = encrypted
|
||||
if err := app.storage.storeMatrixUsers(); err != nil {
|
||||
app.err.Printf("Failed to store Matrix users: %v", err)
|
||||
@ -715,7 +716,7 @@ func (app *appContext) DiscordConnect(gc *gin.Context) {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
app.storage.discord[req.JellyfinID] = user
|
||||
app.storage.SetDiscordKey(req.JellyfinID, user)
|
||||
if err := app.storage.storeDiscordUsers(); err != nil {
|
||||
app.err.Printf("Failed to store Discord users: %v", err)
|
||||
respondBool(500, false, gc)
|
||||
@ -739,8 +740,7 @@ func (app *appContext) UnlinkDiscord(gc *gin.Context) {
|
||||
respond(400, "User not found", gc)
|
||||
return
|
||||
} */
|
||||
delete(app.storage.discord, req.ID)
|
||||
app.storage.storeDiscordUsers()
|
||||
app.storage.DeleteDiscordKey(req.ID)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
@ -758,8 +758,7 @@ func (app *appContext) UnlinkTelegram(gc *gin.Context) {
|
||||
respond(400, "User not found", gc)
|
||||
return
|
||||
} */
|
||||
delete(app.storage.telegram, req.ID)
|
||||
app.storage.storeTelegramUsers()
|
||||
app.storage.DeleteTelegramKey(req.ID)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
@ -777,7 +776,6 @@ func (app *appContext) UnlinkMatrix(gc *gin.Context) {
|
||||
respond(400, "User not found", gc)
|
||||
return
|
||||
} */
|
||||
delete(app.storage.matrix, req.ID)
|
||||
app.storage.storeMatrixUsers()
|
||||
app.storage.DeleteMatrixKey(req.ID)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, er
|
||||
}
|
||||
username := jfUser.Name
|
||||
email := ""
|
||||
if e, ok := app.storage.emails[jfID]; ok {
|
||||
if e, ok := app.storage.GetEmailsKey(jfID); ok {
|
||||
email = e.Addr
|
||||
}
|
||||
for _, ombiUser := range ombiUsers {
|
||||
|
629
api-userpage.go
Normal file
629
api-userpage.go
Normal file
@ -0,0 +1,629 @@
|
||||
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()
|
||||
if emailEnabled {
|
||||
app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
|
||||
app.storage.SetEmailsKey(id, EmailAddress{Addr: req.Email, Contact: true})
|
||||
app.storage.storeEmails()
|
||||
}
|
||||
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
|
||||
}
|
||||
} else {
|
||||
discordUser, discordVerified = app.discord.verifiedTokens[req.DiscordPIN]
|
||||
discordUser, discordVerified = app.discord.UserVerified(req.DiscordPIN)
|
||||
if !discordVerified {
|
||||
f = func(gc *gin.Context) {
|
||||
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
|
||||
}
|
||||
if app.config.Section("discord").Key("require_unique").MustBool(false) {
|
||||
for _, u := range app.storage.discord {
|
||||
for _, u := range app.storage.GetDiscord() {
|
||||
if discordUser.ID == u.ID {
|
||||
f = func(gc *gin.Context) {
|
||||
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
|
||||
}
|
||||
if app.config.Section("matrix").Key("require_unique").MustBool(false) {
|
||||
for _, u := range app.storage.matrix {
|
||||
for _, u := range app.storage.GetMatrix() {
|
||||
if user.User.UserID == u.UserID {
|
||||
f = func(gc *gin.Context) {
|
||||
app.debug.Printf("%s: New user failed: Matrix user already linked", req.Code)
|
||||
@ -193,7 +193,8 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
|
||||
}
|
||||
}
|
||||
telegramTokenIndex := -1
|
||||
var tgToken TelegramVerifiedToken
|
||||
telegramVerified := false
|
||||
if telegramEnabled {
|
||||
if req.TelegramPIN == "" {
|
||||
if app.config.Section("telegram").Key("required").MustBool(false) {
|
||||
@ -205,13 +206,8 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
return
|
||||
}
|
||||
} else {
|
||||
for i, v := range app.telegram.verifiedTokens {
|
||||
if v.Token == req.TelegramPIN {
|
||||
telegramTokenIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if telegramTokenIndex == -1 {
|
||||
tgToken, telegramVerified = app.telegram.TokenVerified(req.TelegramPIN)
|
||||
if !telegramVerified {
|
||||
f = func(gc *gin.Context) {
|
||||
app.debug.Printf("%s: New user failed: Telegram PIN was invalid", req.Code)
|
||||
respond(401, "errorInvalidPIN", gc)
|
||||
@ -219,9 +215,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
success = false
|
||||
return
|
||||
}
|
||||
if app.config.Section("telegram").Key("require_unique").MustBool(false) {
|
||||
for _, u := range app.storage.telegram {
|
||||
if app.telegram.verifiedTokens[telegramTokenIndex].Username == u.Username {
|
||||
if app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(tgToken.Username) {
|
||||
f = func(gc *gin.Context) {
|
||||
app.debug.Printf("%s: New user failed: Telegram user already linked", req.Code)
|
||||
respond(400, "errorAccountLinked", gc)
|
||||
@ -231,17 +225,11 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) && !confirmed {
|
||||
claims := jwt.MapClaims{
|
||||
"valid": true,
|
||||
"invite": req.Code,
|
||||
"email": req.Email,
|
||||
"username": req.Username,
|
||||
"password": req.Password,
|
||||
"telegramPIN": req.TelegramPIN,
|
||||
"exp": time.Now().Add(time.Hour * 12).Unix(),
|
||||
"exp": time.Now().Add(30 * time.Minute).Unix(),
|
||||
"type": "confirmation",
|
||||
}
|
||||
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
@ -254,10 +242,17 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
success = false
|
||||
return
|
||||
}
|
||||
inv := app.storage.invites[req.Code]
|
||||
inv.Keys = append(inv.Keys, key)
|
||||
app.storage.invites[req.Code] = inv
|
||||
app.storage.storeInvites()
|
||||
if app.ConfirmationKeys == nil {
|
||||
app.ConfirmationKeys = map[string]map[string]newUserDTO{}
|
||||
}
|
||||
cKeys, ok := app.ConfirmationKeys[req.Code]
|
||||
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) {
|
||||
app.debug.Printf("%s: Email confirmation required", req.Code)
|
||||
respond(401, "confirmEmail", gc)
|
||||
@ -284,7 +279,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
return
|
||||
}
|
||||
app.storage.loadProfiles()
|
||||
invite := app.storage.invites[req.Code]
|
||||
invite, _ := app.storage.GetInvitesKey(req.Code)
|
||||
app.checkInvite(req.Code, true, req.Username)
|
||||
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) {
|
||||
for address, settings := range invite.Notify {
|
||||
@ -339,7 +334,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
}
|
||||
// if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
||||
if req.Email != "" {
|
||||
app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
|
||||
app.storage.SetEmailsKey(id, EmailAddress{Addr: req.Email, Contact: true})
|
||||
app.storage.storeEmails()
|
||||
}
|
||||
expiry := time.Time{}
|
||||
@ -352,20 +347,19 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
app.err.Printf("Failed to store user duration: %v", err)
|
||||
}
|
||||
}
|
||||
if discordEnabled && discordVerified {
|
||||
if discordVerified {
|
||||
discordUser.Contact = req.DiscordContact
|
||||
if app.storage.discord == nil {
|
||||
app.storage.discord = map[string]DiscordUser{}
|
||||
app.storage.discord = discordStore{}
|
||||
}
|
||||
app.storage.discord[user.ID] = discordUser
|
||||
app.storage.SetDiscordKey(user.ID, discordUser)
|
||||
if err := app.storage.storeDiscordUsers(); err != nil {
|
||||
app.err.Printf("Failed to store Discord users: %v", err)
|
||||
} else {
|
||||
delete(app.discord.verifiedTokens, req.DiscordPIN)
|
||||
}
|
||||
}
|
||||
if telegramEnabled && telegramTokenIndex != -1 {
|
||||
tgToken := app.telegram.verifiedTokens[telegramTokenIndex]
|
||||
if telegramVerified {
|
||||
tgUser := TelegramUser{
|
||||
ChatID: tgToken.ChatID,
|
||||
Username: tgToken.Username,
|
||||
@ -375,15 +369,10 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
tgUser.Lang = lang
|
||||
}
|
||||
if app.storage.telegram == nil {
|
||||
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.storage.telegram = telegramStore{}
|
||||
}
|
||||
app.telegram.DeleteVerifiedToken(req.TelegramPIN)
|
||||
app.storage.SetTelegramKey(user.ID, tgUser)
|
||||
}
|
||||
if invite.Profile != "" && app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
if profile.Ombi != nil && len(profile.Ombi) != 0 {
|
||||
@ -394,18 +383,19 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", "))
|
||||
} else {
|
||||
app.info.Println("Created Ombi user")
|
||||
if (discordEnabled && discordVerified) || (telegramEnabled && telegramTokenIndex != -1) {
|
||||
if discordVerified || telegramVerified {
|
||||
ombiUser, status, err := app.getOmbiUser(id)
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to get Ombi user (%d): %v", status, err)
|
||||
} else {
|
||||
dID := ""
|
||||
tUser := ""
|
||||
if discordEnabled && discordVerified {
|
||||
if discordVerified {
|
||||
dID = discordUser.ID
|
||||
}
|
||||
if telegramEnabled && telegramTokenIndex != -1 {
|
||||
tUser = app.storage.telegram[user.ID].Username
|
||||
if telegramVerified {
|
||||
u, _ := app.storage.GetTelegramKey(user.ID)
|
||||
tUser = u.Username
|
||||
}
|
||||
resp, status, err := app.ombi.SetNotificationPrefs(ombiUser, dID, tUser)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
@ -423,14 +413,14 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
matrixUser.Contact = req.MatrixContact
|
||||
delete(app.matrix.tokens, req.MatrixPIN)
|
||||
if app.storage.matrix == nil {
|
||||
app.storage.matrix = map[string]MatrixUser{}
|
||||
app.storage.matrix = matrixStore{}
|
||||
}
|
||||
app.storage.matrix[user.ID] = matrixUser
|
||||
app.storage.SetMatrixKey(user.ID, matrixUser)
|
||||
if err := app.storage.storeMatrixUsers(); err != nil {
|
||||
app.err.Printf("Failed to store Matrix users: %v", err)
|
||||
}
|
||||
}
|
||||
if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 || discordVerified {
|
||||
if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramVerified || discordVerified || matrixVerified {
|
||||
name := app.getAddressOrName(user.ID)
|
||||
app.debug.Printf("%s: Sending welcome message to %s", req.Username, name)
|
||||
msg, err := app.email.constructWelcome(req.Username, expiry, app, false)
|
||||
@ -473,6 +463,7 @@ func (app *appContext) NewUser(gc *gin.Context) {
|
||||
for _, val := range validation {
|
||||
if !val {
|
||||
valid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
@ -488,7 +479,7 @@ func (app *appContext) NewUser(gc *gin.Context) {
|
||||
return
|
||||
}
|
||||
if app.config.Section("email").Key("require_unique").MustBool(false) && req.Email != "" {
|
||||
for _, email := range app.storage.emails {
|
||||
for _, email := range app.storage.GetEmails() {
|
||||
if req.Email == email.Addr {
|
||||
app.info.Printf("%s: New user failed: Email already in use", req.Code)
|
||||
respond(400, "errorEmailLinked", gc)
|
||||
@ -897,7 +888,7 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
||||
if !jfUser.LastActivityDate.IsZero() {
|
||||
user.LastActive = jfUser.LastActivityDate.Unix()
|
||||
}
|
||||
if email, ok := app.storage.emails[jfUser.ID]; ok {
|
||||
if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok {
|
||||
user.Email = email.Addr
|
||||
user.NotifyThroughEmail = email.Contact
|
||||
user.Label = email.Label
|
||||
@ -907,15 +898,15 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
||||
if ok {
|
||||
user.Expiry = expiry.Unix()
|
||||
}
|
||||
if tgUser, ok := app.storage.telegram[jfUser.ID]; ok {
|
||||
if tgUser, ok := app.storage.GetTelegramKey(jfUser.ID); ok {
|
||||
user.Telegram = tgUser.Username
|
||||
user.NotifyThroughTelegram = tgUser.Contact
|
||||
}
|
||||
if mxUser, ok := app.storage.matrix[jfUser.ID]; ok {
|
||||
if mxUser, ok := app.storage.GetMatrixKey(jfUser.ID); ok {
|
||||
user.Matrix = mxUser.UserID
|
||||
user.NotifyThroughMatrix = mxUser.Contact
|
||||
}
|
||||
if dcUser, ok := app.storage.discord[jfUser.ID]; ok {
|
||||
if dcUser, ok := app.storage.GetDiscordKey(jfUser.ID); ok {
|
||||
user.Discord = RenderDiscordUsername(dcUser)
|
||||
// user.Discord = dcUser.Username + "#" + dcUser.Discriminator
|
||||
user.DiscordID = dcUser.ID
|
||||
@ -949,11 +940,11 @@ func (app *appContext) SetAccountsAdmin(gc *gin.Context) {
|
||||
id := jfUser.ID
|
||||
if admin, ok := req[id]; ok {
|
||||
var emailStore = EmailAddress{}
|
||||
if oldEmail, ok := app.storage.emails[id]; ok {
|
||||
if oldEmail, ok := app.storage.GetEmailsKey(id); ok {
|
||||
emailStore = oldEmail
|
||||
}
|
||||
emailStore.Admin = admin
|
||||
app.storage.emails[id] = emailStore
|
||||
app.storage.SetEmailsKey(id, emailStore)
|
||||
}
|
||||
}
|
||||
if err := app.storage.storeEmails(); err != nil {
|
||||
@ -986,11 +977,11 @@ func (app *appContext) ModifyLabels(gc *gin.Context) {
|
||||
id := jfUser.ID
|
||||
if label, ok := req[id]; ok {
|
||||
var emailStore = EmailAddress{}
|
||||
if oldEmail, ok := app.storage.emails[id]; ok {
|
||||
if oldEmail, ok := app.storage.GetEmailsKey(id); ok {
|
||||
emailStore = oldEmail
|
||||
}
|
||||
emailStore.Label = label
|
||||
app.storage.emails[id] = emailStore
|
||||
app.storage.SetEmailsKey(id, emailStore)
|
||||
}
|
||||
}
|
||||
if err := app.storage.storeEmails(); err != nil {
|
||||
@ -1024,7 +1015,7 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
|
||||
id := jfUser.ID
|
||||
if address, ok := req[id]; ok {
|
||||
var emailStore = EmailAddress{}
|
||||
oldEmail, ok := app.storage.emails[id]
|
||||
oldEmail, ok := app.storage.GetEmailsKey(id)
|
||||
if ok {
|
||||
emailStore = oldEmail
|
||||
}
|
||||
@ -1035,7 +1026,7 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
|
||||
}
|
||||
|
||||
emailStore.Addr = address
|
||||
app.storage.emails[id] = emailStore
|
||||
app.storage.SetEmailsKey(id, emailStore)
|
||||
if ombiEnabled {
|
||||
ombiUser, code, err := app.getOmbiUser(id)
|
||||
if code == 200 && err == nil {
|
||||
|
13
api.go
13
api.go
@ -143,6 +143,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
respondBool(status, false, gc)
|
||||
return
|
||||
}
|
||||
delete(app.internalPWRs, req.PIN)
|
||||
} else {
|
||||
resp, status, err := app.jf.ResetPassword(req.PIN)
|
||||
if status != 200 || err != nil || !resp.Success {
|
||||
@ -214,7 +215,7 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
app.info.Println("Config requested")
|
||||
resp := app.configBase
|
||||
// Load language options
|
||||
formOptions := app.storage.lang.Form.getOptions()
|
||||
formOptions := app.storage.lang.User.getOptions()
|
||||
fl := resp.Sections["ui"].Settings["language-form"]
|
||||
fl.Options = formOptions
|
||||
fl.Value = app.config.Section("ui").Key("language-form").MustString("en-us")
|
||||
@ -268,7 +269,7 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
val := app.config.Section(sectName).Key(settingName)
|
||||
s := resp.Sections[sectName].Settings[settingName]
|
||||
switch setting.Type {
|
||||
case "text", "email", "select", "password":
|
||||
case "text", "email", "select", "password", "note":
|
||||
s.Value = val.MustString("")
|
||||
case "number":
|
||||
s.Value = val.MustInt(0)
|
||||
@ -452,8 +453,8 @@ func (app *appContext) GetLanguages(gc *gin.Context) {
|
||||
page := gc.Param("page")
|
||||
resp := langDTO{}
|
||||
switch page {
|
||||
case "form":
|
||||
for key, lang := range app.storage.lang.Form {
|
||||
case "form", "user":
|
||||
for key, lang := range app.storage.lang.User {
|
||||
resp[key] = lang.Meta.Name
|
||||
}
|
||||
case "admin":
|
||||
@ -494,8 +495,8 @@ func (app *appContext) ServeLang(gc *gin.Context) {
|
||||
if page == "admin" {
|
||||
gc.JSON(200, app.storage.lang.Admin[lang])
|
||||
return
|
||||
} else if page == "form" {
|
||||
gc.JSON(200, app.storage.lang.Form[lang])
|
||||
} else if page == "form" || page == "user" {
|
||||
gc.JSON(200, app.storage.lang.User[lang])
|
||||
return
|
||||
}
|
||||
respondBool(400, false, gc)
|
||||
|
159
auth.go
159
auth.go
@ -9,21 +9,28 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
TOKEN_VALIDITY_SEC = 20 * 60
|
||||
REFRESH_TOKEN_VALIDITY_SEC = 3600 * 24
|
||||
)
|
||||
|
||||
func (app *appContext) webAuth() gin.HandlerFunc {
|
||||
return app.authenticate
|
||||
}
|
||||
|
||||
// CreateToken returns a web token as well as a refresh token, which can be used to obtain new tokens.
|
||||
func CreateToken(userId, jfId string) (string, string, error) {
|
||||
func CreateToken(userId, jfId string, admin bool) (string, string, error) {
|
||||
var token, refresh string
|
||||
claims := jwt.MapClaims{
|
||||
"valid": true,
|
||||
"id": userId,
|
||||
"exp": time.Now().Add(time.Minute * 20).Unix(),
|
||||
"exp": time.Now().Add(time.Second * TOKEN_VALIDITY_SEC).Unix(),
|
||||
"jfid": jfId,
|
||||
"admin": admin,
|
||||
"type": "bearer",
|
||||
}
|
||||
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
@ -31,7 +38,7 @@ func CreateToken(userId, jfId string) (string, string, error) {
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
claims["exp"] = time.Now().Add(time.Hour * 24).Unix()
|
||||
claims["exp"] = time.Now().Add(time.Second * REFRESH_TOKEN_VALIDITY_SEC).Unix()
|
||||
claims["type"] = "refresh"
|
||||
tk = jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
refresh, err = tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
|
||||
@ -41,8 +48,9 @@ func CreateToken(userId, jfId string) (string, string, error) {
|
||||
return token, refresh, nil
|
||||
}
|
||||
|
||||
// Check header for token
|
||||
func (app *appContext) authenticate(gc *gin.Context) {
|
||||
// Caller should return if this returns false.
|
||||
func (app *appContext) decodeValidateAuthHeader(gc *gin.Context) (claims jwt.MapClaims, ok bool) {
|
||||
ok = false
|
||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
||||
if header[0] != "Bearer" {
|
||||
app.debug.Println("Invalid authorization header")
|
||||
@ -55,23 +63,48 @@ func (app *appContext) authenticate(gc *gin.Context) {
|
||||
respond(401, "Unauthorized", gc)
|
||||
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))
|
||||
if err != nil {
|
||||
app.debug.Printf("Auth denied: %s", err)
|
||||
respond(401, "Unauthorized", gc)
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
expiry := time.Unix(expiryUnix, 0)
|
||||
if !(ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now())) {
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
userID := claims["id"].(string)
|
||||
jfID := claims["jfid"].(string)
|
||||
match := false
|
||||
for _, user := range app.users {
|
||||
for _, user := range app.adminUsers {
|
||||
if user.UserID == userID {
|
||||
match = true
|
||||
break
|
||||
@ -84,6 +117,7 @@ func (app *appContext) authenticate(gc *gin.Context) {
|
||||
}
|
||||
gc.Set("jfId", jfID)
|
||||
gc.Set("userId", userID)
|
||||
gc.Set("userMode", false)
|
||||
app.debug.Println("Auth succeeded")
|
||||
gc.Next()
|
||||
}
|
||||
@ -99,6 +133,44 @@ type getTokenDTO struct {
|
||||
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.
|
||||
// @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
|
||||
@ -109,18 +181,14 @@ type getTokenDTO struct {
|
||||
// @Security getTokenAuth
|
||||
func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
app.info.Println("Token requested (login attempt)")
|
||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
||||
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)
|
||||
username, password, ok := app.decodeValidateLoginHeader(gc)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var userID, jfID string
|
||||
match := false
|
||||
for _, user := range app.users {
|
||||
if user.Username == creds[0] && user.Password == creds[1] {
|
||||
for _, user := range app.adminUsers {
|
||||
if user.Username == username && user.Password == password {
|
||||
match = true
|
||||
app.debug.Println("Found existing user")
|
||||
userID = user.UserID
|
||||
@ -133,27 +201,20 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
return
|
||||
}
|
||||
if !match {
|
||||
user, status, err := app.authJf.Authenticate(creds[0], creds[1])
|
||||
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)
|
||||
user, ok := app.validateJellyfinCredentials(username, password, gc)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
jfID = user.ID
|
||||
if !app.config.Section("ui").Key("allow_all").MustBool(false) {
|
||||
accountsAdmin := false
|
||||
adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
|
||||
if emailStore, ok := app.storage.emails[jfID]; ok {
|
||||
if emailStore, ok := app.storage.GetEmailsKey(jfID); ok {
|
||||
accountsAdmin = emailStore.Admin
|
||||
}
|
||||
accountsAdmin = accountsAdmin || (adminOnly && user.Policy.IsAdministrator)
|
||||
if !accountsAdmin {
|
||||
app.debug.Printf("Auth denied: Users \"%s\" isn't admin", creds[0])
|
||||
app.debug.Printf("Auth denied: Users \"%s\" isn't admin", username)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
@ -163,10 +224,10 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
newUser := User{
|
||||
UserID: userID,
|
||||
}
|
||||
app.debug.Printf("Token generated for user \"%s\"", creds[0])
|
||||
app.users = append(app.users, newUser)
|
||||
app.debug.Printf("Token generated for user \"%s\"", username)
|
||||
app.adminUsers = append(app.adminUsers, newUser)
|
||||
}
|
||||
token, refresh, err := CreateToken(userID, jfID)
|
||||
token, refresh, err := CreateToken(userID, jfID, true)
|
||||
if err != nil {
|
||||
app.err.Printf("getToken failed: Couldn't generate token (%s)", err)
|
||||
respond(500, "Couldn't generate token", gc)
|
||||
@ -176,15 +237,9 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
gc.JSON(200, getTokenDTO{token})
|
||||
}
|
||||
|
||||
// @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)")
|
||||
cookie, err := gc.Cookie("refresh")
|
||||
func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName string) (claims jwt.MapClaims, ok bool) {
|
||||
ok = false
|
||||
cookie, err := gc.Cookie(cookieName)
|
||||
if err != nil || cookie == "" {
|
||||
app.debug.Printf("getTokenRefresh denied: Couldn't get token: %s", err)
|
||||
respond(400, "Couldn't get token", gc)
|
||||
@ -203,27 +258,45 @@ func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
||||
respond(400, "Invalid token", gc)
|
||||
return
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
claims, ok = token.Claims.(jwt.MapClaims)
|
||||
expiryUnix := int64(claims["exp"].(float64))
|
||||
if err != nil {
|
||||
app.debug.Printf("getTokenRefresh: Invalid token expiry: %s", err)
|
||||
respond(401, "Invalid token", gc)
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
expiry := time.Unix(expiryUnix, 0)
|
||||
if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) {
|
||||
app.debug.Printf("getTokenRefresh: Invalid token: %s", err)
|
||||
app.debug.Printf("getTokenRefresh: Invalid token: %+v", err)
|
||||
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
|
||||
}
|
||||
userID := claims["id"].(string)
|
||||
jfID := claims["jfid"].(string)
|
||||
jwt, refresh, err := CreateToken(userID, jfID)
|
||||
jwt, refresh, err := CreateToken(userID, jfID, true)
|
||||
if err != nil {
|
||||
app.err.Printf("getTokenRefresh failed: Couldn't generate token (%s)", err)
|
||||
respond(500, "Couldn't generate token", gc)
|
||||
return
|
||||
}
|
||||
gc.SetCookie("refresh", refresh, (3600 * 24), "/", gc.Request.URL.Hostname(), true, true)
|
||||
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", gc.Request.URL.Hostname(), true, true)
|
||||
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"))))
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users", "matrix_users", "announcements"} {
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users", "matrix_users", "announcements", "custom_user_page_content"} {
|
||||
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
|
||||
}
|
||||
for _, key := range []string{"matrix_sql"} {
|
||||
@ -160,6 +160,12 @@ func (app *appContext) loadConfig() error {
|
||||
app.storage.customEmails_path = app.config.Section("files").Key("custom_emails").String()
|
||||
app.storage.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("")
|
||||
|
||||
if substituteStrings != "" {
|
||||
@ -169,11 +175,11 @@ func (app *appContext) loadConfig() error {
|
||||
|
||||
oldFormLang := app.config.Section("ui").Key("language").MustString("")
|
||||
if oldFormLang != "" {
|
||||
app.storage.lang.chosenFormLang = oldFormLang
|
||||
app.storage.lang.chosenUserLang = oldFormLang
|
||||
}
|
||||
newFormLang := app.config.Section("ui").Key("language-form").MustString("")
|
||||
if newFormLang != "" {
|
||||
app.storage.lang.chosenFormLang = newFormLang
|
||||
app.storage.lang.chosenUserLang = newFormLang
|
||||
}
|
||||
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")
|
||||
|
@ -373,6 +373,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
@ -465,6 +499,14 @@
|
||||
"type": "text",
|
||||
"value": "Need help? contact me.",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -1534,6 +1576,14 @@
|
||||
"value": "",
|
||||
"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": {
|
||||
"name": "Telegram users",
|
||||
"required": false,
|
||||
|
11
css/base.css
11
css/base.css
@ -13,6 +13,8 @@
|
||||
--border-width-2: 3px;
|
||||
--border-width-4: 5px;
|
||||
--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 {
|
||||
@ -485,6 +487,15 @@ a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) {
|
||||
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 {
|
||||
display: block;
|
||||
text-align: center;
|
||||
|
24
daemon.go
24
daemon.go
@ -13,14 +13,16 @@ func (app *appContext) clearEmails() {
|
||||
return
|
||||
}
|
||||
// Rebuild email storage to from existing users to reduce time complexity
|
||||
emails := map[string]EmailAddress{}
|
||||
emails := emailStore{}
|
||||
app.storage.emailsLock.Lock()
|
||||
for _, user := range users {
|
||||
if email, ok := app.storage.emails[user.ID]; ok {
|
||||
if email, ok := app.storage.GetEmailsKey(user.ID); ok {
|
||||
emails[user.ID] = email
|
||||
}
|
||||
}
|
||||
app.storage.emails = emails
|
||||
app.storage.storeEmails()
|
||||
app.storage.emailsLock.Unlock()
|
||||
}
|
||||
|
||||
// clearDiscord does the same as clearEmails, but for Discord Users.
|
||||
@ -32,14 +34,16 @@ func (app *appContext) clearDiscord() {
|
||||
return
|
||||
}
|
||||
// Rebuild discord storage to from existing users to reduce time complexity
|
||||
dcUsers := map[string]DiscordUser{}
|
||||
dcUsers := discordStore{}
|
||||
app.storage.discordLock.Lock()
|
||||
for _, user := range users {
|
||||
if dcUser, ok := app.storage.discord[user.ID]; ok {
|
||||
if dcUser, ok := app.storage.GetDiscordKey(user.ID); ok {
|
||||
dcUsers[user.ID] = dcUser
|
||||
}
|
||||
}
|
||||
app.storage.discord = dcUsers
|
||||
app.storage.storeDiscordUsers()
|
||||
app.storage.discordLock.Unlock()
|
||||
}
|
||||
|
||||
// clearMatrix does the same as clearEmails, but for Matrix Users.
|
||||
@ -51,14 +55,16 @@ func (app *appContext) clearMatrix() {
|
||||
return
|
||||
}
|
||||
// Rebuild matrix storage to from existing users to reduce time complexity
|
||||
mxUsers := map[string]MatrixUser{}
|
||||
mxUsers := matrixStore{}
|
||||
app.storage.matrixLock.Lock()
|
||||
for _, user := range users {
|
||||
if mxUser, ok := app.storage.matrix[user.ID]; ok {
|
||||
if mxUser, ok := app.storage.GetMatrixKey(user.ID); ok {
|
||||
mxUsers[user.ID] = mxUser
|
||||
}
|
||||
}
|
||||
app.storage.matrix = mxUsers
|
||||
app.storage.storeMatrixUsers()
|
||||
app.storage.matrixLock.Unlock()
|
||||
}
|
||||
|
||||
// clearTelegram does the same as clearEmails, but for Telegram Users.
|
||||
@ -70,14 +76,16 @@ func (app *appContext) clearTelegram() {
|
||||
return
|
||||
}
|
||||
// Rebuild telegram storage to from existing users to reduce time complexity
|
||||
tgUsers := map[string]TelegramUser{}
|
||||
tgUsers := telegramStore{}
|
||||
app.storage.telegramLock.Lock()
|
||||
for _, user := range users {
|
||||
if tgUser, ok := app.storage.telegram[user.ID]; ok {
|
||||
if tgUser, ok := app.storage.GetTelegramKey(user.ID); ok {
|
||||
tgUsers[user.ID] = tgUser
|
||||
}
|
||||
}
|
||||
app.storage.telegram = tgUsers
|
||||
app.storage.storeTelegramUsers()
|
||||
app.storage.telegramLock.Unlock()
|
||||
}
|
||||
|
||||
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
|
||||
|
106
discord.go
106
discord.go
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dg "github.com/bwmarrin/discordgo"
|
||||
)
|
||||
@ -12,8 +13,8 @@ type DiscordDaemon struct {
|
||||
ShutdownChannel chan string
|
||||
bot *dg.Session
|
||||
username string
|
||||
tokens []string
|
||||
verifiedTokens map[string]DiscordUser // Map of tokens to discord users.
|
||||
tokens map[string]VerifToken // Map of pins to tokens.
|
||||
verifiedTokens map[string]DiscordUser // Map of token pins to discord users.
|
||||
channelID, channelName, inviteChannelID, inviteChannelName string
|
||||
guildID string
|
||||
serverChannelName, serverName string
|
||||
@ -37,7 +38,7 @@ func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
|
||||
Stopped: false,
|
||||
ShutdownChannel: make(chan string),
|
||||
bot: bot,
|
||||
tokens: []string{},
|
||||
tokens: map[string]VerifToken{},
|
||||
verifiedTokens: map[string]DiscordUser{},
|
||||
users: map[string]DiscordUser{},
|
||||
app: app,
|
||||
@ -48,7 +49,7 @@ func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
|
||||
dd.commandHandlers[app.config.Section("discord").Key("start_command").MustString("start")] = dd.cmdStart
|
||||
dd.commandHandlers["lang"] = dd.cmdLang
|
||||
dd.commandHandlers["pin"] = dd.cmdPIN
|
||||
for _, user := range app.storage.discord {
|
||||
for _, user := range app.storage.GetDiscord() {
|
||||
dd.users[user.ID] = user
|
||||
}
|
||||
|
||||
@ -58,7 +59,15 @@ func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
|
||||
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
||||
func (d *DiscordDaemon) NewAuthToken() string {
|
||||
pin := genAuthToken()
|
||||
d.tokens = append(d.tokens, pin)
|
||||
d.tokens[pin] = VerifToken{Expiry: time.Now().Add(VERIF_TOKEN_EXPIRY_SEC * time.Second), JellyfinID: ""}
|
||||
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
|
||||
}
|
||||
|
||||
@ -210,7 +219,9 @@ func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconU
|
||||
d.app.err.Printf("Discord: Failed to get guild: %v", err)
|
||||
return
|
||||
}
|
||||
// FIXME: Fix CSS, and handle no icon
|
||||
iconURL = guild.IconURL("256")
|
||||
fmt.Println("GOT ICON", iconURL)
|
||||
return
|
||||
}
|
||||
|
||||
@ -429,14 +440,8 @@ func (d *DiscordDaemon) cmdStart(s *dg.Session, i *dg.InteractionCreate, lang st
|
||||
|
||||
func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang string) {
|
||||
pin := i.ApplicationCommandData().Options[0].StringValue()
|
||||
tokenIndex := -1
|
||||
for i, token := range d.tokens {
|
||||
if pin == token {
|
||||
tokenIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if tokenIndex == -1 {
|
||||
user, ok := d.tokens[pin]
|
||||
if !ok || time.Now().After(user.Expiry) {
|
||||
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
||||
// Type: dg.InteractionResponseChannelMessageWithSource,
|
||||
Type: dg.InteractionResponseChannelMessageWithSource,
|
||||
@ -448,6 +453,7 @@ func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang stri
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
|
||||
}
|
||||
delete(d.tokens, pin)
|
||||
return
|
||||
}
|
||||
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
||||
@ -461,23 +467,21 @@ func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang stri
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
|
||||
}
|
||||
d.verifiedTokens[pin] = d.users[i.Interaction.Member.User.ID]
|
||||
d.tokens[len(d.tokens)-1], d.tokens[tokenIndex] = d.tokens[tokenIndex], d.tokens[len(d.tokens)-1]
|
||||
d.tokens = d.tokens[:len(d.tokens)-1]
|
||||
dcUser := d.users[i.Interaction.Member.User.ID]
|
||||
dcUser.JellyfinID = user.JellyfinID
|
||||
d.verifiedTokens[pin] = dcUser
|
||||
delete(d.tokens, pin)
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang string) {
|
||||
code := i.ApplicationCommandData().Options[0].StringValue()
|
||||
if _, ok := d.app.storage.lang.Telegram[code]; ok {
|
||||
var user DiscordUser
|
||||
for jfID, u := range d.app.storage.discord {
|
||||
for jfID, u := range d.app.storage.GetDiscord() {
|
||||
if u.ID == i.Interaction.Member.User.ID {
|
||||
u.Lang = code
|
||||
lang = code
|
||||
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)
|
||||
}
|
||||
d.app.storage.SetDiscordKey(jfID, u)
|
||||
user = u
|
||||
break
|
||||
}
|
||||
@ -580,13 +584,10 @@ func (d *DiscordDaemon) msgLang(s *dg.Session, m *dg.MessageCreate, sects []stri
|
||||
}
|
||||
if _, ok := d.app.storage.lang.Telegram[sects[1]]; ok {
|
||||
var user DiscordUser
|
||||
for jfID, u := range d.app.storage.discord {
|
||||
for jfID, u := range d.app.storage.GetDiscord() {
|
||||
if u.ID == m.Author.ID {
|
||||
u.Lang = sects[1]
|
||||
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)
|
||||
}
|
||||
d.app.storage.SetDiscordKey(jfID, u)
|
||||
user = u
|
||||
break
|
||||
}
|
||||
@ -610,14 +611,8 @@ func (d *DiscordDaemon) msgPIN(s *dg.Session, m *dg.MessageCreate, sects []strin
|
||||
d.app.debug.Println("Discord: Ignoring message as user was not found")
|
||||
return
|
||||
}
|
||||
tokenIndex := -1
|
||||
for i, token := range d.tokens {
|
||||
if sects[0] == token {
|
||||
tokenIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if tokenIndex == -1 {
|
||||
user, ok := d.tokens[sects[0]]
|
||||
if !ok || time.Now().After(user.Expiry) {
|
||||
_, err := s.ChannelMessageSend(
|
||||
m.ChannelID,
|
||||
d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"),
|
||||
@ -625,6 +620,7 @@ func (d *DiscordDaemon) msgPIN(s *dg.Session, m *dg.MessageCreate, sects []strin
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||
}
|
||||
delete(d.tokens, sects[0])
|
||||
return
|
||||
}
|
||||
_, err := s.ChannelMessageSend(
|
||||
@ -634,9 +630,10 @@ func (d *DiscordDaemon) msgPIN(s *dg.Session, m *dg.MessageCreate, sects []strin
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||
}
|
||||
d.verifiedTokens[sects[0]] = d.users[m.Author.ID]
|
||||
d.tokens[len(d.tokens)-1], d.tokens[tokenIndex] = d.tokens[tokenIndex], d.tokens[len(d.tokens)-1]
|
||||
d.tokens = d.tokens[:len(d.tokens)-1]
|
||||
dcUser := d.users[m.Author.ID]
|
||||
dcUser.JellyfinID = user.JellyfinID
|
||||
d.verifiedTokens[sects[0]] = dcUser
|
||||
delete(d.tokens, sects[0])
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) SendDM(message *Message, userID ...string) error {
|
||||
@ -690,3 +687,38 @@ func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
|
||||
}
|
||||
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,6 +10,7 @@ import (
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -23,7 +24,7 @@ import (
|
||||
sMail "github.com/xhit/go-simple-mail/v2"
|
||||
)
|
||||
|
||||
var renderer = html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
|
||||
var markdownRenderer = html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
|
||||
|
||||
// EmailClient implements email sending, right now via smtp, mailgun or a dummy client.
|
||||
type EmailClient interface {
|
||||
@ -304,10 +305,17 @@ func (emailer *Emailer) confirmationValues(code, username, key string, app *appC
|
||||
} else {
|
||||
message := app.config.Section("messages").Key("message").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") {
|
||||
inviteLink += "/invite"
|
||||
}
|
||||
inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key)
|
||||
inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, url.PathEscape(key))
|
||||
}
|
||||
template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": username})
|
||||
template["confirmationURL"] = inviteLink
|
||||
template["message"] = message
|
||||
@ -345,7 +353,7 @@ func (emailer *Emailer) constructTemplate(subject, md string, app *appContext, u
|
||||
subject = templateEmail(subject, []string{"{username}"}, nil, map[string]interface{}{"username": username[0]})
|
||||
}
|
||||
email := &Message{Subject: subject}
|
||||
html := markdown.ToHTML([]byte(md), nil, renderer)
|
||||
html := markdown.ToHTML([]byte(md), nil, markdownRenderer)
|
||||
text := stripMarkdown(md)
|
||||
message := app.config.Section("messages").Key("message").String()
|
||||
var err error
|
||||
@ -514,18 +522,6 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
|
||||
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{} {
|
||||
d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
|
||||
message := app.config.Section("messages").Key("message").String()
|
||||
@ -827,49 +823,82 @@ func (emailer *Emailer) send(email *Message, address ...string) error {
|
||||
return emailer.sender.Send(emailer.fromName, emailer.fromAddr, email, address...)
|
||||
}
|
||||
|
||||
func (app *appContext) sendByID(email *Message, ID ...string) error {
|
||||
func (app *appContext) sendByID(email *Message, ID ...string) (err error) {
|
||||
for _, id := range ID {
|
||||
var err error
|
||||
if tgChat, ok := app.storage.telegram[id]; ok && tgChat.Contact && telegramEnabled {
|
||||
if tgChat, ok := app.storage.GetTelegramKey(id); ok && tgChat.Contact && telegramEnabled {
|
||||
err = app.telegram.Send(email, tgChat.ChatID)
|
||||
if err != nil {
|
||||
return err
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
}
|
||||
}
|
||||
if dcChat, ok := app.storage.discord[id]; ok && dcChat.Contact && discordEnabled {
|
||||
if dcChat, ok := app.storage.GetDiscordKey(id); ok && dcChat.Contact && discordEnabled {
|
||||
err = app.discord.Send(email, dcChat.ChannelID)
|
||||
if err != nil {
|
||||
return err
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
}
|
||||
}
|
||||
if mxChat, ok := app.storage.matrix[id]; ok && mxChat.Contact && matrixEnabled {
|
||||
if mxChat, ok := app.storage.GetMatrixKey(id); ok && mxChat.Contact && matrixEnabled {
|
||||
err = app.matrix.Send(email, mxChat)
|
||||
if err != nil {
|
||||
return err
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
}
|
||||
}
|
||||
if address, ok := app.storage.emails[id]; ok && address.Contact && emailEnabled {
|
||||
if address, ok := app.storage.GetEmailsKey(id); ok && address.Contact && emailEnabled {
|
||||
err = app.email.send(email, address.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
}
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
func (app *appContext) getAddressOrName(jfID string) string {
|
||||
if dcChat, ok := app.storage.discord[jfID]; ok && dcChat.Contact && discordEnabled {
|
||||
if dcChat, ok := app.storage.GetDiscordKey(jfID); ok && dcChat.Contact && discordEnabled {
|
||||
return RenderDiscordUsername(dcChat)
|
||||
}
|
||||
if tgChat, ok := app.storage.telegram[jfID]; ok && tgChat.Contact && telegramEnabled {
|
||||
if tgChat, ok := app.storage.GetTelegramKey(jfID); ok && tgChat.Contact && telegramEnabled {
|
||||
return "@" + tgChat.Username
|
||||
}
|
||||
if addr, ok := app.storage.emails[jfID]; ok {
|
||||
if addr, ok := app.storage.GetEmailsKey(jfID); ok {
|
||||
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 ""
|
||||
}
|
||||
|
52
html/account-linking.html
Normal file
52
html/account-linking.html
Normal file
@ -0,0 +1,52 @@
|
||||
{{ 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,17 +22,7 @@
|
||||
{{ template "header.html" . }}
|
||||
</head>
|
||||
<body class="max-w-full overflow-x-hidden section">
|
||||
<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>
|
||||
{{ template "login-modal.html" . }}
|
||||
<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="">
|
||||
<span class="heading">{{ .strings.newUser }} <span class="modal-close">×</span></span>
|
||||
@ -420,6 +410,9 @@
|
||||
</span>
|
||||
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
|
||||
</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="mb-4">
|
||||
<header class="flex flex-wrap items-center justify-between">
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
|
||||
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>{{ .strings.successHeader }} - jfa-go</title>
|
||||
</head>
|
||||
|
@ -29,6 +29,8 @@
|
||||
window.captcha = {{ .captcha }};
|
||||
window.reCAPTCHA = {{ .reCAPTCHA }};
|
||||
window.reCAPTCHASiteKey = "{{ .reCAPTCHASiteKey }}";
|
||||
window.userPageEnabled = {{ .userPageEnabled }};
|
||||
window.userPageAddress = "{{ .userPageAddress }}";
|
||||
</script>
|
||||
{{ if .passwordReset }}
|
||||
<script src="js/pwr.js" type="module"></script>
|
||||
|
@ -17,6 +17,7 @@
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -26,53 +27,7 @@
|
||||
<p class="content mb-4">{{ .strings.confirmationRequiredMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ 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 }}
|
||||
{{ template "account-linking.html" . }}
|
||||
<div class="top-4 left-4 absolute">
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
@ -169,7 +124,7 @@
|
||||
</div>
|
||||
<div class="flex-initial">
|
||||
<div class="card ~neutral @low mb-4">
|
||||
<span class="label supra" for="inv-uses">{{ .strings.passwordRequirementsHeader }}</span>
|
||||
<span class="label supra">{{ .strings.passwordRequirementsHeader }}</span>
|
||||
<ul>
|
||||
{{ range $key, $value := .requirements }}
|
||||
<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="Description" content="jfa-go, a better way to manage Jellyfin users.">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
|
||||
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>Invalid Code - jfa-go</title>
|
||||
</head>
|
||||
|
25
html/login-modal.html
Normal file
25
html/login-modal.html
Normal file
@ -0,0 +1,25 @@
|
||||
<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
Normal file
158
html/user.html
Normal file
@ -0,0 +1,158 @@
|
||||
<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,6 +28,8 @@ type commonLangs map[string]commonLang
|
||||
type commonLang struct {
|
||||
Meta langMeta `json:"meta"`
|
||||
Strings langSection `json:"strings"`
|
||||
Notifications langSection `json:"notifications"`
|
||||
QuantityStrings map[string]quantityString `json:"quantityStrings"`
|
||||
}
|
||||
|
||||
type adminLang struct {
|
||||
@ -38,9 +40,9 @@ type adminLang struct {
|
||||
JSON string
|
||||
}
|
||||
|
||||
type formLangs map[string]formLang
|
||||
type userLangs map[string]userLang
|
||||
|
||||
func (ls *formLangs) getOptions() [][2]string {
|
||||
func (ls *userLangs) getOptions() [][2]string {
|
||||
opts := make([][2]string, len(*ls))
|
||||
i := 0
|
||||
for key, lang := range *ls {
|
||||
@ -50,13 +52,15 @@ func (ls *formLangs) getOptions() [][2]string {
|
||||
return opts
|
||||
}
|
||||
|
||||
type formLang struct {
|
||||
type userLang struct {
|
||||
Meta langMeta `json:"meta"`
|
||||
Strings langSection `json:"strings"`
|
||||
Notifications langSection `json:"notifications"`
|
||||
notificationsJSON string
|
||||
ValidationStrings map[string]quantityString `json:"validationStrings"`
|
||||
validationStringsJSON string
|
||||
QuantityStrings map[string]quantityString `json:"quantityStrings"`
|
||||
JSON string
|
||||
}
|
||||
|
||||
type pwrLangs map[string]pwrLang
|
||||
|
@ -15,20 +15,11 @@
|
||||
"warning": "Advarsel",
|
||||
"inviteInfiniteUsesWarning": "invitationer med uendelig brug kan blive misbrugt",
|
||||
"inviteSendToEmail": "Send til",
|
||||
"login": "Log på",
|
||||
"logout": "Log ud",
|
||||
"create": "Opret",
|
||||
"apply": "Anvend",
|
||||
"delete": "Slet",
|
||||
"add": "Tilføj",
|
||||
"select": "Vælg",
|
||||
"name": "Navn",
|
||||
"date": "Dato",
|
||||
"enabled": "Aktiveret",
|
||||
"disabled": "Deaktiveret",
|
||||
"reEnable": "Genaktiver",
|
||||
"disable": "Deaktiver",
|
||||
"admin": "Administrator",
|
||||
"updates": "Opdateringer",
|
||||
"update": "Opdatering",
|
||||
"download": "Hent",
|
||||
@ -37,7 +28,6 @@
|
||||
"lastActiveTime": "Sidst Aktiv",
|
||||
"from": "Fra",
|
||||
"user": "Bruger",
|
||||
"expiry": "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.",
|
||||
"aboutProgram": "Om",
|
||||
@ -54,7 +44,6 @@
|
||||
"conditionals": "Betingelser",
|
||||
"preview": "Eksempel",
|
||||
"reset": "Nulstil",
|
||||
"edit": "Rediger",
|
||||
"donate": "Doner",
|
||||
"contactThrough": "Kontakt gennem:",
|
||||
"extendExpiry": "Forlæng udløb",
|
||||
@ -128,13 +117,9 @@
|
||||
"updateAppliedRefresh": "Opdatering anvendt, genindlæs venligst siden.",
|
||||
"telegramVerified": "Telegram konto verificeret.",
|
||||
"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.",
|
||||
"errorHomescreenAppliedNoSettings": "Startskærmens layout blev anvendt, men anvendelsen af indstillingerne mislykkedes muligvis.",
|
||||
"errorSettingsFailed": "Ansøgningen mislykkedes.",
|
||||
"errorLoginBlank": "Brugernavnet og/eller adgangskoden blev efterladt tomme.",
|
||||
"errorUnknown": "Ukendt fejl.",
|
||||
"errorSaveEmail": "Kunne ikke gemme e-mail.",
|
||||
"errorBlankFields": "Felter blev efterladt tomme",
|
||||
"errorDeleteProfile": "Kunne ikke slette profilen {n}",
|
||||
@ -142,7 +127,6 @@
|
||||
"errorCreateProfile": "Kunne ikke oprette profilen {n}",
|
||||
"errorSetDefaultProfile": "Standard profilen kunne ikke indstilles.",
|
||||
"errorLoadUsers": "Kunne ikke indlæse brugere.",
|
||||
"errorSaveSettings": "Kunne ikke gemme indstillingerne.",
|
||||
"errorLoadSettings": "Indstillingerne kunne ikke indlæses.",
|
||||
"errorSetOmbiDefaults": "Ombi standarderne kunne ikke gemmes.",
|
||||
"errorLoadOmbiUsers": "Kunne ikke indlæse ombi brugere.",
|
||||
|
@ -13,11 +13,8 @@
|
||||
"warning": "Warnung",
|
||||
"inviteInfiniteUsesWarning": "Invites mit unendlich vielen Verwendungen können missbräuchlich verwendet werden",
|
||||
"inviteSendToEmail": "Senden an",
|
||||
"login": "Anmelden",
|
||||
"logout": "Abmelden",
|
||||
"create": "Erstellen",
|
||||
"apply": "Anwenden",
|
||||
"delete": "Löschen",
|
||||
"name": "Name",
|
||||
"date": "Datum",
|
||||
"lastActiveTime": "Zuletzt aktiv",
|
||||
@ -68,7 +65,6 @@
|
||||
"variables": "Variablen",
|
||||
"preview": "Vorschau",
|
||||
"reset": "Zurücksetzen",
|
||||
"edit": "Bearbeiten",
|
||||
"customizeMessages": "Benachrichtigungen anpassen",
|
||||
"customizeMessagesDescription": "Wenn du jfa-go's E-Mail-Vorlagen nicht benutzen willst, kannst du deinen eigenen unter Verwendung von Markdown erstellen.",
|
||||
"announce": "Ankündigen",
|
||||
@ -79,23 +75,16 @@
|
||||
"search": "Suchen",
|
||||
"userExpiry": "Benutzer Ablaufdatum",
|
||||
"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.",
|
||||
"disabled": "Deaktiviert",
|
||||
"admin": "Admin",
|
||||
"download": "Herunterladen",
|
||||
"update": "Aktualisieren",
|
||||
"updates": "Aktualisierungen",
|
||||
"expiry": "Ablaufdatum",
|
||||
"extendExpiry": "Ablaufdatum verlängern",
|
||||
"reEnable": "Wieder aktivieren",
|
||||
"disable": "Deaktivieren",
|
||||
"donate": "Spenden",
|
||||
"conditionals": "Bedingungen",
|
||||
"contactThrough": "Kontakt über:",
|
||||
"sendPIN": "Bitte den Benutzer, die unten stehende PIN an den Bot zu senden.",
|
||||
"inviteMonths": "Monate",
|
||||
"add": "Hinzufügen",
|
||||
"select": "Auswählen",
|
||||
"searchDiscordUser": "Gib den Discord-Benutzername ein, um den Benutzer zu finden.",
|
||||
"findDiscordUser": "Suche Discord-Benutzer",
|
||||
@ -122,20 +111,15 @@
|
||||
"createProfile": "Profil {n} erstellt.",
|
||||
"saveSettings": "Einstellungen wurden 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.",
|
||||
"errorHomescreenAppliedNoSettings": "Startbildschirmlayout wurde angewendet, aber die Anwendung der Einstellungen ist möglicherweise 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",
|
||||
"errorDeleteProfile": "Fehler beim Löschen des Profils {n}",
|
||||
"errorLoadProfiles": "Fehler beim Laden der Profile.",
|
||||
"errorCreateProfile": "Fehler beim Erstellen des Profils {n}",
|
||||
"errorSetDefaultProfile": "Fehler beim Setzen des Standardprofils.",
|
||||
"errorLoadUsers": "Fehler beim Laden der Benutzer.",
|
||||
"errorSaveSettings": "Einstellungen konnten nicht gespeichert werden.",
|
||||
"errorLoadSettings": "Fehler beim Laden der Einstellungen.",
|
||||
"errorSetOmbiDefaults": "Fehler beim Speichern der Ombi-Standardeinstellungen.",
|
||||
"errorLoadOmbiUsers": "Fehler beim Laden der Ombi-Benutzer.",
|
||||
|
@ -13,11 +13,8 @@
|
||||
"warning": "Προσοχή",
|
||||
"inviteInfiniteUsesWarning": "μπορεί να γίνει κατάχρηση των προσκλήσεων με άπειρες χρήσεις",
|
||||
"inviteSendToEmail": "Αποστολή σε",
|
||||
"login": "Σύνδεση",
|
||||
"logout": "Αποσύνδεση",
|
||||
"create": "Δημιουργία",
|
||||
"apply": "Εφαρμογή",
|
||||
"delete": "Διαγραφή",
|
||||
"name": "Όνομα",
|
||||
"date": "Ημερομηνία",
|
||||
"lastActiveTime": "Τελευταία Ενεργός",
|
||||
@ -68,7 +65,6 @@
|
||||
"variables": "Μεταβλητές",
|
||||
"preview": "Προεπισκόπηση",
|
||||
"reset": "Επαναφορά",
|
||||
"edit": "Επεξεργασία",
|
||||
"customizeMessages": "Παραμετροποίηση Emails",
|
||||
"advancedSettings": "Προχωρημένες Ρυθμίσεις",
|
||||
"customizeMessagesDescription": "Αν δεν θέλετε να ζρησιμοποιήσετε τα πρότυπα email του jfa-go, μπορείτε να δημιουργήσετε τα δικά σας με χρήση Markdown.",
|
||||
@ -77,10 +73,6 @@
|
||||
"download": "Λήψη",
|
||||
"search": "Αναζήτηση",
|
||||
"inviteDuration": "Διάρκεια Πρόσκλησης",
|
||||
"enabled": "Ενεργοποιημένο",
|
||||
"disabled": "Απενεργοποιημένο",
|
||||
"admin": "Διαχειριστής",
|
||||
"expiry": "Λήξη",
|
||||
"userExpiry": "Λήξη Χρήστη",
|
||||
"userExpiryDescription": "Μετά απο ένα καθορισμένο χρόνο μετά απο κάθε εγγραφή, το jfa-go θα διαγράφει/απενεργοποιεί τον λογαριασμό. Μπορείτε να αλλάξετε αυτή την συμπεριφορά στις ρυθμίσεις.",
|
||||
"announce": "Ανακοίνωση",
|
||||
@ -88,8 +80,6 @@
|
||||
"message": "Μήνυμα",
|
||||
"extendExpiry": "Παράταση λήξης",
|
||||
"markdownSupported": "Το Markdown υποστυρίζεται.",
|
||||
"reEnable": "Επανα-ενεργοποίηση",
|
||||
"disable": "Απενεργοποίηση",
|
||||
"inviteMonths": "Μήνες"
|
||||
},
|
||||
"notifications": {
|
||||
@ -98,20 +88,15 @@
|
||||
"createProfile": "Δημιουργήθηκε το {n} προφίλ.",
|
||||
"saveSettings": "Οι ρυθμίσεις αποθηκεύτηκαν",
|
||||
"setOmbiDefaults": "Αποθηκεύτηκαν οι προκαθορισμένες ρυθμίσεις του ombi.",
|
||||
"errorConnection": "Δεν μπόρεσε να συνδεθεί με το jfa-go.",
|
||||
"error401Unauthorized": "Ανεξουσιοδότητος. Προσπαθήστε να κάνετε επαναφόρτωση την σελίδα.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Οι ρυθμίσεις αποθηκεύτηκαν, αλλά η καταχώρηση δομής αρχικής οθόνης ίσως απέτυχε.",
|
||||
"errorHomescreenAppliedNoSettings": "Η δομή αρχικής οθόνης εφαρμόστηκε, αλλά οι ρυθμίσεις ίσως απέτυχαν.",
|
||||
"errorSettingsFailed": "Η εφαρμογή απέτυχε.",
|
||||
"errorLoginBlank": "Το όνομα χρήστη και/ή ο κωδικός ήταν κενά.",
|
||||
"errorUnknown": "Άγνωστο σφάλμα.",
|
||||
"errorBlankFields": "Τα πεφία ήταν κενά",
|
||||
"errorDeleteProfile": "Αποτυχία διαγραφής του προφίλ {n}",
|
||||
"errorLoadProfiles": "Αποτυχία φόρτωσης των προφίλ.",
|
||||
"errorCreateProfile": "Αποτυχία δημιουργίας του προφίλ {n}",
|
||||
"errorSetDefaultProfile": "Αποτυχία ορισμού του προκαθορισμένου προφίλ.",
|
||||
"errorLoadUsers": "Αποτυχία φόρτωσης χρηστών.",
|
||||
"errorSaveSettings": "Αποτυχία αποθήκευσης ρυθμίσεων.",
|
||||
"errorLoadSettings": "Αποτυχία φόρτωσης ρυθμίσεων.",
|
||||
"errorSetOmbiDefaults": "Αποτυχία αποθήκευσης προκαθορισμένων ρυθμίσεων για το Ombi.",
|
||||
"errorLoadOmbiUsers": "Αποτυχία φόρτωσης χρηστών Ombi.",
|
||||
|
@ -68,12 +68,8 @@
|
||||
"inviteHours": "Hours",
|
||||
"inviteInfiniteUsesWarning": "invites with infinite uses can be used abusively",
|
||||
"inviteSendToEmail": "Send to",
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"apply": "Apply",
|
||||
"delete": "Delete",
|
||||
"updates": "Updates",
|
||||
"expiry": "Expiry",
|
||||
"variables": "Variables",
|
||||
"preview": "Preview",
|
||||
"markdownSupported": "Markdown is supported.",
|
||||
@ -96,16 +92,10 @@
|
||||
"contactThrough": "Contact through:",
|
||||
"select": "Select",
|
||||
"date": "Date",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"disable": "Disable",
|
||||
"edit": "Edit",
|
||||
"extendExpiry": "Extend expiry",
|
||||
"sendPWR": "Send Password Reset",
|
||||
"inviteMonths": "Months",
|
||||
"inviteDuration": "Invite Duration",
|
||||
"add": "Add",
|
||||
"reEnable": "Re-enable",
|
||||
"update": "Update",
|
||||
"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.",
|
||||
@ -150,7 +140,6 @@
|
||||
"settingsApplyRestartLater": "Apply, restart later",
|
||||
"subject": "Subject",
|
||||
"setExpiry": "Set expiry",
|
||||
"admin": "Admin",
|
||||
"download": "Download",
|
||||
"search": "Search",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
@ -175,9 +164,7 @@
|
||||
},
|
||||
"notifications": {
|
||||
"errorSettingsFailed": "Application failed.",
|
||||
"errorLoginBlank": "The username and/or password was left blank.",
|
||||
"errorLoadSettings": "Failed to load settings.",
|
||||
"errorUnknown": "Unknown error.",
|
||||
"errorDeleteProfile": "Failed to delete profile {n}",
|
||||
"sentAnnouncement": "Announcement sent.",
|
||||
"savedAnnouncement": "Announcement saved.",
|
||||
@ -186,13 +173,11 @@
|
||||
"updateAppliedRefresh": "Update applied, please refresh.",
|
||||
"telegramVerified": "Telegram account verified.",
|
||||
"accountConnected": "Account connected.",
|
||||
"error401Unauthorized": "Unauthorised. Try refreshing the page.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
||||
"errorSaveEmail": "Failed to save email.",
|
||||
"errorLoadProfiles": "Failed to load profiles.",
|
||||
"errorCreateProfile": "Failed to create profile {n}",
|
||||
"errorLoadUsers": "Failed to load users.",
|
||||
"errorSaveSettings": "Couldn't save settings.",
|
||||
"errorSetOmbiProfile": "Failed to store ombi profile.",
|
||||
"errorLoadOmbiUsers": "Failed to load ombi users.",
|
||||
"errorFailureCheckLogs": "Failed (check console/logs)",
|
||||
@ -207,7 +192,6 @@
|
||||
"saveEmail": "Email saved.",
|
||||
"createProfile": "Created profile {n}.",
|
||||
"saveSettings": "Settings were saved",
|
||||
"errorConnection": "Couldn't connect to jfa-go.",
|
||||
"errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.",
|
||||
"errorBlankFields": "Fields were left blank",
|
||||
"errorSetDefaultProfile": "Failed to set default profile.",
|
||||
|
@ -15,21 +15,12 @@
|
||||
"warning": "Warning",
|
||||
"inviteInfiniteUsesWarning": "invites with infinite uses can be used abusively",
|
||||
"inviteSendToEmail": "Send to",
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"create": "Create",
|
||||
"apply": "Apply",
|
||||
"delete": "Delete",
|
||||
"add": "Add",
|
||||
"select": "Select",
|
||||
"name": "Name",
|
||||
"date": "Date",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"reEnable": "Re-enable",
|
||||
"setExpiry": "Set expiry",
|
||||
"disable": "Disable",
|
||||
"admin": "Admin",
|
||||
"updates": "Updates",
|
||||
"update": "Update",
|
||||
"download": "Download",
|
||||
@ -40,7 +31,6 @@
|
||||
"after": "After",
|
||||
"before": "Before",
|
||||
"user": "User",
|
||||
"expiry": "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.",
|
||||
"aboutProgram": "About",
|
||||
@ -59,7 +49,6 @@
|
||||
"conditionals": "Conditionals",
|
||||
"preview": "Preview",
|
||||
"reset": "Reset",
|
||||
"edit": "Edit",
|
||||
"donate": "Donate",
|
||||
"unlink": "Unlink Account",
|
||||
"sendPWR": "Send Password Reset",
|
||||
@ -124,7 +113,9 @@
|
||||
"actions": "Actions",
|
||||
"searchOptions": "Search Options",
|
||||
"matchText": "Match Text",
|
||||
"jellyfinID": "Jellyfin ID"
|
||||
"jellyfinID": "Jellyfin ID",
|
||||
"userPageLogin": "User Page: Login",
|
||||
"userPagePage": "User Page: Page"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Changed email address of {n}.",
|
||||
@ -139,13 +130,9 @@
|
||||
"updateAppliedRefresh": "Update applied, please refresh.",
|
||||
"telegramVerified": "Telegram account verified.",
|
||||
"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.",
|
||||
"errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.",
|
||||
"errorSettingsFailed": "Application failed.",
|
||||
"errorLoginBlank": "The username and/or password were left blank.",
|
||||
"errorUnknown": "Unknown error.",
|
||||
"errorSaveEmail": "Failed to save email.",
|
||||
"errorBlankFields": "Fields were left blank",
|
||||
"errorDeleteProfile": "Failed to delete profile {n}",
|
||||
@ -153,7 +140,6 @@
|
||||
"errorCreateProfile": "Failed to create profile {n}",
|
||||
"errorSetDefaultProfile": "Failed to set default profile.",
|
||||
"errorLoadUsers": "Failed to load users.",
|
||||
"errorSaveSettings": "Couldn't save settings.",
|
||||
"errorLoadSettings": "Failed to load settings.",
|
||||
"errorSetOmbiProfile": "Failed to store ombi profile.",
|
||||
"errorLoadOmbiUsers": "Failed to load ombi users.",
|
||||
|
@ -15,18 +15,10 @@
|
||||
"warning": "Advertencia",
|
||||
"inviteInfiniteUsesWarning": "Las invitaciones con usos infinitos se pueden usar de forma abusiva",
|
||||
"inviteSendToEmail": "Enviar a",
|
||||
"login": "Acceso",
|
||||
"logout": "Cerrar sesión",
|
||||
"create": "Crear",
|
||||
"apply": "Aplicar",
|
||||
"delete": "Eliminar",
|
||||
"name": "Nombre",
|
||||
"date": "Fecha",
|
||||
"enabled": "Activado",
|
||||
"disabled": "Desactivado",
|
||||
"reEnable": "Reactivar",
|
||||
"disable": "Desactivar",
|
||||
"admin": "Administrador",
|
||||
"updates": "Actualizaciones",
|
||||
"update": "Actualizar",
|
||||
"download": "Descargar",
|
||||
@ -35,7 +27,6 @@
|
||||
"lastActiveTime": "Último activo",
|
||||
"from": "De",
|
||||
"user": "Usuario",
|
||||
"expiry": "Expiración",
|
||||
"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.",
|
||||
"aboutProgram": "Acerca de",
|
||||
@ -51,7 +42,6 @@
|
||||
"variables": "Variables",
|
||||
"preview": "Vista previa",
|
||||
"reset": "Reiniciar",
|
||||
"edit": "Editar",
|
||||
"extendExpiry": "Extender el vencimiento",
|
||||
"customizeMessages": "Personalizar mensajes",
|
||||
"customizeMessagesDescription": "Si no desea utilizar las plantillas de mensajes de jfa-go, puede crear las suyas con Markdown.",
|
||||
@ -93,7 +83,6 @@
|
||||
"notifyUserCreation": "Sobre la creación de usuarios",
|
||||
"conditionals": "Condicionales",
|
||||
"donate": "Donar",
|
||||
"add": "Agregar",
|
||||
"templates": "Plantillas",
|
||||
"contactThrough": "Contactar a través de:",
|
||||
"select": "Seleccionar",
|
||||
@ -125,13 +114,9 @@
|
||||
"sentAnnouncement": "Anuncio enviado.",
|
||||
"setOmbiDefaults": "Valores predeterminados de ombi almacenados.",
|
||||
"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.",
|
||||
"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ó.",
|
||||
"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.",
|
||||
"errorBlankFields": "Los campos se dejaron en blanco",
|
||||
"errorDeleteProfile": "No se pudo borrar el perfil {n}",
|
||||
@ -139,7 +124,6 @@
|
||||
"errorCreateProfile": "No se pudo crear el perfil {n}",
|
||||
"errorSetDefaultProfile": "No se pudo establecer el perfil predeterminado.",
|
||||
"errorLoadUsers": "No se pudieron cargar los usuarios.",
|
||||
"errorSaveSettings": "No se pudo guardar la configuración.",
|
||||
"errorLoadSettings": "No se pudo cargar la configuración.",
|
||||
"errorSetOmbiDefaults": "No se pudieron almacenar los valores predeterminados de ombi.",
|
||||
"errorLoadOmbiUsers": "No se pudieron cargar los usuarios de Ombi.",
|
||||
|
@ -15,11 +15,8 @@
|
||||
"warning": "Attention",
|
||||
"inviteInfiniteUsesWarning": "les invitations infinies peuvent être utilisées abusivement",
|
||||
"inviteSendToEmail": "Envoyer à",
|
||||
"login": "S'identifier",
|
||||
"logout": "Se déconnecter",
|
||||
"create": "Créer",
|
||||
"apply": "Appliquer",
|
||||
"delete": "Effacer",
|
||||
"name": "Nom",
|
||||
"date": "Date",
|
||||
"lastActiveTime": "Dernière activité",
|
||||
@ -75,15 +72,8 @@
|
||||
"variables": "Variables",
|
||||
"preview": "Aperçu",
|
||||
"reset": "Réinitialisation",
|
||||
"edit": "Éditer",
|
||||
"customizeMessages": "Personnaliser les e-mails",
|
||||
"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",
|
||||
"userExpiry": "Expiration de l'utilisateur",
|
||||
"updates": "Mises à jour",
|
||||
@ -96,7 +86,6 @@
|
||||
"extendExpiry": "Prolonger l'expiration",
|
||||
"contactThrough": "Contacté par :",
|
||||
"sendPIN": "Demandez à l'utilisateur d'envoyer le code PIN ci-dessous au bot.",
|
||||
"add": "Ajouter",
|
||||
"select": "Sélectionner",
|
||||
"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.",
|
||||
@ -123,20 +112,15 @@
|
||||
"createProfile": "Profil créé {n}.",
|
||||
"saveSettings": "Les paramètres ont été enregistrés",
|
||||
"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é.",
|
||||
"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é.",
|
||||
"errorLoginBlank": "Le nom d'utilisateur et/ou le mot de passe sont vides.",
|
||||
"errorUnknown": "Erreur inconnue.",
|
||||
"errorBlankFields": "Les champs sont vides",
|
||||
"errorDeleteProfile": "Échec de la suppression du profil {n}",
|
||||
"errorLoadProfiles": "Échec du chargement des profils.",
|
||||
"errorCreateProfile": "Échec de la création du profil {n}",
|
||||
"errorSetDefaultProfile": "Échec de la définition du profil par défaut.",
|
||||
"errorLoadUsers": "Échec du chargement des utilisateurs.",
|
||||
"errorSaveSettings": "Impossible d'enregistrer les paramètres.",
|
||||
"errorLoadSettings": "Échec du chargement des paramètres.",
|
||||
"errorSetOmbiDefaults": "Impossible de stocker les valeurs par défaut d'Ombi.",
|
||||
"errorLoadOmbiUsers": "Échec du chargement des utilisateurs Ombi.",
|
||||
|
@ -15,21 +15,12 @@
|
||||
"warning": "Figyelmeztetés",
|
||||
"inviteInfiniteUsesWarning": "a végtelen felhasználású meghívókkal visszaélhetnek",
|
||||
"inviteSendToEmail": "Címzett",
|
||||
"login": "Belépés",
|
||||
"logout": "Kijelentkezés",
|
||||
"create": "Létrehozás",
|
||||
"apply": "Alkalmaz",
|
||||
"delete": "Törlés",
|
||||
"add": "Hozzáadás",
|
||||
"select": "Kiválasztás",
|
||||
"name": "Név",
|
||||
"date": "Dátum",
|
||||
"enabled": "Engedélyezve",
|
||||
"disabled": "Tiltva",
|
||||
"reEnable": "Újra engedélyezés",
|
||||
"setExpiry": "Lejárat beállítása",
|
||||
"disable": "Letiltás",
|
||||
"admin": "Adminisztrátor",
|
||||
"updates": "Frissítések",
|
||||
"update": "Frissítés",
|
||||
"download": "Letöltés",
|
||||
@ -38,7 +29,6 @@
|
||||
"lastActiveTime": "Utoljára aktív",
|
||||
"from": "Feladó",
|
||||
"user": "Felhasználó",
|
||||
"expiry": "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.",
|
||||
"aboutProgram": "Névjegy",
|
||||
@ -57,7 +47,6 @@
|
||||
"conditionals": "Feltételek",
|
||||
"preview": "Előnézet",
|
||||
"reset": "Visszaállítás",
|
||||
"edit": "Szerkesztés",
|
||||
"donate": "Támogatás",
|
||||
"sendPWR": "Jelszó visszaállítás küldése",
|
||||
"contactThrough": "",
|
||||
@ -126,13 +115,9 @@
|
||||
"updateAppliedRefresh": "",
|
||||
"telegramVerified": "",
|
||||
"accountConnected": "",
|
||||
"errorConnection": "",
|
||||
"error401Unauthorized": "",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "",
|
||||
"errorHomescreenAppliedNoSettings": "",
|
||||
"errorSettingsFailed": "",
|
||||
"errorLoginBlank": "",
|
||||
"errorUnknown": "",
|
||||
"errorSaveEmail": "",
|
||||
"errorBlankFields": "",
|
||||
"errorDeleteProfile": "",
|
||||
@ -140,7 +125,6 @@
|
||||
"errorCreateProfile": "",
|
||||
"errorSetDefaultProfile": "",
|
||||
"errorLoadUsers": "",
|
||||
"errorSaveSettings": "",
|
||||
"errorLoadSettings": "",
|
||||
"errorSetOmbiProfile": "",
|
||||
"errorLoadOmbiUsers": "",
|
||||
|
@ -13,11 +13,8 @@
|
||||
"warning": "Peringatan",
|
||||
"inviteInfiniteUsesWarning": "Undangan dalam jumlah tak terbatas dapat disalahgunakan",
|
||||
"inviteSendToEmail": "Dikirim kepada",
|
||||
"login": "Masuk",
|
||||
"logout": "Keluar",
|
||||
"create": "Buat",
|
||||
"apply": "Terapkan",
|
||||
"delete": "Hapus",
|
||||
"name": "Nama",
|
||||
"date": "Tanggal",
|
||||
"lastActiveTime": "Terakhir Aktif",
|
||||
@ -68,7 +65,6 @@
|
||||
"variables": "Variabel",
|
||||
"preview": "Pratinjau",
|
||||
"reset": "Setel ulang",
|
||||
"edit": "Edit",
|
||||
"customizeMessages": "Sesuaikan Email",
|
||||
"customizeMessagesDescription": "Jika Anda tidak ingin menggunakan templat email jfa-go, Anda dapat membuatnya sendiri menggunakan Markdown.",
|
||||
"announce": "Mengumumkan",
|
||||
@ -82,20 +78,15 @@
|
||||
"createProfile": "Membuat profil {n}.",
|
||||
"saveSettings": "Pengaturan telah disimpan",
|
||||
"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.",
|
||||
"errorHomescreenAppliedNoSettings": "Tata letak layar beranda diterapkan, tetapi menerapkan pengaturan mungkin gagal.",
|
||||
"errorSettingsFailed": "Aplikasi gagal.",
|
||||
"errorLoginBlank": "Nama pengguna dan / atau sandi kosong.",
|
||||
"errorUnknown": "Kesalahan yang tidak diketahui.",
|
||||
"errorBlankFields": "Isian dibiarkan kosong",
|
||||
"errorDeleteProfile": "Gagal menghapus profil {n}",
|
||||
"errorLoadProfiles": "Gagal memuat profil.",
|
||||
"errorCreateProfile": "Gagal membuat profil {n}",
|
||||
"errorSetDefaultProfile": "Gagal menyetel profil default.",
|
||||
"errorLoadUsers": "Gagal memuat pengguna.",
|
||||
"errorSaveSettings": "Tidak dapat menyimpan pengaturan.",
|
||||
"errorLoadSettings": "Gagal memuat pengaturan.",
|
||||
"errorSetOmbiDefaults": "Gagal menyimpan default ombi.",
|
||||
"errorLoadOmbiUsers": "Gagal memuat pengguna ombi.",
|
||||
|
@ -13,11 +13,8 @@
|
||||
"warning": "Waarschuwing",
|
||||
"inviteInfiniteUsesWarning": "ongelimiteerde uitnodigingen kunnen misbruikt worden",
|
||||
"inviteSendToEmail": "Stuur naar",
|
||||
"login": "Inloggen",
|
||||
"logout": "Uitloggen",
|
||||
"create": "Aanmaken",
|
||||
"apply": "Toepassen",
|
||||
"delete": "Verwijderen",
|
||||
"name": "Naam",
|
||||
"date": "Datum",
|
||||
"lastActiveTime": "Laatst actief",
|
||||
@ -73,14 +70,9 @@
|
||||
"customizeMessagesDescription": "Als je de e-mailsjablonen van jfa-go niet wilt gebruiken, kun je met gebruik van Markdown je eigen aanmaken.",
|
||||
"preview": "Voorbeeld",
|
||||
"reset": "Reset",
|
||||
"edit": "Bewerken",
|
||||
"customizeMessages": "E-mails aanpassen",
|
||||
"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.",
|
||||
"enabled": "Ingeschakeld",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"admin": "Beheerder",
|
||||
"expiry": "Verloop",
|
||||
"userExpiry": "Gebruikersverloop",
|
||||
"extendExpiry": "Verleng verloop",
|
||||
"updates": "Updates",
|
||||
@ -89,13 +81,10 @@
|
||||
"search": "Zoeken",
|
||||
"advancedSettings": "Geavanceerde instellingen",
|
||||
"inviteMonths": "Maanden",
|
||||
"reEnable": "Opnieuw inschakelen",
|
||||
"disable": "Uitschakelen",
|
||||
"conditionals": "Voorwaarden",
|
||||
"donate": "Doneer",
|
||||
"contactThrough": "Stuur bericht via:",
|
||||
"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.",
|
||||
"linkMatrixDescription": "Vul de gebruikersnaam en wachtwoord in van de gebruiker om als bot te gebruiken. De app start zodra ze zijn verstuurd.",
|
||||
"select": "Selecteer",
|
||||
@ -122,20 +111,15 @@
|
||||
"createProfile": "Profiel {n} aangemaakt.",
|
||||
"saveSettings": "De instellingen 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.",
|
||||
"errorHomescreenAppliedNoSettings": "Startpaginaindeling toegepast, maar opslaan van instellingen is misschien mislukt.",
|
||||
"errorSettingsFailed": "Opslaan mislukt.",
|
||||
"errorLoginBlank": "De gebruikersnaam en/of wachtwoord is leeg.",
|
||||
"errorUnknown": "Onbekende fout.",
|
||||
"errorBlankFields": "Velden leeggelaten",
|
||||
"errorDeleteProfile": "Verwijderen van profiel {n} mislukt",
|
||||
"errorLoadProfiles": "Fout bij het laden van profielen.",
|
||||
"errorCreateProfile": "Aanmaken van profile {n} mislukt",
|
||||
"errorSetDefaultProfile": "Fout bij instellen van standaardprofiel.",
|
||||
"errorLoadUsers": "Laden van gebruikers mislukt.",
|
||||
"errorSaveSettings": "Opslaan van instellingen mislukt.",
|
||||
"errorLoadSettings": "Laden van instellingen mislukt.",
|
||||
"errorSetOmbiDefaults": "Opslaan van ombi standaardinstellingen mislukt.",
|
||||
"errorLoadOmbiUsers": "Laden van ombi gebruikers mislukt.",
|
||||
|
@ -15,21 +15,12 @@
|
||||
"warning": "Ostrzeżenie",
|
||||
"inviteInfiniteUsesWarning": "",
|
||||
"inviteSendToEmail": "",
|
||||
"login": "",
|
||||
"logout": "",
|
||||
"create": "",
|
||||
"apply": "",
|
||||
"delete": "",
|
||||
"add": "",
|
||||
"select": "",
|
||||
"name": "Imię",
|
||||
"date": "Data",
|
||||
"enabled": "Włączone",
|
||||
"disabled": "Wyłączone",
|
||||
"reEnable": "",
|
||||
"setExpiry": "",
|
||||
"disable": "Wyłączone",
|
||||
"admin": "Admin",
|
||||
"updates": "Aktualizacje",
|
||||
"update": "Aktualizacja",
|
||||
"download": "Pobierz",
|
||||
@ -38,7 +29,6 @@
|
||||
"lastActiveTime": "Ostatnia aktywność",
|
||||
"from": "Od",
|
||||
"user": "Użytkownik",
|
||||
"expiry": "Wygasa",
|
||||
"userExpiry": "Użytkownik wygasa",
|
||||
"userExpiryDescription": "",
|
||||
"aboutProgram": "O",
|
||||
@ -57,7 +47,6 @@
|
||||
"conditionals": "",
|
||||
"preview": "",
|
||||
"reset": "Zresetuj",
|
||||
"edit": "Edytuj",
|
||||
"donate": "",
|
||||
"sendPWR": "",
|
||||
"contactThrough": "",
|
||||
@ -128,13 +117,9 @@
|
||||
"updateAppliedRefresh": "Aktualizacja zastosowana, odśwież.",
|
||||
"telegramVerified": "Konto telegramu zweryfikowane.",
|
||||
"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ść.",
|
||||
"errorHomescreenAppliedNoSettings": "",
|
||||
"errorSettingsFailed": "",
|
||||
"errorLoginBlank": "",
|
||||
"errorUnknown": "Nieznany błąd.",
|
||||
"errorSaveEmail": "",
|
||||
"errorBlankFields": "",
|
||||
"errorDeleteProfile": "",
|
||||
@ -142,7 +127,6 @@
|
||||
"errorCreateProfile": "",
|
||||
"errorSetDefaultProfile": "",
|
||||
"errorLoadUsers": "",
|
||||
"errorSaveSettings": "",
|
||||
"errorLoadSettings": "",
|
||||
"errorSetOmbiProfile": "",
|
||||
"errorLoadOmbiUsers": "",
|
@ -13,11 +13,8 @@
|
||||
"warning": "Aviso",
|
||||
"inviteInfiniteUsesWarning": "convites infinitos podem ser usados de forma abusiva",
|
||||
"inviteSendToEmail": "Enviar para",
|
||||
"login": "Login",
|
||||
"logout": "Sair",
|
||||
"create": "Criar",
|
||||
"apply": "Aplicar",
|
||||
"delete": "Deletar",
|
||||
"name": "Nome",
|
||||
"date": "Data",
|
||||
"lastActiveTime": "Ativo pela última vez",
|
||||
@ -73,14 +70,9 @@
|
||||
"variables": "Variáveis",
|
||||
"preview": "Pre-visualizar",
|
||||
"reset": "Redefinir",
|
||||
"edit": "Editar",
|
||||
"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.",
|
||||
"inviteDuration": "Duração do Convite",
|
||||
"enabled": "Habilitado",
|
||||
"admin": "Admin",
|
||||
"expiry": "Expira",
|
||||
"userExpiry": "Vencimento do Usuário",
|
||||
"extendExpiry": "Extender o vencimento",
|
||||
"updates": "Atualizações",
|
||||
@ -89,15 +81,12 @@
|
||||
"search": "Procurar",
|
||||
"advancedSettings": "Configurações Avançada",
|
||||
"inviteMonths": "Meses",
|
||||
"reEnable": "Reativar",
|
||||
"disable": "Desativar",
|
||||
"conditionals": "Condicionais",
|
||||
"donate": "Doar",
|
||||
"contactThrough": "Contato através:",
|
||||
"sendPIN": "Peça que o usuário envie o PIN abaixo para o bot.",
|
||||
"searchDiscordUser": "Digite o nome de usuário do 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.",
|
||||
"select": "Selecionar",
|
||||
"templates": "Modelos",
|
||||
@ -122,20 +111,15 @@
|
||||
"createProfile": "Perfil {n} criado.",
|
||||
"saveSettings": "As configurações foram salvas",
|
||||
"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.",
|
||||
"errorHomescreenAppliedNoSettings": "O layout da tela inicial foi aplicado, mas a aplicação das configurações pode ter falhado.",
|
||||
"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",
|
||||
"errorDeleteProfile": "Falha ao excluir perfil {n}",
|
||||
"errorLoadProfiles": "Falha ao carregar perfis.",
|
||||
"errorCreateProfile": "Falha ao criar perfil {n}",
|
||||
"errorSetDefaultProfile": "Falha ao definir o perfil padrão.",
|
||||
"errorLoadUsers": "Falha ao carregar usuários.",
|
||||
"errorSaveSettings": "Não foi possível salvar as configurações.",
|
||||
"errorLoadSettings": "Falha ao carregar as configurações.",
|
||||
"errorSetOmbiDefaults": "Falha em armazenar os padrões ombi.",
|
||||
"errorLoadOmbiUsers": "Falha ao carregar usuários ombi.",
|
||||
|
@ -13,11 +13,8 @@
|
||||
"warning": "Varning",
|
||||
"inviteInfiniteUsesWarning": "inbjudningar med oändligt antal användningar kan missbrukas",
|
||||
"inviteSendToEmail": "Skicka till",
|
||||
"login": "Logga in",
|
||||
"logout": "Logga ut",
|
||||
"create": "Skapa",
|
||||
"apply": "Tillämpa",
|
||||
"delete": "Radera",
|
||||
"name": "Namn",
|
||||
"date": "Datum",
|
||||
"lastActiveTime": "Senast aktiv",
|
||||
@ -36,7 +33,6 @@
|
||||
"variables": "Variabler",
|
||||
"preview": "Förhandsvisning",
|
||||
"reset": "Återställ",
|
||||
"edit": "Redigera",
|
||||
"customizeMessages": "Anpassa e-post",
|
||||
"customizeMessagesDescription": "Om du inte vill använda jfa-go's e-postmallar, så kan du skapa dina egna med Markdown.",
|
||||
"markdownSupported": "Markdown stöds.",
|
||||
@ -75,11 +71,7 @@
|
||||
"notifyEvent": "Meddela den:",
|
||||
"notifyInviteExpiry": "Vid utgång",
|
||||
"notifyUserCreation": "Vid användarskapande",
|
||||
"disabled": "Inaktiverad",
|
||||
"enabled": "Aktiverad",
|
||||
"inviteDuration": "Varaktighet för inbjudan",
|
||||
"admin": "Admin",
|
||||
"expiry": "Löper ut",
|
||||
"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.",
|
||||
"extendExpiry": "Förläng utgång"
|
||||
@ -92,13 +84,9 @@
|
||||
"saveEmail": "E-post sparad.",
|
||||
"sentAnnouncement": "Meddelande skickat.",
|
||||
"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.",
|
||||
"errorHomescreenAppliedNoSettings": "Hemskärmslayout tillämpades, men tillämpningen av inställningar kan ha misslyckats.",
|
||||
"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.",
|
||||
"errorBlankFields": "Fält lämnades tomma",
|
||||
"errorDeleteProfile": "Det gick inte att ta bort profilen {n}",
|
||||
@ -106,7 +94,6 @@
|
||||
"errorCreateProfile": "Det gick inte att skapa profilen {n}",
|
||||
"errorSetDefaultProfile": "Det gick inte att ange standardprofil.",
|
||||
"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.",
|
||||
"errorSetOmbiDefaults": "Det gick inte att lagra ombi-standardvärden.",
|
||||
"errorLoadOmbiUsers": "Det gick inte att ladda ombi-användare.",
|
||||
|
@ -15,21 +15,12 @@
|
||||
"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",
|
||||
"inviteSendToEmail": "Gửi tới",
|
||||
"login": "Đăng nhập",
|
||||
"logout": "Đăng xuất",
|
||||
"create": "Tạo mới",
|
||||
"apply": "Áp dụng",
|
||||
"delete": "Xóa",
|
||||
"add": "Thêm",
|
||||
"select": "Chọn",
|
||||
"name": "Tên",
|
||||
"date": "Ngày",
|
||||
"enabled": "Mở",
|
||||
"disabled": "Tắt",
|
||||
"reEnable": "Mở lại",
|
||||
"setExpiry": "Đặt hết hạn",
|
||||
"disable": "Tắt",
|
||||
"admin": "Admin",
|
||||
"updates": "Cập nhật",
|
||||
"update": "Cập nhật",
|
||||
"download": "Tải về",
|
||||
@ -38,7 +29,6 @@
|
||||
"lastActiveTime": "Lần cuối Hoạt động",
|
||||
"from": "Từ",
|
||||
"user": "Người dùng",
|
||||
"expiry": "Hết hạn",
|
||||
"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.",
|
||||
"aboutProgram": "Thông tin",
|
||||
@ -56,7 +46,6 @@
|
||||
"conditionals": "Điều kiện",
|
||||
"preview": "Xem trước",
|
||||
"reset": "Đặt lại",
|
||||
"edit": "Chỉnh sửa",
|
||||
"donate": "Đóng góp",
|
||||
"sendPWR": "Gửi Đặt lại Mật khẩu",
|
||||
"contactThrough": "Liên lạc qua:",
|
||||
@ -127,13 +116,9 @@
|
||||
"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.",
|
||||
"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.",
|
||||
"errorHomescreenAppliedNoSettings": "",
|
||||
"errorSettingsFailed": "",
|
||||
"errorLoginBlank": "",
|
||||
"errorUnknown": "",
|
||||
"errorSaveEmail": "",
|
||||
"errorBlankFields": "",
|
||||
"errorDeleteProfile": "",
|
||||
@ -141,7 +126,6 @@
|
||||
"errorCreateProfile": "",
|
||||
"errorSetDefaultProfile": "",
|
||||
"errorLoadUsers": "",
|
||||
"errorSaveSettings": "",
|
||||
"errorLoadSettings": "",
|
||||
"errorSetOmbiProfile": "",
|
||||
"errorLoadOmbiUsers": "",
|
||||
|
@ -15,20 +15,11 @@
|
||||
"warning": "警告",
|
||||
"inviteInfiniteUsesWarning": "无限使用次数的邀请码可能被滥用",
|
||||
"inviteSendToEmail": "发送到",
|
||||
"login": "登录",
|
||||
"logout": "登出",
|
||||
"create": "创建",
|
||||
"apply": "申请",
|
||||
"delete": "删除",
|
||||
"add": "添加",
|
||||
"select": "选择",
|
||||
"name": "名称",
|
||||
"date": "日期",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"reEnable": "重新启用",
|
||||
"disable": "禁用",
|
||||
"admin": "管理员",
|
||||
"updates": "更新",
|
||||
"update": "更新",
|
||||
"download": "下载",
|
||||
@ -37,7 +28,6 @@
|
||||
"lastActiveTime": "上次活动",
|
||||
"from": "从",
|
||||
"user": "用户",
|
||||
"expiry": "到期",
|
||||
"userExpiry": "用户到期",
|
||||
"userExpiryDescription": "每次注册后的指定时间,jfa-go 将删除/禁用该帐户。您可以在设置中更改此行为。",
|
||||
"aboutProgram": "关于",
|
||||
@ -55,7 +45,6 @@
|
||||
"conditionals": "条件性条款",
|
||||
"preview": "预览",
|
||||
"reset": "重设",
|
||||
"edit": "编辑",
|
||||
"donate": "捐助",
|
||||
"contactThrough": "联系方式:",
|
||||
"extendExpiry": "延长有效期",
|
||||
@ -129,13 +118,9 @@
|
||||
"updateAppliedRefresh": "已应用更新,请刷新。",
|
||||
"telegramVerified": "Telegram账户已验证。",
|
||||
"accountConnected": "帐户已连接。",
|
||||
"errorConnection": "无法连接到 jfa-go。",
|
||||
"error401Unauthorized": "无授权。尝试刷新页面。",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "已应用设置,但应用主屏幕布局可能失败。",
|
||||
"errorHomescreenAppliedNoSettings": "已应用主屏幕布局,但应用设置可能失败。",
|
||||
"errorSettingsFailed": "应用失败。",
|
||||
"errorLoginBlank": "用户名/密码留空。",
|
||||
"errorUnknown": "未知错误。",
|
||||
"errorSaveEmail": "电子邮箱保存失败。",
|
||||
"errorBlankFields": "字段留空",
|
||||
"errorDeleteProfile": "删除配置文件{n}失败",
|
||||
@ -143,7 +128,6 @@
|
||||
"errorCreateProfile": "创建配置文件{n}失败",
|
||||
"errorSetDefaultProfile": "设置默认配置文件失败。",
|
||||
"errorLoadUsers": "加载用户列表失败。",
|
||||
"errorSaveSettings": "无法保存设置。",
|
||||
"errorLoadSettings": "加载配置列表失败。",
|
||||
"errorSetOmbiDefaults": "存储Ombi默认值失败。",
|
||||
"errorLoadOmbiUsers": "加载ombi用户列表失败。",
|
||||
|
@ -15,21 +15,12 @@
|
||||
"warning": "警告",
|
||||
"inviteInfiniteUsesWarning": "無限使用次數的邀請碼可能被濫用",
|
||||
"inviteSendToEmail": "發送到",
|
||||
"login": "登錄",
|
||||
"logout": "登出",
|
||||
"create": "創建",
|
||||
"apply": "應用",
|
||||
"delete": "刪除",
|
||||
"add": "添加",
|
||||
"select": "選擇",
|
||||
"name": "帳戶名稱",
|
||||
"date": "日期",
|
||||
"enabled": "已啟用",
|
||||
"disabled": "已禁用",
|
||||
"reEnable": "重新啟用",
|
||||
"setExpiry": "設置到期時間",
|
||||
"disable": "禁用",
|
||||
"admin": "管理員",
|
||||
"updates": "更新",
|
||||
"update": "更新",
|
||||
"download": "下載",
|
||||
@ -38,7 +29,6 @@
|
||||
"lastActiveTime": "上次啟用時間",
|
||||
"from": "從",
|
||||
"user": "帳戶",
|
||||
"expiry": "到期",
|
||||
"userExpiry": "帳戶到期",
|
||||
"userExpiryDescription": "每次註冊后指定的時間,jfa-go 將刪除/禁用該帳戶。您可以在設定中更改此行為。",
|
||||
"aboutProgram": "關於",
|
||||
@ -57,7 +47,6 @@
|
||||
"conditionals": "條件",
|
||||
"preview": "預覽",
|
||||
"reset": "重設",
|
||||
"edit": "編輯",
|
||||
"donate": "捐贈",
|
||||
"sendPWR": "發送密碼重置",
|
||||
"contactThrough": "聯繫方式:",
|
||||
@ -128,13 +117,9 @@
|
||||
"updateAppliedRefresh": "更新已應用,請重新整理。",
|
||||
"telegramVerified": "Telegram 帳戶已驗證。",
|
||||
"accountConnected": "帳戶已連接。",
|
||||
"errorConnection": "無法連接到 jfa-go。",
|
||||
"error401Unauthorized": "未經授權。嘗試重新整理頁面。",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "已應用設置,但應用主螢幕佈局可能失敗。",
|
||||
"errorHomescreenAppliedNoSettings": "已應用主螢幕佈局,但應用設置可能失敗。",
|
||||
"errorSettingsFailed": "應用失敗。",
|
||||
"errorLoginBlank": "帳戶名稱和/或密碼留空。",
|
||||
"errorUnknown": "未知的錯誤。",
|
||||
"errorSaveEmail": "無法儲存電子郵件。",
|
||||
"errorBlankFields": "欄位留空",
|
||||
"errorDeleteProfile": "無法刪除設置文件 {n}",
|
||||
@ -142,7 +127,6 @@
|
||||
"errorCreateProfile": "無法創建設置文件 {n}",
|
||||
"errorSetDefaultProfile": "無法設置預設設置文件。",
|
||||
"errorLoadUsers": "無法讀取帳戶。",
|
||||
"errorSaveSettings": "無法儲存設置。",
|
||||
"errorLoadSettings": "無法讀取設置。",
|
||||
"errorSetOmbiProfile": "無法儲存 ombi 設置文件。",
|
||||
"errorLoadOmbiUsers": "無法讀取 ombi 帳戶。",
|
8
lang/common/ar-aa.json
Normal file
8
lang/common/ar-aa.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "العربية (AR)"
|
||||
},
|
||||
"strings": {},
|
||||
"notifications": {},
|
||||
"quantityStrings": {}
|
||||
}
|
@ -24,6 +24,25 @@
|
||||
"contactDiscord": "Kontakt gennem Discord",
|
||||
"theme": "Tema",
|
||||
"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,26 +4,45 @@
|
||||
},
|
||||
"strings": {
|
||||
"username": "Benutzername",
|
||||
"name": "Name",
|
||||
"password": "Passwort",
|
||||
"emailAddress": "E-Mail Adresse",
|
||||
"name": "Name",
|
||||
"submit": "Absenden",
|
||||
"send": "Senden",
|
||||
"success": "Erfolgreich",
|
||||
"continue": "Weiter",
|
||||
"error": "Fehler",
|
||||
"copy": "Kopieren",
|
||||
"theme": "Thema",
|
||||
"copied": "Kopiert",
|
||||
"time24h": "24h-Format",
|
||||
"time12h": "12h-Format",
|
||||
"copied": "Kopiert",
|
||||
"linkTelegram": "Link Telegram",
|
||||
"contactEmail": "Kontakt über E-Mail",
|
||||
"contactTelegram": "Kontakt über Telegram",
|
||||
"linkDiscord": "Link Discord",
|
||||
"linkMatrix": "Link Matrix",
|
||||
"send": "Senden",
|
||||
"contactDiscord": "Kontakt über Discord",
|
||||
"theme": "Thema",
|
||||
"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,9 +12,27 @@
|
||||
"continue": "Συνέχεια",
|
||||
"error": "Σφάλμα",
|
||||
"copy": "Αντιγραφή",
|
||||
"theme": "Θέμα",
|
||||
"copied": "Αντιγράφηκε",
|
||||
"time24h": "24 Ώρες",
|
||||
"time12h": "12 Ώρες",
|
||||
"copied": "Αντιγράφηκε"
|
||||
}
|
||||
"theme": "Θέμα",
|
||||
"login": "Σύνδεση",
|
||||
"logout": "Αποσύνδεση",
|
||||
"admin": "Διαχειριστής",
|
||||
"enabled": "Ενεργοποιημένο",
|
||||
"disabled": "Απενεργοποιημένο",
|
||||
"reEnable": "Επανα-ενεργοποίηση",
|
||||
"disable": "Απενεργοποίηση",
|
||||
"expiry": "Λήξη",
|
||||
"edit": "Επεξεργασία",
|
||||
"delete": "Διαγραφή"
|
||||
},
|
||||
"notifications": {
|
||||
"errorLoginBlank": "Το όνομα χρήστη και/ή ο κωδικός ήταν κενά.",
|
||||
"errorConnection": "Δεν μπόρεσε να συνδεθεί με το jfa-go.",
|
||||
"errorUnknown": "Άγνωστο σφάλμα.",
|
||||
"error401Unauthorized": "Ανεξουσιοδότητος. Προσπαθήστε να κάνετε επαναφόρτωση την σελίδα.",
|
||||
"errorSaveSettings": "Αποτυχία αποθήκευσης ρυθμίσεων."
|
||||
},
|
||||
"quantityStrings": {}
|
||||
}
|
@ -3,27 +3,46 @@
|
||||
"name": "English (GB)"
|
||||
},
|
||||
"strings": {
|
||||
"continue": "Continue",
|
||||
"time24h": "24h Time",
|
||||
"linkTelegram": "Link Telegram",
|
||||
"send": "Send",
|
||||
"linkDiscord": "Link Discord",
|
||||
"linkMatrix": "Link Matrix",
|
||||
"contactDiscord": "Contact through Discord",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"emailAddress": "Email Address",
|
||||
"name": "Name",
|
||||
"submit": "Submit",
|
||||
"send": "Send",
|
||||
"success": "Success",
|
||||
"continue": "Continue",
|
||||
"error": "Error",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"submit": "Submit",
|
||||
"success": "Success",
|
||||
"error": "Error",
|
||||
"time24h": "24h Time",
|
||||
"time12h": "12h Time",
|
||||
"theme": "Theme",
|
||||
"linkTelegram": "Link Telegram",
|
||||
"contactEmail": "Contact through Email",
|
||||
"contactTelegram": "Contact through Telegram",
|
||||
"name": "Name",
|
||||
"linkDiscord": "Link Discord",
|
||||
"linkMatrix": "Link Matrix",
|
||||
"contactDiscord": "Contact through Discord",
|
||||
"theme": "Theme",
|
||||
"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,6 +24,42 @@
|
||||
"contactDiscord": "Contact through Discord",
|
||||
"theme": "Theme",
|
||||
"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,6 +8,7 @@
|
||||
"emailAddress": "Correo electrónico",
|
||||
"name": "Nombre",
|
||||
"submit": "Enviar",
|
||||
"send": "Enviar",
|
||||
"success": "Éxito",
|
||||
"continue": "Continuar",
|
||||
"error": "Error",
|
||||
@ -15,15 +16,33 @@
|
||||
"copied": "Copiado",
|
||||
"time24h": "Formato de 24 horas",
|
||||
"time12h": "Formato de 12 horas",
|
||||
"theme": "Tema",
|
||||
"send": "Enviar",
|
||||
"contactDiscord": "Contactar por Discord",
|
||||
"linkTelegram": "Enlace Telegram",
|
||||
"contactEmail": "Contactar por correo electrónico",
|
||||
"contactTelegram": "Contactar por Telegram",
|
||||
"linkMatrix": "Enlace Matrix",
|
||||
"linkDiscord": "Enlace Discord",
|
||||
"linkTelegram": "Enlace Telegram",
|
||||
"linkMatrix": "Enlace Matrix",
|
||||
"contactDiscord": "Contactar por Discord",
|
||||
"theme": "Tema",
|
||||
"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,5 +23,7 @@
|
||||
"linkMatrix": "پیوند ماتریکس",
|
||||
"contactDiscord": "از طریق دیسکورد تماس بگیرید",
|
||||
"theme": "موضوع"
|
||||
}
|
||||
},
|
||||
"notifications": {},
|
||||
"quantityStrings": {}
|
||||
}
|
@ -1,30 +1,48 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Français (FR)",
|
||||
"author": "https://github.com/Killianbe"
|
||||
"name": "Français (FR)"
|
||||
},
|
||||
"strings": {
|
||||
"username": "Nom d'utilisateur",
|
||||
"name": "Nom",
|
||||
"password": "Mot de passe",
|
||||
"emailAddress": "Adresse e-mail",
|
||||
"name": "Nom",
|
||||
"submit": "Soumettre",
|
||||
"send": "Envoyer",
|
||||
"success": "Succès",
|
||||
"continue": "Continuer",
|
||||
"error": "Erreur",
|
||||
"copy": "Copier",
|
||||
"copied": "Copié",
|
||||
"time24h": "Temps 24h",
|
||||
"time12h": "Temps 12h",
|
||||
"theme": "Thème",
|
||||
"copied": "Copié",
|
||||
"linkTelegram": "Lien Telegram",
|
||||
"contactEmail": "Contact par e-mail",
|
||||
"contactTelegram": "Contact par Telegram",
|
||||
"linkDiscord": "Lier Discord",
|
||||
"linkMatrix": "Lier Matrix",
|
||||
"send": "Envoyer",
|
||||
"contactDiscord": "Contacter par Discord",
|
||||
"theme": "Thème",
|
||||
"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": {}
|
||||
}
|
20
lang/common/hu-hu.json
Normal file
20
lang/common/hu-hu.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"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,6 +8,7 @@
|
||||
"emailAddress": "Alamat Email",
|
||||
"name": "Nama",
|
||||
"submit": "Submit",
|
||||
"send": "Kirim",
|
||||
"success": "Sukses",
|
||||
"continue": "Lanjut",
|
||||
"error": "Error",
|
||||
@ -15,6 +16,17 @@
|
||||
"time24h": "Waktu 24 jam",
|
||||
"time12h": "Waktu 12 jam",
|
||||
"theme": "Tema",
|
||||
"send": "Kirim"
|
||||
}
|
||||
"login": "Masuk",
|
||||
"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,5 +25,7 @@
|
||||
"theme": "Tema",
|
||||
"refresh": "Aggiorna",
|
||||
"required": "Richiesto"
|
||||
}
|
||||
},
|
||||
"notifications": {},
|
||||
"quantityStrings": {}
|
||||
}
|
@ -4,26 +4,45 @@
|
||||
},
|
||||
"strings": {
|
||||
"username": "Gebruikersnaam",
|
||||
"name": "Naam",
|
||||
"password": "Wachtwoord",
|
||||
"emailAddress": "E-mailadres",
|
||||
"name": "Naam",
|
||||
"submit": "Verstuur",
|
||||
"send": "Verstuur",
|
||||
"success": "Succes",
|
||||
"continue": "Doorgaan",
|
||||
"error": "Fout",
|
||||
"copy": "Kopiëer",
|
||||
"theme": "Thema",
|
||||
"copied": "Gekopieerd",
|
||||
"time24h": "24u-formaat",
|
||||
"time12h": "12u-formaat",
|
||||
"copied": "Gekopieerd",
|
||||
"linkTelegram": "Koppel Telegram",
|
||||
"contactEmail": "Stuur e-mailbericht",
|
||||
"contactTelegram": "Stuur Telegram-bericht",
|
||||
"send": "Verstuur",
|
||||
"linkDiscord": "Koppel Discord",
|
||||
"linkMatrix": "Koppel Matrix",
|
||||
"contactDiscord": "Stuur Discord bericht",
|
||||
"theme": "Thema",
|
||||
"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,6 +24,18 @@
|
||||
"contactDiscord": "Kontakt przez Discord",
|
||||
"theme": "Motyw",
|
||||
"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,26 +4,45 @@
|
||||
},
|
||||
"strings": {
|
||||
"username": "Nome do Usuário",
|
||||
"name": "Nome",
|
||||
"password": "Senha",
|
||||
"emailAddress": "Endereço de e-mail",
|
||||
"name": "Nome",
|
||||
"submit": "Enviar",
|
||||
"send": "Enviar",
|
||||
"success": "Sucesso",
|
||||
"continue": "Continuar",
|
||||
"error": "Erro",
|
||||
"copy": "Copiar",
|
||||
"theme": "Tema",
|
||||
"copied": "Copiado",
|
||||
"time24h": "Horário 24h",
|
||||
"time12h": "Horário 12h",
|
||||
"copied": "Copiado",
|
||||
"linkTelegram": "Link do Telegram",
|
||||
"contactEmail": "Contato por Email",
|
||||
"contactTelegram": "Contato pelo Telegram",
|
||||
"send": "Enviar",
|
||||
"linkDiscord": "Link do Discord",
|
||||
"linkMatrix": "Link do Matrix",
|
||||
"contactDiscord": "Contato através do Discord",
|
||||
"theme": "Tema",
|
||||
"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": {}
|
||||
}
|
8
lang/common/ro-ro.json
Normal file
8
lang/common/ro-ro.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Română (ROU)"
|
||||
},
|
||||
"strings": {},
|
||||
"notifications": {},
|
||||
"quantityStrings": {}
|
||||
}
|
@ -25,5 +25,7 @@
|
||||
"theme": "Tema",
|
||||
"refresh": "Osveži",
|
||||
"required": "Obvezno"
|
||||
}
|
||||
},
|
||||
"notifications": {},
|
||||
"quantityStrings": {}
|
||||
}
|
@ -14,6 +14,22 @@
|
||||
"copy": "Kopiera",
|
||||
"time24h": "24 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": {}
|
||||
}
|
23
lang/common/vi-vn.json
Normal file
23
lang/common/vi-vn.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"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,6 +24,25 @@
|
||||
"contactDiscord": "通过Discord联系",
|
||||
"theme": "主题",
|
||||
"refresh": "刷新",
|
||||
"required": "必需的"
|
||||
}
|
||||
"required": "必需的",
|
||||
"login": "登录",
|
||||
"logout": "登出",
|
||||
"admin": "管理员",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"reEnable": "重新启用",
|
||||
"disable": "禁用",
|
||||
"expiry": "到期",
|
||||
"add": "添加",
|
||||
"edit": "编辑",
|
||||
"delete": "删除"
|
||||
},
|
||||
"notifications": {
|
||||
"errorLoginBlank": "用户名/密码留空。",
|
||||
"errorConnection": "无法连接到 jfa-go。",
|
||||
"errorUnknown": "未知错误。",
|
||||
"error401Unauthorized": "无授权。尝试刷新页面。",
|
||||
"errorSaveSettings": "无法保存设置。"
|
||||
},
|
||||
"quantityStrings": {}
|
||||
}
|
@ -24,6 +24,25 @@
|
||||
"contactDiscord": "通過 Discord 聯繫",
|
||||
"theme": "主題",
|
||||
"refresh": "重新整理",
|
||||
"required": "必填"
|
||||
}
|
||||
"required": "必填",
|
||||
"login": "登錄",
|
||||
"logout": "登出",
|
||||
"admin": "管理員",
|
||||
"enabled": "已啟用",
|
||||
"disabled": "已禁用",
|
||||
"reEnable": "重新啟用",
|
||||
"disable": "禁用",
|
||||
"expiry": "到期",
|
||||
"add": "添加",
|
||||
"edit": "編輯",
|
||||
"delete": "刪除"
|
||||
},
|
||||
"notifications": {
|
||||
"errorLoginBlank": "帳戶名稱和/或密碼留空。",
|
||||
"errorConnection": "無法連接到 jfa-go。",
|
||||
"errorUnknown": "未知的錯誤。",
|
||||
"error401Unauthorized": "未經授權。嘗試重新整理頁面。",
|
||||
"errorSaveSettings": "無法儲存設置。"
|
||||
},
|
||||
"quantityStrings": {}
|
||||
}
|
@ -8,6 +8,8 @@
|
||||
"accountDetails": "Details",
|
||||
"emailAddress": "Email",
|
||||
"username": "Username",
|
||||
"oldPassword": "Old Password",
|
||||
"newPassword": "New Password",
|
||||
"password": "Password",
|
||||
"reEnterPassword": "Re-enter Password",
|
||||
"reEnterPasswordInvalid": "Passwords are not the same.",
|
||||
@ -19,7 +21,20 @@
|
||||
"yourAccountIsValidUntil": "Your account will be valid until {date}.",
|
||||
"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.",
|
||||
"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": {
|
||||
"errorUserExists": "User already exists.",
|
||||
@ -35,6 +50,8 @@
|
||||
"errorCaptcha": "Captcha incorrect.",
|
||||
"errorPassword": "Check password requirements.",
|
||||
"errorNoMatch": "Passwords don't match.",
|
||||
"errorOldPassword": "Old password incorrect.",
|
||||
"passwordChanged": "Password Changed.",
|
||||
"verified": "Account verified."
|
||||
},
|
||||
"validationStrings": {
|
||||
|
15
main.go
15
main.go
@ -17,6 +17,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@ -87,7 +88,7 @@ type appContext struct {
|
||||
webFS httpFS
|
||||
cssClass string // Default theme, "light"|"dark".
|
||||
jellyfinLogin bool
|
||||
users []User
|
||||
adminUsers []User
|
||||
invalidTokens []string
|
||||
// Keeping jf name because I can't think of a better one
|
||||
jf *mediabrowser.MediaBrowser
|
||||
@ -111,6 +112,8 @@ type appContext struct {
|
||||
tag Tag
|
||||
update Update
|
||||
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) {
|
||||
@ -284,7 +287,7 @@ func start(asDaemon, firstCall bool) {
|
||||
}
|
||||
|
||||
app.storage.lang.CommonPath = "common"
|
||||
app.storage.lang.FormPath = "form"
|
||||
app.storage.lang.UserPath = "form"
|
||||
app.storage.lang.AdminPath = "admin"
|
||||
app.storage.lang.EmailPath = "email"
|
||||
app.storage.lang.TelegramPath = "telegram"
|
||||
@ -450,7 +453,7 @@ func start(asDaemon, firstCall bool) {
|
||||
user.UserID = shortuuid.New()
|
||||
user.Username = app.config.Section("ui").Key("username").String()
|
||||
user.Password = app.config.Section("ui").Key("password").String()
|
||||
app.users = append(app.users, user)
|
||||
app.adminUsers = append(app.adminUsers, user)
|
||||
} else {
|
||||
app.debug.Println("Using Jellyfin for authentication")
|
||||
app.authJf, _ = mediabrowser.NewServer(serverType, server, "jfa-go", app.version, "auth", "auth", timeoutHandler, cacheTimeout)
|
||||
@ -645,9 +648,15 @@ func flagPassed(name string) (found bool) {
|
||||
// @securityDefinitions.basic getTokenAuth
|
||||
// @name getTokenAuth
|
||||
|
||||
// @securityDefinitions.basic getUserTokenAuth
|
||||
// @name getUserTokenAuth
|
||||
|
||||
// @tag.name Auth
|
||||
// @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.description Jellyfin user related operations.
|
||||
|
||||
|
11
matrix.go
11
matrix.go
@ -83,7 +83,7 @@ func newMatrixDaemon(app *appContext) (d *MatrixDaemon, err error) {
|
||||
// return
|
||||
// }
|
||||
// d.bot.Store.SaveFilterID(d.userID, resp.FilterID)
|
||||
for _, user := range app.storage.matrix {
|
||||
for _, user := range app.storage.GetMatrix() {
|
||||
if user.Lang != "" {
|
||||
d.languages[id.RoomID(user.RoomID)] = user.Lang
|
||||
}
|
||||
@ -176,12 +176,9 @@ func (d *MatrixDaemon) commandLang(evt *event.Event, code, lang string) {
|
||||
return
|
||||
}
|
||||
d.languages[evt.RoomID] = code
|
||||
if u, ok := d.app.storage.matrix[string(evt.RoomID)]; ok {
|
||||
if u, ok := d.app.storage.GetMatrixKey(string(evt.RoomID)); ok {
|
||||
u.Lang = code
|
||||
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)
|
||||
}
|
||||
d.app.storage.SetMatrixKey(string(evt.RoomID), u)
|
||||
}
|
||||
}
|
||||
|
||||
@ -252,7 +249,7 @@ func (d *MatrixDaemon) Send(message *Message, users ...MatrixUser) (err error) {
|
||||
md := ""
|
||||
if message.Markdown != "" {
|
||||
// Convert images to links
|
||||
md = string(markdown.ToHTML([]byte(strings.ReplaceAll(message.Markdown, "![", "[")), nil, renderer))
|
||||
md = string(markdown.ToHTML([]byte(strings.ReplaceAll(message.Markdown, "![", "[")), nil, markdownRenderer))
|
||||
}
|
||||
content := &event.MessageEventContent{
|
||||
MsgType: "m.text",
|
||||
|
@ -139,7 +139,7 @@ func migrateNotificationMethods(app *appContext) error {
|
||||
if !strings.Contains(address, "@") {
|
||||
continue
|
||||
}
|
||||
for id, email := range app.storage.emails {
|
||||
for id, email := range app.storage.GetEmails() {
|
||||
if email.Addr == address {
|
||||
invite.Notify[id] = notifyPrefs
|
||||
delete(invite.Notify, address)
|
||||
@ -168,10 +168,10 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
|
||||
return nil
|
||||
}
|
||||
idList := map[string][2]string{}
|
||||
for jfID, user := range app.storage.discord {
|
||||
for jfID, user := range app.storage.GetDiscord() {
|
||||
idList[jfID] = [2]string{user.ID, ""}
|
||||
}
|
||||
for jfID, user := range app.storage.telegram {
|
||||
for jfID, user := range app.storage.GetTelegram() {
|
||||
vals, ok := idList[jfID]
|
||||
if !ok {
|
||||
vals = [2]string{"", ""}
|
||||
@ -212,7 +212,7 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
|
||||
// app.jf.GetUsers(false)
|
||||
//
|
||||
// noHyphens := true
|
||||
// for id := range app.storage.emails {
|
||||
// for id := range app.storage.GetEmails() {
|
||||
// if strings.Contains(id, "-") {
|
||||
// noHyphens = false
|
||||
// break
|
||||
|
40
models.go
40
models.go
@ -215,6 +215,7 @@ type setting struct {
|
||||
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.
|
||||
DependsFalse string `json:"depends_false,omitempty"` // If specified, opposite behaviour of DependsTrue.
|
||||
Style string `json:"style,omitempty"`
|
||||
}
|
||||
|
||||
type section struct {
|
||||
@ -373,3 +374,42 @@ type ReCaptchaResponseDTO struct {
|
||||
Hostname string `json:"hostname"`
|
||||
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",
|
||||
"browserslist": "^4.21.7",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"esbuild": "^0.18.3",
|
||||
"esbuild": "^0.18.6",
|
||||
"fs-cheerio": "^3.0.0",
|
||||
"inline-source": "^8.0.2",
|
||||
"jsdom": "^22.1.0",
|
||||
@ -57,9 +57,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.3.tgz",
|
||||
"integrity": "sha512-QOn3VIlL6Qv1eHBpQB/s7simaZgGss2ASyxDOwYSLmc6vD0uuizZkuYawHmuLjWEm5wPwp0JQWhbpaYwwGevYw==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.6.tgz",
|
||||
"integrity": "sha512-J3lwhDSXBBppSzm/LC1uZ8yKSIpExc+5T8MxrYD9KNVZG81FOAu2VF2gXi/6A/LwDDQQ+b6DpQbYlo3VwxFepQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -72,9 +72,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.3.tgz",
|
||||
"integrity": "sha512-PgabCsoaEEnnOiF6rUhOBXgYoLFIrHWP6mfLOzuQ1oZ1lwBdTL0hp5ivC4K3Kvz3BD8EipjeQo6l0aty3nr4qQ==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.6.tgz",
|
||||
"integrity": "sha512-pL0Ci8P9q1sWbtPx8CXbc8JvPvvYdJJQ+LO09PLFsbz3aYNdFBGWJjiHU+CaObO4Ames+GOFpXRAJZS2L3ZK/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -87,9 +87,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.3.tgz",
|
||||
"integrity": "sha512-1OkJf8wNX1W5ucbp5HrK+z42b9DINb4ix59oJH/PIsh9cyFMqjgRKtCBXg0zEWhkmP1k3egdfrnS7cDTpLH43g==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.6.tgz",
|
||||
"integrity": "sha512-hE2vZxOlJ05aY28lUpB0y0RokngtZtcUB+TVl9vnLEnY0z/8BicSvrkThg5/iI1rbf8TwXrbr2heEjl9fLf+EA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -102,9 +102,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.3.tgz",
|
||||
"integrity": "sha512-57aofORpY7wDAuMs6DeqpmgSnVfZ63RgGbR/BHdOSTqJgYvHDCMY7/o1myFntl3k0YxtLE3WAm56nMf4qy3UDw==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.6.tgz",
|
||||
"integrity": "sha512-/tuyl4R+QhhoROQtuQj9E/yfJtZNdv2HKaHwYhhHGQDN1Teziem2Kh7BWQMumfiY7Lu9g5rO7scWdGE4OsQ6MQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -117,9 +117,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.3.tgz",
|
||||
"integrity": "sha512-NVBqMnxT9qvgu7Z322LUDlwjh4GDk6wEePyAQnHF9noxik/WvLFmr5v3Vgz5LSvqFducLCxsdmLztKhdpFW0Gg==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.6.tgz",
|
||||
"integrity": "sha512-L7IQga2pDT+14Ti8HZwsVfbCjuKP4U213T3tuPggOzyK/p4KaUJxQFXJgfUFHKzU0zOXx8QcYRYZf0hSQtppkw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -132,9 +132,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.3.tgz",
|
||||
"integrity": "sha512-XiLK1AsCk2wKxN7j8h9GXXCs8FPZhp07U0rnpwRkAVSVGgLaIWYSqpTRzKjAfqJiZlp+XKo1HwsmDdICEKB3Dg==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.6.tgz",
|
||||
"integrity": "sha512-bq10jFv42V20Kk77NvmO+WEZaLHBKuXcvEowixnBOMkaBgS7kQaqTc77ZJDbsUpXU3KKNLQFZctfaeINmeTsZA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -147,9 +147,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.3.tgz",
|
||||
"integrity": "sha512-xyITfrF0G3l1gwR79hvNCCWKQ/16uK14xNNPFgzjbIqF4EpBvhO6l3jrWxXFUW51z6dVIl2Szh3x3uIbBWzH1Q==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.6.tgz",
|
||||
"integrity": "sha512-HbDLlkDZqUMBQaiday0pJzB6/8Xx/10dI3xRebJBReOEeDSeS+7GzTtW9h8ZnfB7/wBCqvtAjGtWQLTNPbR2+g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -162,9 +162,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.3.tgz",
|
||||
"integrity": "sha512-fc/T0QHMzvmnlF+kfD6bHLB8u+17gg13260p/E86yYjVoKNFjonL/+Y0GGQjMbFUas9QijqOa7pcR00a9RNkwg==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.6.tgz",
|
||||
"integrity": "sha512-C+5kb6rgsGMmvIdUI7v1PPgC98A6BMv233e97aXZ5AE03iMdlILFD/20HlHrOi0x2CzbspXn9HOnlE4/Ijn5Kw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -177,9 +177,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.3.tgz",
|
||||
"integrity": "sha512-lsKUYVd8L/j2uNs8dhMjMsKC5MHYh77gR9EThu7YCeeFz1XpIkx1I4a7mhoVfPS2VPVD1pMCh+PgxuAHUcEmXw==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.6.tgz",
|
||||
"integrity": "sha512-NMY9yg/88MskEZH2s4i6biz/3av+M8xY5ua4HE7CCz5DBz542cr7REe317+v7oKjnYBCijHpkzo5vU85bkXQmQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -192,9 +192,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.3.tgz",
|
||||
"integrity": "sha512-EyfGWeOwRqK5Xj18vok0qv8IFBZ1/+hKV+cqD44oVhGsxHo9TmPtoSiDrWn8Sa2swq/VuO5Aiog6YPDj81oIkA==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.6.tgz",
|
||||
"integrity": "sha512-AXazA0ljvQEp7cA9jscABNXsjodKbEcqPcAE3rDzKN82Vb3lYOq6INd+HOCA7hk8IegEyHW4T72Z7QGIhyCQEA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -207,9 +207,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.3.tgz",
|
||||
"integrity": "sha512-PwXkcl3t0kSeYH5RuJIeh/fHOzKZd+ZdifAWzpVO+9TLWArutTFBJvOSkTZ3CcqQqNrTj1Qyo6nqE8MQj/a7cQ==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.6.tgz",
|
||||
"integrity": "sha512-JjBf7TwY7ldcPgHYt9UcrjZB03+WZqg/jSwMAfzOzM5ZG+tu5umUqzy5ugH/crGI4eoDIhSOTDp1NL3Uo/05Fw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@ -222,9 +222,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.3.tgz",
|
||||
"integrity": "sha512-CRVkkSXf5GQcq7Am2a2tdIn85oqi/bkjuPvhNqcdeTgI0xgNbqLnEPRy2AEGkRuaJWB5uCX1IC4sqnY8ET14Yg==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.6.tgz",
|
||||
"integrity": "sha512-kATNsslryVxcH1sO3KP2nnyUWtZZVkgyhAUnyTVVa0OQQ9pmDRjTpHaE+2EQHoCM5wt/uav2edrAUqbwn3tkKQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@ -237,9 +237,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.3.tgz",
|
||||
"integrity": "sha512-t7zK1Cheh0xvzfZbimztiE0wGnpV+YRsBg3tefcEBN3O4GzgLu6fFpA5HxEyVm3hHZW1jAC4OhoGEp7C5Ii6Eg==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.6.tgz",
|
||||
"integrity": "sha512-B+wTKz+8pi7mcWXFQV0LA79dJ+qhiut5uK9q0omoKnq8yRIwQJwfg3/vclXoqqcX89Ri5Y5538V0Se2v5qlcLA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@ -252,9 +252,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.3.tgz",
|
||||
"integrity": "sha512-fUZPtyCYih6y4lDYdSM4Yoax4nS7aH0/XixJStys+9tfp5cAlIAZhEVKOOdeGXmQn0IEyiUtlIsPnfObbeDQfQ==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.6.tgz",
|
||||
"integrity": "sha512-h44RBLVXFUSjvhOfseE+5UxQ/r9LVeqK2S8JziJKOm9W7SePYRPDyn7MhzhNCCFPkcjIy+soCxfhlJXHXXCR0A==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@ -267,9 +267,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.3.tgz",
|
||||
"integrity": "sha512-oIcK2LqHWqfMERqjvaKJ3QJmycHn723HsXIv5gH4iGfmePfSj+gi0ZQv2h4bHUg2bs2gJtV0DlIjGhEuvdgxLw==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.6.tgz",
|
||||
"integrity": "sha512-FlYpyr2Xc2AUePoAbc84NRV+mj7xpsISeQ36HGf9etrY5rTBEA+IU9HzWVmw5mDFtC62EQxzkLRj8h5Hq85yOQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@ -282,9 +282,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.3.tgz",
|
||||
"integrity": "sha512-RW9lpfZ6XZ6f5to2DJPvt0f/4RXEW229Xf++quVoW+YbnPrcapIJChtD/AmZ8cK3hglO/hXxJjs21pV0/l7L5w==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.6.tgz",
|
||||
"integrity": "sha512-Mc4EUSYwzLci77u0Kao6ajB2WbTe5fNc7+lHwS3a+vJISC/oprwURezUYu1SdWAYoczbsyOvKAJwuNftoAdjjg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -297,9 +297,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.3.tgz",
|
||||
"integrity": "sha512-piZ2oBoaq58pKZvhgdV6PemlL30Uhd9GmmOkIGZYgChwNcyVSSl6iMEJxMzU7x44Lk9q+hJ6a343M/iVEMEvxA==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.6.tgz",
|
||||
"integrity": "sha512-3hgZlp7NqIM5lNG3fpdhBI5rUnPmdahraSmwAi+YX/bp7iZ7mpTv2NkypGs/XngdMtpzljICxnUG3uPfqLFd3w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -312,9 +312,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.3.tgz",
|
||||
"integrity": "sha512-vaMfouYTz/4tKdQsXDccqhV6wgPEr+hfuxdNU5Pl/vQxYTsqcXv5DYEa5Z1RAxCoua5aEB+Uj5V7VT/bM92wxw==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.6.tgz",
|
||||
"integrity": "sha512-aEWTdZQHtSRROlDYn7ygB8yAqtnall/UnmoVIJVqccKitkAWVVSYocQUWrBOxLEFk8XdlRouVrLZe6WXszyviA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -327,9 +327,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.3.tgz",
|
||||
"integrity": "sha512-Fa3rNQQ9q1qwy9u2cdDvuGKy3jmPnPPMDdyy/qbn5d395Pb9hjLYiPzX9BozXMPJDlCNofSY7jN3miM9gyAdHA==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.6.tgz",
|
||||
"integrity": "sha512-uxk/5yAGpjKZUHOECtI9W+9IcLjKj+2m0qf+RG7f7eRBHr8wP6wsr3XbNbgtOD1qSpPapd6R2ZfSeXTkCcAo5g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -342,9 +342,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.3.tgz",
|
||||
"integrity": "sha512-LPJC8ub+9uzyC6ygVmp00dAqet1q1DsZ/OldGIIBt+y+Ctd1OfnKNlzQgXK8nxwY1G8fAhklFSeSRRgAUJnR0w==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.6.tgz",
|
||||
"integrity": "sha512-oXlXGS9zvNCGoAT/tLHAsFKrIKye1JaIIP0anCdpaI+Dc10ftaNZcqfLzEwyhdzFAYInXYH4V7kEdH4hPyo9GA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -357,9 +357,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.3.tgz",
|
||||
"integrity": "sha512-WtUyRspyxZR6NTc2HG4xd9Wvz8lP4C6OUY1gAqisrf151HvXIxsK0mfAacFJNS7EN2wvPTgjP+SM8vgBOx5+zA==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.6.tgz",
|
||||
"integrity": "sha512-qh7IcAHUvvmMBmoIG+V+BbE9ZWSR0ohF51e5g8JZvU08kZF58uDFL5tHs0eoYz31H6Finv17te3W3QB042GqVA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -372,9 +372,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.3.tgz",
|
||||
"integrity": "sha512-Z8qCK4BkBm40j5KUM4NrkxYQS0R12cBO1NBVtI4vws6uwh1n/VaNu31Hm+n2cJUWdFbfH57PBghkhm9yLgmPfw==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.6.tgz",
|
||||
"integrity": "sha512-9UDwkz7Wlm4N9jnv+4NL7F8vxLhSZfEkRArz2gD33HesAFfMLGIGNVXRoIHtWNw8feKsnGly9Hq1EUuRkWl0zA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -1647,9 +1647,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.3.tgz",
|
||||
"integrity": "sha512-eadWJC4CRpj93+miO5ZBlvCv+m2x6pzyNBznTvUeLFObMmxs1IMd8cCf6qiDVEZuDL6W8W7u+ZNW3GKEfOdDsA==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.6.tgz",
|
||||
"integrity": "sha512-5QgxWaAhU/tPBpvkxUmnFv2YINHuZzjbk0LeUUnC2i3aJHjfi5yR49lgKgF7cb98bclOp/kans8M5TGbGFfJlQ==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
@ -1658,28 +1658,28 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.18.3",
|
||||
"@esbuild/android-arm64": "0.18.3",
|
||||
"@esbuild/android-x64": "0.18.3",
|
||||
"@esbuild/darwin-arm64": "0.18.3",
|
||||
"@esbuild/darwin-x64": "0.18.3",
|
||||
"@esbuild/freebsd-arm64": "0.18.3",
|
||||
"@esbuild/freebsd-x64": "0.18.3",
|
||||
"@esbuild/linux-arm": "0.18.3",
|
||||
"@esbuild/linux-arm64": "0.18.3",
|
||||
"@esbuild/linux-ia32": "0.18.3",
|
||||
"@esbuild/linux-loong64": "0.18.3",
|
||||
"@esbuild/linux-mips64el": "0.18.3",
|
||||
"@esbuild/linux-ppc64": "0.18.3",
|
||||
"@esbuild/linux-riscv64": "0.18.3",
|
||||
"@esbuild/linux-s390x": "0.18.3",
|
||||
"@esbuild/linux-x64": "0.18.3",
|
||||
"@esbuild/netbsd-x64": "0.18.3",
|
||||
"@esbuild/openbsd-x64": "0.18.3",
|
||||
"@esbuild/sunos-x64": "0.18.3",
|
||||
"@esbuild/win32-arm64": "0.18.3",
|
||||
"@esbuild/win32-ia32": "0.18.3",
|
||||
"@esbuild/win32-x64": "0.18.3"
|
||||
"@esbuild/android-arm": "0.18.6",
|
||||
"@esbuild/android-arm64": "0.18.6",
|
||||
"@esbuild/android-x64": "0.18.6",
|
||||
"@esbuild/darwin-arm64": "0.18.6",
|
||||
"@esbuild/darwin-x64": "0.18.6",
|
||||
"@esbuild/freebsd-arm64": "0.18.6",
|
||||
"@esbuild/freebsd-x64": "0.18.6",
|
||||
"@esbuild/linux-arm": "0.18.6",
|
||||
"@esbuild/linux-arm64": "0.18.6",
|
||||
"@esbuild/linux-ia32": "0.18.6",
|
||||
"@esbuild/linux-loong64": "0.18.6",
|
||||
"@esbuild/linux-mips64el": "0.18.6",
|
||||
"@esbuild/linux-ppc64": "0.18.6",
|
||||
"@esbuild/linux-riscv64": "0.18.6",
|
||||
"@esbuild/linux-s390x": "0.18.6",
|
||||
"@esbuild/linux-x64": "0.18.6",
|
||||
"@esbuild/netbsd-x64": "0.18.6",
|
||||
"@esbuild/openbsd-x64": "0.18.6",
|
||||
"@esbuild/sunos-x64": "0.18.6",
|
||||
"@esbuild/win32-arm64": "0.18.6",
|
||||
"@esbuild/win32-ia32": "0.18.6",
|
||||
"@esbuild/win32-x64": "0.18.6"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
@ -6797,135 +6797,135 @@
|
||||
}
|
||||
},
|
||||
"@esbuild/android-arm": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.3.tgz",
|
||||
"integrity": "sha512-QOn3VIlL6Qv1eHBpQB/s7simaZgGss2ASyxDOwYSLmc6vD0uuizZkuYawHmuLjWEm5wPwp0JQWhbpaYwwGevYw==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.6.tgz",
|
||||
"integrity": "sha512-J3lwhDSXBBppSzm/LC1uZ8yKSIpExc+5T8MxrYD9KNVZG81FOAu2VF2gXi/6A/LwDDQQ+b6DpQbYlo3VwxFepQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/android-arm64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.3.tgz",
|
||||
"integrity": "sha512-PgabCsoaEEnnOiF6rUhOBXgYoLFIrHWP6mfLOzuQ1oZ1lwBdTL0hp5ivC4K3Kvz3BD8EipjeQo6l0aty3nr4qQ==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.6.tgz",
|
||||
"integrity": "sha512-pL0Ci8P9q1sWbtPx8CXbc8JvPvvYdJJQ+LO09PLFsbz3aYNdFBGWJjiHU+CaObO4Ames+GOFpXRAJZS2L3ZK/A==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/android-x64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.3.tgz",
|
||||
"integrity": "sha512-1OkJf8wNX1W5ucbp5HrK+z42b9DINb4ix59oJH/PIsh9cyFMqjgRKtCBXg0zEWhkmP1k3egdfrnS7cDTpLH43g==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.6.tgz",
|
||||
"integrity": "sha512-hE2vZxOlJ05aY28lUpB0y0RokngtZtcUB+TVl9vnLEnY0z/8BicSvrkThg5/iI1rbf8TwXrbr2heEjl9fLf+EA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/darwin-arm64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.3.tgz",
|
||||
"integrity": "sha512-57aofORpY7wDAuMs6DeqpmgSnVfZ63RgGbR/BHdOSTqJgYvHDCMY7/o1myFntl3k0YxtLE3WAm56nMf4qy3UDw==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.6.tgz",
|
||||
"integrity": "sha512-/tuyl4R+QhhoROQtuQj9E/yfJtZNdv2HKaHwYhhHGQDN1Teziem2Kh7BWQMumfiY7Lu9g5rO7scWdGE4OsQ6MQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/darwin-x64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.3.tgz",
|
||||
"integrity": "sha512-NVBqMnxT9qvgu7Z322LUDlwjh4GDk6wEePyAQnHF9noxik/WvLFmr5v3Vgz5LSvqFducLCxsdmLztKhdpFW0Gg==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.6.tgz",
|
||||
"integrity": "sha512-L7IQga2pDT+14Ti8HZwsVfbCjuKP4U213T3tuPggOzyK/p4KaUJxQFXJgfUFHKzU0zOXx8QcYRYZf0hSQtppkw==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/freebsd-arm64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.3.tgz",
|
||||
"integrity": "sha512-XiLK1AsCk2wKxN7j8h9GXXCs8FPZhp07U0rnpwRkAVSVGgLaIWYSqpTRzKjAfqJiZlp+XKo1HwsmDdICEKB3Dg==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.6.tgz",
|
||||
"integrity": "sha512-bq10jFv42V20Kk77NvmO+WEZaLHBKuXcvEowixnBOMkaBgS7kQaqTc77ZJDbsUpXU3KKNLQFZctfaeINmeTsZA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/freebsd-x64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.3.tgz",
|
||||
"integrity": "sha512-xyITfrF0G3l1gwR79hvNCCWKQ/16uK14xNNPFgzjbIqF4EpBvhO6l3jrWxXFUW51z6dVIl2Szh3x3uIbBWzH1Q==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.6.tgz",
|
||||
"integrity": "sha512-HbDLlkDZqUMBQaiday0pJzB6/8Xx/10dI3xRebJBReOEeDSeS+7GzTtW9h8ZnfB7/wBCqvtAjGtWQLTNPbR2+g==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-arm": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.3.tgz",
|
||||
"integrity": "sha512-fc/T0QHMzvmnlF+kfD6bHLB8u+17gg13260p/E86yYjVoKNFjonL/+Y0GGQjMbFUas9QijqOa7pcR00a9RNkwg==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.6.tgz",
|
||||
"integrity": "sha512-C+5kb6rgsGMmvIdUI7v1PPgC98A6BMv233e97aXZ5AE03iMdlILFD/20HlHrOi0x2CzbspXn9HOnlE4/Ijn5Kw==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-arm64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.3.tgz",
|
||||
"integrity": "sha512-lsKUYVd8L/j2uNs8dhMjMsKC5MHYh77gR9EThu7YCeeFz1XpIkx1I4a7mhoVfPS2VPVD1pMCh+PgxuAHUcEmXw==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.6.tgz",
|
||||
"integrity": "sha512-NMY9yg/88MskEZH2s4i6biz/3av+M8xY5ua4HE7CCz5DBz542cr7REe317+v7oKjnYBCijHpkzo5vU85bkXQmQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-ia32": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.3.tgz",
|
||||
"integrity": "sha512-EyfGWeOwRqK5Xj18vok0qv8IFBZ1/+hKV+cqD44oVhGsxHo9TmPtoSiDrWn8Sa2swq/VuO5Aiog6YPDj81oIkA==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.6.tgz",
|
||||
"integrity": "sha512-AXazA0ljvQEp7cA9jscABNXsjodKbEcqPcAE3rDzKN82Vb3lYOq6INd+HOCA7hk8IegEyHW4T72Z7QGIhyCQEA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-loong64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.3.tgz",
|
||||
"integrity": "sha512-PwXkcl3t0kSeYH5RuJIeh/fHOzKZd+ZdifAWzpVO+9TLWArutTFBJvOSkTZ3CcqQqNrTj1Qyo6nqE8MQj/a7cQ==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.6.tgz",
|
||||
"integrity": "sha512-JjBf7TwY7ldcPgHYt9UcrjZB03+WZqg/jSwMAfzOzM5ZG+tu5umUqzy5ugH/crGI4eoDIhSOTDp1NL3Uo/05Fw==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-mips64el": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.3.tgz",
|
||||
"integrity": "sha512-CRVkkSXf5GQcq7Am2a2tdIn85oqi/bkjuPvhNqcdeTgI0xgNbqLnEPRy2AEGkRuaJWB5uCX1IC4sqnY8ET14Yg==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.6.tgz",
|
||||
"integrity": "sha512-kATNsslryVxcH1sO3KP2nnyUWtZZVkgyhAUnyTVVa0OQQ9pmDRjTpHaE+2EQHoCM5wt/uav2edrAUqbwn3tkKQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-ppc64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.3.tgz",
|
||||
"integrity": "sha512-t7zK1Cheh0xvzfZbimztiE0wGnpV+YRsBg3tefcEBN3O4GzgLu6fFpA5HxEyVm3hHZW1jAC4OhoGEp7C5Ii6Eg==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.6.tgz",
|
||||
"integrity": "sha512-B+wTKz+8pi7mcWXFQV0LA79dJ+qhiut5uK9q0omoKnq8yRIwQJwfg3/vclXoqqcX89Ri5Y5538V0Se2v5qlcLA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-riscv64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.3.tgz",
|
||||
"integrity": "sha512-fUZPtyCYih6y4lDYdSM4Yoax4nS7aH0/XixJStys+9tfp5cAlIAZhEVKOOdeGXmQn0IEyiUtlIsPnfObbeDQfQ==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.6.tgz",
|
||||
"integrity": "sha512-h44RBLVXFUSjvhOfseE+5UxQ/r9LVeqK2S8JziJKOm9W7SePYRPDyn7MhzhNCCFPkcjIy+soCxfhlJXHXXCR0A==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-s390x": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.3.tgz",
|
||||
"integrity": "sha512-oIcK2LqHWqfMERqjvaKJ3QJmycHn723HsXIv5gH4iGfmePfSj+gi0ZQv2h4bHUg2bs2gJtV0DlIjGhEuvdgxLw==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.6.tgz",
|
||||
"integrity": "sha512-FlYpyr2Xc2AUePoAbc84NRV+mj7xpsISeQ36HGf9etrY5rTBEA+IU9HzWVmw5mDFtC62EQxzkLRj8h5Hq85yOQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-x64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.3.tgz",
|
||||
"integrity": "sha512-RW9lpfZ6XZ6f5to2DJPvt0f/4RXEW229Xf++quVoW+YbnPrcapIJChtD/AmZ8cK3hglO/hXxJjs21pV0/l7L5w==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.6.tgz",
|
||||
"integrity": "sha512-Mc4EUSYwzLci77u0Kao6ajB2WbTe5fNc7+lHwS3a+vJISC/oprwURezUYu1SdWAYoczbsyOvKAJwuNftoAdjjg==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/netbsd-x64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.3.tgz",
|
||||
"integrity": "sha512-piZ2oBoaq58pKZvhgdV6PemlL30Uhd9GmmOkIGZYgChwNcyVSSl6iMEJxMzU7x44Lk9q+hJ6a343M/iVEMEvxA==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.6.tgz",
|
||||
"integrity": "sha512-3hgZlp7NqIM5lNG3fpdhBI5rUnPmdahraSmwAi+YX/bp7iZ7mpTv2NkypGs/XngdMtpzljICxnUG3uPfqLFd3w==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/openbsd-x64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.3.tgz",
|
||||
"integrity": "sha512-vaMfouYTz/4tKdQsXDccqhV6wgPEr+hfuxdNU5Pl/vQxYTsqcXv5DYEa5Z1RAxCoua5aEB+Uj5V7VT/bM92wxw==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.6.tgz",
|
||||
"integrity": "sha512-aEWTdZQHtSRROlDYn7ygB8yAqtnall/UnmoVIJVqccKitkAWVVSYocQUWrBOxLEFk8XdlRouVrLZe6WXszyviA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/sunos-x64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.3.tgz",
|
||||
"integrity": "sha512-Fa3rNQQ9q1qwy9u2cdDvuGKy3jmPnPPMDdyy/qbn5d395Pb9hjLYiPzX9BozXMPJDlCNofSY7jN3miM9gyAdHA==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.6.tgz",
|
||||
"integrity": "sha512-uxk/5yAGpjKZUHOECtI9W+9IcLjKj+2m0qf+RG7f7eRBHr8wP6wsr3XbNbgtOD1qSpPapd6R2ZfSeXTkCcAo5g==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-arm64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.3.tgz",
|
||||
"integrity": "sha512-LPJC8ub+9uzyC6ygVmp00dAqet1q1DsZ/OldGIIBt+y+Ctd1OfnKNlzQgXK8nxwY1G8fAhklFSeSRRgAUJnR0w==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.6.tgz",
|
||||
"integrity": "sha512-oXlXGS9zvNCGoAT/tLHAsFKrIKye1JaIIP0anCdpaI+Dc10ftaNZcqfLzEwyhdzFAYInXYH4V7kEdH4hPyo9GA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-ia32": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.3.tgz",
|
||||
"integrity": "sha512-WtUyRspyxZR6NTc2HG4xd9Wvz8lP4C6OUY1gAqisrf151HvXIxsK0mfAacFJNS7EN2wvPTgjP+SM8vgBOx5+zA==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.6.tgz",
|
||||
"integrity": "sha512-qh7IcAHUvvmMBmoIG+V+BbE9ZWSR0ohF51e5g8JZvU08kZF58uDFL5tHs0eoYz31H6Finv17te3W3QB042GqVA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-x64": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.3.tgz",
|
||||
"integrity": "sha512-Z8qCK4BkBm40j5KUM4NrkxYQS0R12cBO1NBVtI4vws6uwh1n/VaNu31Hm+n2cJUWdFbfH57PBghkhm9yLgmPfw==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.6.tgz",
|
||||
"integrity": "sha512-9UDwkz7Wlm4N9jnv+4NL7F8vxLhSZfEkRArz2gD33HesAFfMLGIGNVXRoIHtWNw8feKsnGly9Hq1EUuRkWl0zA==",
|
||||
"optional": true
|
||||
},
|
||||
"@jridgewell/gen-mapping": {
|
||||
@ -7896,32 +7896,32 @@
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.18.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.3.tgz",
|
||||
"integrity": "sha512-eadWJC4CRpj93+miO5ZBlvCv+m2x6pzyNBznTvUeLFObMmxs1IMd8cCf6qiDVEZuDL6W8W7u+ZNW3GKEfOdDsA==",
|
||||
"version": "0.18.6",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.6.tgz",
|
||||
"integrity": "sha512-5QgxWaAhU/tPBpvkxUmnFv2YINHuZzjbk0LeUUnC2i3aJHjfi5yR49lgKgF7cb98bclOp/kans8M5TGbGFfJlQ==",
|
||||
"requires": {
|
||||
"@esbuild/android-arm": "0.18.3",
|
||||
"@esbuild/android-arm64": "0.18.3",
|
||||
"@esbuild/android-x64": "0.18.3",
|
||||
"@esbuild/darwin-arm64": "0.18.3",
|
||||
"@esbuild/darwin-x64": "0.18.3",
|
||||
"@esbuild/freebsd-arm64": "0.18.3",
|
||||
"@esbuild/freebsd-x64": "0.18.3",
|
||||
"@esbuild/linux-arm": "0.18.3",
|
||||
"@esbuild/linux-arm64": "0.18.3",
|
||||
"@esbuild/linux-ia32": "0.18.3",
|
||||
"@esbuild/linux-loong64": "0.18.3",
|
||||
"@esbuild/linux-mips64el": "0.18.3",
|
||||
"@esbuild/linux-ppc64": "0.18.3",
|
||||
"@esbuild/linux-riscv64": "0.18.3",
|
||||
"@esbuild/linux-s390x": "0.18.3",
|
||||
"@esbuild/linux-x64": "0.18.3",
|
||||
"@esbuild/netbsd-x64": "0.18.3",
|
||||
"@esbuild/openbsd-x64": "0.18.3",
|
||||
"@esbuild/sunos-x64": "0.18.3",
|
||||
"@esbuild/win32-arm64": "0.18.3",
|
||||
"@esbuild/win32-ia32": "0.18.3",
|
||||
"@esbuild/win32-x64": "0.18.3"
|
||||
"@esbuild/android-arm": "0.18.6",
|
||||
"@esbuild/android-arm64": "0.18.6",
|
||||
"@esbuild/android-x64": "0.18.6",
|
||||
"@esbuild/darwin-arm64": "0.18.6",
|
||||
"@esbuild/darwin-x64": "0.18.6",
|
||||
"@esbuild/freebsd-arm64": "0.18.6",
|
||||
"@esbuild/freebsd-x64": "0.18.6",
|
||||
"@esbuild/linux-arm": "0.18.6",
|
||||
"@esbuild/linux-arm64": "0.18.6",
|
||||
"@esbuild/linux-ia32": "0.18.6",
|
||||
"@esbuild/linux-loong64": "0.18.6",
|
||||
"@esbuild/linux-mips64el": "0.18.6",
|
||||
"@esbuild/linux-ppc64": "0.18.6",
|
||||
"@esbuild/linux-riscv64": "0.18.6",
|
||||
"@esbuild/linux-s390x": "0.18.6",
|
||||
"@esbuild/linux-x64": "0.18.6",
|
||||
"@esbuild/netbsd-x64": "0.18.6",
|
||||
"@esbuild/openbsd-x64": "0.18.6",
|
||||
"@esbuild/sunos-x64": "0.18.6",
|
||||
"@esbuild/win32-arm64": "0.18.6",
|
||||
"@esbuild/win32-ia32": "0.18.6",
|
||||
"@esbuild/win32-x64": "0.18.6"
|
||||
}
|
||||
},
|
||||
"escalade": {
|
||||
|
@ -23,7 +23,7 @@
|
||||
"any-date-parser": "^1.5.4",
|
||||
"browserslist": "^4.21.7",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"esbuild": "^0.18.3",
|
||||
"esbuild": "^0.18.6",
|
||||
"fs-cheerio": "^3.0.0",
|
||||
"inline-source": "^8.0.2",
|
||||
"jsdom": "^22.1.0",
|
||||
|
13
pwreset.go
13
pwreset.go
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@ -25,6 +26,18 @@ func (app *appContext) GenInternalReset(userID string) (InternalPWR, error) {
|
||||
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() {
|
||||
app.info.Println("Starting password reset daemon")
|
||||
path := app.config.Section("password_resets").Key("watch_directory").String()
|
||||
|
41
router.go
41
router.go
@ -101,6 +101,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
if app.URLBase != "" {
|
||||
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 {
|
||||
router.GET(p+"/lang/:page", app.GetLanguages)
|
||||
router.Use(static.Serve(p+"/", app.webFS))
|
||||
@ -140,6 +143,13 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
router.POST(p+"/invite/:invCode/matrix/user", app.MatrixSendPIN)
|
||||
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 {
|
||||
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
|
||||
@ -147,7 +157,14 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
}
|
||||
}
|
||||
|
||||
api := router.Group("/", app.webAuth())
|
||||
|
||||
var user *gin.RouterGroup
|
||||
if userPageEnabled {
|
||||
user = router.Group("/my", app.userAuth())
|
||||
}
|
||||
|
||||
for _, p := range routePrefixes {
|
||||
router.POST(p+"/logout", app.Logout)
|
||||
api.DELETE(p+"/users", app.DeleteUsers)
|
||||
@ -180,10 +197,10 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
|
||||
api.GET(p+"/config/update", app.CheckUpdate)
|
||||
api.POST(p+"/config/update", app.ApplyUpdate)
|
||||
api.GET(p+"/config/emails", app.GetCustomEmails)
|
||||
api.GET(p+"/config/emails/:id", app.GetCustomEmailTemplate)
|
||||
api.POST(p+"/config/emails/:id", app.SetCustomEmail)
|
||||
api.POST(p+"/config/emails/:id/state/:state", app.SetCustomEmailState)
|
||||
api.GET(p+"/config/emails", app.GetCustomContent)
|
||||
api.GET(p+"/config/emails/:id", app.GetCustomMessageTemplate)
|
||||
api.POST(p+"/config/emails/:id", app.SetCustomMessage)
|
||||
api.POST(p+"/config/emails/:id/state/:state", app.SetCustomMessageState)
|
||||
api.GET(p+"/config", app.GetConfig)
|
||||
api.POST(p+"/config", app.ModifyConfig)
|
||||
api.POST(p+"/restart", app.restart)
|
||||
@ -210,6 +227,22 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
}
|
||||
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,6 +21,8 @@ def generate_ini(base_file, ini_file):
|
||||
if "meta" in config_base["sections"][section]:
|
||||
ini.set(section, fix_description(config_base["sections"][section]["meta"]["description"]))
|
||||
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]:
|
||||
ini.set(section, fix_description(config_base["sections"][section]["settings"][entry]["description"]))
|
||||
value = config_base["sections"][section]["settings"][entry]["value"]
|
||||
|
33
scripts/langmover/README.md
Normal file
33
scripts/langmover/README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# *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**.
|
||||
|
||||
|
55
scripts/langmover/common.json
Normal file
55
scripts/langmover/common.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
138
scripts/langmover/langmover.py
Normal file
138
scripts/langmover/langmover.py
Normal file
@ -0,0 +1,138 @@
|
||||
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))
|
41
scripts/langmover/login.json
Normal file
41
scripts/langmover/login.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
15
scripts/langmover/template.json
Normal file
15
scripts/langmover/template.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"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,7 +111,8 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||
st.lang.Common.patchCommonStrings(&lang.Strings, index)
|
||||
st.lang.Common.patchCommonNotifications(&lang.Notifications, index)
|
||||
if fname != "en-us.json" {
|
||||
if lang.Meta.Fallback != "" {
|
||||
fallback, ok := st.lang.Setup[lang.Meta.Fallback]
|
||||
|
333
storage.go
333
storage.go
@ -15,24 +15,189 @@ import (
|
||||
"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 {
|
||||
timePattern string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path, announcements_path, matrix_sql_path string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path, announcements_path, matrix_sql_path, userPage_path string
|
||||
users map[string]time.Time // Map of Jellyfin User IDs to their expiry times.
|
||||
invites Invites
|
||||
profiles map[string]Profile
|
||||
defaultProfile string
|
||||
displayprefs, ombi_template map[string]interface{}
|
||||
emails map[string]EmailAddress
|
||||
telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
|
||||
discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users.
|
||||
matrix map[string]MatrixUser // Map of Jellyfin user IDs to Matrix users.
|
||||
emails emailStore
|
||||
telegram telegramStore // Map of Jellyfin User IDs to telegram users.
|
||||
discord discordStore // Map of Jellyfin user IDs to discord users.
|
||||
matrix matrixStore // Map of Jellyfin user IDs to Matrix users.
|
||||
customEmails customEmails
|
||||
userPage userPageContent
|
||||
policy mediabrowser.Policy
|
||||
configuration mediabrowser.Configuration
|
||||
lang Lang
|
||||
announcements map[string]announcementTemplate
|
||||
invitesLock, usersLock sync.Mutex
|
||||
invitesLock, usersLock, discordLock, telegramLock, matrixLock, emailsLock 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 {
|
||||
@ -49,6 +214,7 @@ type DiscordUser struct {
|
||||
Discriminator string
|
||||
Lang string
|
||||
Contact bool
|
||||
JellyfinID string `json:"-"` // Used internally in discord.go
|
||||
}
|
||||
|
||||
type EmailAddress struct {
|
||||
@ -59,25 +225,30 @@ type EmailAddress struct {
|
||||
}
|
||||
|
||||
type customEmails struct {
|
||||
UserCreated customEmail `json:"userCreated"`
|
||||
InviteExpiry customEmail `json:"inviteExpiry"`
|
||||
PasswordReset customEmail `json:"passwordReset"`
|
||||
UserDeleted customEmail `json:"userDeleted"`
|
||||
UserDisabled customEmail `json:"userDisabled"`
|
||||
UserEnabled customEmail `json:"userEnabled"`
|
||||
InviteEmail customEmail `json:"inviteEmail"`
|
||||
WelcomeEmail customEmail `json:"welcomeEmail"`
|
||||
EmailConfirmation customEmail `json:"emailConfirmation"`
|
||||
UserExpired customEmail `json:"userExpired"`
|
||||
UserCreated customContent `json:"userCreated"`
|
||||
InviteExpiry customContent `json:"inviteExpiry"`
|
||||
PasswordReset customContent `json:"passwordReset"`
|
||||
UserDeleted customContent `json:"userDeleted"`
|
||||
UserDisabled customContent `json:"userDisabled"`
|
||||
UserEnabled customContent `json:"userEnabled"`
|
||||
InviteEmail customContent `json:"inviteEmail"`
|
||||
WelcomeEmail customContent `json:"welcomeEmail"`
|
||||
EmailConfirmation customContent `json:"emailConfirmation"`
|
||||
UserExpired customContent `json:"userExpired"`
|
||||
}
|
||||
|
||||
type customEmail struct {
|
||||
type customContent struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Content string `json:"content"`
|
||||
Variables []string `json:"variables,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
|
||||
|
||||
type Profile struct {
|
||||
@ -107,7 +278,6 @@ type Invite struct {
|
||||
Notify map[string]map[string]bool `json:"notify"`
|
||||
Profile string `json:"profile"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Keys []string `json:"keys,omitempty"`
|
||||
Captchas map[string]*captcha.Data // Map of Captcha IDs to answers
|
||||
}
|
||||
|
||||
@ -116,9 +286,9 @@ type Lang struct {
|
||||
chosenAdminLang string
|
||||
Admin adminLangs
|
||||
AdminJSON map[string]string
|
||||
FormPath string
|
||||
chosenFormLang string
|
||||
Form formLangs
|
||||
UserPath string
|
||||
chosenUserLang string
|
||||
User userLangs
|
||||
PasswordResetPath string
|
||||
chosenPWRLang string
|
||||
PasswordReset pwrLangs
|
||||
@ -144,7 +314,11 @@ func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = st.loadLangForm(filesystems...)
|
||||
err = st.loadLangEmail(filesystems...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = st.loadLangUser(filesystems...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -152,10 +326,6 @@ func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = st.loadLangEmail(filesystems...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = st.loadLangTelegram(filesystems...)
|
||||
return
|
||||
}
|
||||
@ -164,7 +334,7 @@ func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
|
||||
// from a list of other sources in a preferred order.
|
||||
// languages to patch from should be in decreasing priority,
|
||||
// E.g: If to = fr-be, from = [fr-fr, en-us].
|
||||
func (common *commonLangs) patchCommon(to *langSection, from ...string) {
|
||||
func (common *commonLangs) patchCommonStrings(to *langSection, from ...string) {
|
||||
if *to == nil {
|
||||
*to = langSection{}
|
||||
}
|
||||
@ -183,6 +353,44 @@ func (common *commonLangs) patchCommon(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) {
|
||||
if *to == nil {
|
||||
*to = langSection{}
|
||||
@ -264,10 +472,14 @@ func (st *Storage) loadLangCommon(filesystems ...fs.FS) error {
|
||||
if err == nil {
|
||||
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
||||
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 == "" {
|
||||
patchLang(&lang.Strings, &english.Strings)
|
||||
patchLang(&lang.Notifications, &english.Notifications)
|
||||
patchQuantityStrings(&lang.QuantityStrings, &english.QuantityStrings)
|
||||
}
|
||||
}
|
||||
st.lang.Common[index] = lang
|
||||
@ -329,7 +541,8 @@ func (st *Storage) loadLangAdmin(filesystems ...fs.FS) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||
st.lang.Common.patchCommonStrings(&lang.Strings, index)
|
||||
st.lang.Common.patchCommonNotifications(&lang.Notifications, index)
|
||||
if fname != "en-us.json" {
|
||||
if lang.Meta.Fallback != "" {
|
||||
fallback, ok := st.lang.Admin[lang.Meta.Fallback]
|
||||
@ -395,16 +608,16 @@ func (st *Storage) loadLangAdmin(filesystems ...fs.FS) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
|
||||
st.lang.Form = map[string]formLang{}
|
||||
var english formLang
|
||||
func (st *Storage) loadLangUser(filesystems ...fs.FS) error {
|
||||
st.lang.User = map[string]userLang{}
|
||||
var english userLang
|
||||
loadedLangs := make([]map[string]bool, len(filesystems))
|
||||
var load loadLangFunc
|
||||
load = func(fsIndex int, fname string) error {
|
||||
filesystem := filesystems[fsIndex]
|
||||
index := strings.TrimSuffix(fname, filepath.Ext(fname))
|
||||
lang := formLang{}
|
||||
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.FormPath, fname))
|
||||
lang := userLang{}
|
||||
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.UserPath, fname))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -415,14 +628,21 @@ func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||
st.lang.Common.patchCommonStrings(&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 lang.Meta.Fallback != "" {
|
||||
fallback, ok := st.lang.Form[lang.Meta.Fallback]
|
||||
fallback, ok := st.lang.User[lang.Meta.Fallback]
|
||||
err = nil
|
||||
if !ok {
|
||||
err = load(fsIndex, lang.Meta.Fallback+".json")
|
||||
fallback = st.lang.Form[lang.Meta.Fallback]
|
||||
fallback = st.lang.User[lang.Meta.Fallback]
|
||||
}
|
||||
if err == nil {
|
||||
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
||||
@ -445,9 +665,14 @@ func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userJSON, err := json.Marshal(lang)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lang.notificationsJSON = string(notifications)
|
||||
lang.validationStringsJSON = string(validationStrings)
|
||||
st.lang.Form[index] = lang
|
||||
lang.JSON = string(userJSON)
|
||||
st.lang.User[index] = lang
|
||||
return nil
|
||||
}
|
||||
engFound := false
|
||||
@ -463,10 +688,10 @@ func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
|
||||
if !engFound {
|
||||
return err
|
||||
}
|
||||
english = st.lang.Form["en-us"]
|
||||
formLoaded := false
|
||||
english = st.lang.User["en-us"]
|
||||
userLoaded := false
|
||||
for i := range filesystems {
|
||||
files, err := fs.ReadDir(filesystems[i], st.lang.FormPath)
|
||||
files, err := fs.ReadDir(filesystems[i], st.lang.UserPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@ -474,13 +699,13 @@ func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
|
||||
if !loadedLangs[i][f.Name()] {
|
||||
err = load(i, f.Name())
|
||||
if err == nil {
|
||||
formLoaded = true
|
||||
userLoaded = true
|
||||
loadedLangs[i][f.Name()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !formLoaded {
|
||||
if !userLoaded {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@ -506,7 +731,7 @@ func (st *Storage) loadLangPWR(filesystems ...fs.FS) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||
st.lang.Common.patchCommonStrings(&lang.Strings, index)
|
||||
if fname != "en-us.json" {
|
||||
if lang.Meta.Fallback != "" {
|
||||
fallback, ok := st.lang.PasswordReset[lang.Meta.Fallback]
|
||||
@ -540,7 +765,7 @@ func (st *Storage) loadLangPWR(filesystems ...fs.FS) error {
|
||||
return err
|
||||
}
|
||||
english = st.lang.PasswordReset["en-us"]
|
||||
formLoaded := false
|
||||
userLoaded := false
|
||||
for i := range filesystems {
|
||||
files, err := fs.ReadDir(filesystems[i], st.lang.PasswordResetPath)
|
||||
if err != nil {
|
||||
@ -550,13 +775,13 @@ func (st *Storage) loadLangPWR(filesystems ...fs.FS) error {
|
||||
if !loadedLangs[i][f.Name()] {
|
||||
err = load(i, f.Name())
|
||||
if err == nil {
|
||||
formLoaded = true
|
||||
userLoaded = true
|
||||
loadedLangs[i][f.Name()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !formLoaded {
|
||||
if !userLoaded {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@ -582,7 +807,7 @@ func (st *Storage) loadLangEmail(filesystems ...fs.FS) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||
st.lang.Common.patchCommonStrings(&lang.Strings, index)
|
||||
if fname != "en-us.json" {
|
||||
if lang.Meta.Fallback != "" {
|
||||
fallback, ok := st.lang.Email[lang.Meta.Fallback]
|
||||
@ -679,7 +904,7 @@ func (st *Storage) loadLangTelegram(filesystems ...fs.FS) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||
st.lang.Common.patchCommonStrings(&lang.Strings, index)
|
||||
if fname != "en-us.json" {
|
||||
if lang.Meta.Fallback != "" {
|
||||
fallback, ok := st.lang.Telegram[lang.Meta.Fallback]
|
||||
@ -739,14 +964,10 @@ func (st *Storage) loadLangTelegram(filesystems ...fs.FS) error {
|
||||
type Invites map[string]Invite
|
||||
|
||||
func (st *Storage) loadInvites() error {
|
||||
st.invitesLock.Lock()
|
||||
defer st.invitesLock.Unlock()
|
||||
return loadJSON(st.invite_path, &st.invites)
|
||||
}
|
||||
|
||||
func (st *Storage) storeInvites() error {
|
||||
st.invitesLock.Lock()
|
||||
defer st.invitesLock.Unlock()
|
||||
return storeJSON(st.invite_path, st.invites)
|
||||
}
|
||||
|
||||
@ -813,6 +1034,14 @@ func (st *Storage) storeCustomEmails() error {
|
||||
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 {
|
||||
return loadJSON(st.policy_path, &st.policy)
|
||||
}
|
||||
|
@ -40,7 +40,8 @@ module.exports = {
|
||||
d_urge: dark.d_urge,
|
||||
d_warning: dark.d_warning,
|
||||
d_info: dark.d_info,
|
||||
d_critical: dark.d_critical
|
||||
d_critical: dark.d_critical,
|
||||
discord: "#5865F2"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
91
telegram.go
91
telegram.go
@ -9,10 +9,20 @@ import (
|
||||
tg "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
)
|
||||
|
||||
const (
|
||||
VERIF_TOKEN_EXPIRY_SEC = 10 * 60
|
||||
)
|
||||
|
||||
type TelegramVerifiedToken struct {
|
||||
Token string
|
||||
ChatID int64
|
||||
Username string
|
||||
JellyfinID string // optional, for ensuring a user-requested change is only accessed by them.
|
||||
}
|
||||
|
||||
// VerifToken stores details about a pending user verification token.
|
||||
type VerifToken struct {
|
||||
Expiry time.Time
|
||||
JellyfinID string // optional, for ensuring a user-requested change is only accessed by them.
|
||||
}
|
||||
|
||||
type TelegramDaemon struct {
|
||||
@ -20,8 +30,8 @@ type TelegramDaemon struct {
|
||||
ShutdownChannel chan string
|
||||
bot *tg.BotAPI
|
||||
username string
|
||||
tokens []string
|
||||
verifiedTokens []TelegramVerifiedToken
|
||||
tokens map[string]VerifToken // Map of pins to tokens.
|
||||
verifiedTokens map[string]TelegramVerifiedToken // Map of token pins to the responsible ChatID+Username.
|
||||
languages map[int64]string // Store of languages for chatIDs. Added to on first interaction, and loaded from app.storage.telegram on start.
|
||||
link string
|
||||
app *appContext
|
||||
@ -40,13 +50,13 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
|
||||
ShutdownChannel: make(chan string),
|
||||
bot: bot,
|
||||
username: bot.Self.UserName,
|
||||
tokens: []string{},
|
||||
verifiedTokens: []TelegramVerifiedToken{},
|
||||
tokens: map[string]VerifToken{},
|
||||
verifiedTokens: map[string]TelegramVerifiedToken{},
|
||||
languages: map[int64]string{},
|
||||
link: "https://t.me/" + bot.Self.UserName,
|
||||
app: app,
|
||||
}
|
||||
for _, user := range app.storage.telegram {
|
||||
for _, user := range app.storage.GetTelegram() {
|
||||
if user.Lang != "" {
|
||||
td.languages[user.ChatID] = user.Lang
|
||||
}
|
||||
@ -72,7 +82,15 @@ var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
||||
func (t *TelegramDaemon) NewAuthToken() string {
|
||||
pin := genAuthToken()
|
||||
t.tokens = append(t.tokens, pin)
|
||||
t.tokens[pin] = VerifToken{Expiry: time.Now().Add(VERIF_TOKEN_EXPIRY_SEC * time.Second), JellyfinID: ""}
|
||||
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 (t *TelegramDaemon) NewAssignedAuthToken(id string) string {
|
||||
pin := genAuthToken()
|
||||
t.tokens[pin] = VerifToken{Expiry: time.Now().Add(VERIF_TOKEN_EXPIRY_SEC * time.Second), JellyfinID: id}
|
||||
return pin
|
||||
}
|
||||
|
||||
@ -198,10 +216,10 @@ func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string
|
||||
}
|
||||
if _, ok := t.app.storage.lang.Telegram[sects[1]]; ok {
|
||||
t.languages[upd.Message.Chat.ID] = sects[1]
|
||||
for jfID, user := range t.app.storage.telegram {
|
||||
for jfID, user := range t.app.storage.GetTelegram() {
|
||||
if user.ChatID == upd.Message.Chat.ID {
|
||||
user.Lang = sects[1]
|
||||
t.app.storage.telegram[jfID] = user
|
||||
t.app.storage.SetTelegramKey(jfID, user)
|
||||
if err := t.app.storage.storeTelegramUsers(); err != nil {
|
||||
t.app.err.Printf("Failed to store Telegram users: %v", err)
|
||||
}
|
||||
@ -212,29 +230,58 @@ func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string
|
||||
}
|
||||
|
||||
func (t *TelegramDaemon) commandPIN(upd *tg.Update, sects []string, lang string) {
|
||||
tokenIndex := -1
|
||||
for i, token := range t.tokens {
|
||||
if upd.Message.Text == token {
|
||||
tokenIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if tokenIndex == -1 {
|
||||
token, ok := t.tokens[upd.Message.Text]
|
||||
if !ok || time.Now().After(token.Expiry) {
|
||||
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"))
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
delete(t.tokens, upd.Message.Text)
|
||||
return
|
||||
}
|
||||
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"))
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
t.verifiedTokens = append(t.verifiedTokens, TelegramVerifiedToken{
|
||||
Token: upd.Message.Text,
|
||||
t.verifiedTokens[upd.Message.Text] = TelegramVerifiedToken{
|
||||
ChatID: upd.Message.Chat.ID,
|
||||
Username: upd.Message.Chat.UserName,
|
||||
})
|
||||
t.tokens[len(t.tokens)-1], t.tokens[tokenIndex] = t.tokens[tokenIndex], t.tokens[len(t.tokens)-1]
|
||||
t.tokens = t.tokens[:len(t.tokens)-1]
|
||||
JellyfinID: token.JellyfinID,
|
||||
}
|
||||
delete(t.tokens, upd.Message.Text)
|
||||
}
|
||||
|
||||
// TokenVerified returns whether or not a token with the given PIN has been verified, and the token itself.
|
||||
func (t *TelegramDaemon) TokenVerified(pin string) (token TelegramVerifiedToken, ok bool) {
|
||||
token, ok = t.verifiedTokens[pin]
|
||||
// delete(t.verifiedTokens, pin)
|
||||
return
|
||||
}
|
||||
|
||||
// AssignedTokenVerified returns whether or not a token 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 token.
|
||||
func (t *TelegramDaemon) AssignedTokenVerified(pin string, jfID string) (token TelegramVerifiedToken, ok bool) {
|
||||
token, ok = t.verifiedTokens[pin]
|
||||
if ok && token.JellyfinID != jfID {
|
||||
ok = false
|
||||
}
|
||||
// delete(t.verifiedTokens, pin)
|
||||
return
|
||||
}
|
||||
|
||||
// UserExists returns whether or not a user with the given username exists.
|
||||
func (t *TelegramDaemon) UserExists(username string) (ok bool) {
|
||||
ok = false
|
||||
for _, u := range t.app.storage.GetTelegram() {
|
||||
if u.Username == username {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteVerifiedToken removes the token with the given PIN.
|
||||
func (t *TelegramDaemon) DeleteVerifiedToken(pin string) {
|
||||
delete(t.verifiedTokens, pin)
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user