1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-22 09:00:10 +00:00

userpage: change email (+ confirmation)

edit/add button added for email address. Confirmation works too.
This commit is contained in:
Harvey Tindall 2023-06-18 19:38:09 +01:00
parent 03f1a3dbc0
commit 609039baeb
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
40 changed files with 309 additions and 66 deletions

View File

@ -1,6 +1,14 @@
package main package main
import "github.com/gin-gonic/gin" 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. // @Summary Returns the logged-in user's Jellyfin ID & Username.
// @Produce json // @Produce json
@ -104,3 +112,149 @@ func (app *appContext) LogoutUser(gc *gin.Context) {
gc.SetCookie("refresh", "invalid", -1, "/my", gc.Request.URL.Hostname(), true, true) gc.SetCookie("refresh", "invalid", -1, "/my", gc.Request.URL.Hostname(), true, true)
respondBool(200, true, gc) 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.emails[id]
if !ok {
emailStore = EmailAddress{
Contact: true,
}
}
emailStore.Addr = claims["email"].(string)
app.storage.emails[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 Users
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
}

View File

@ -10,6 +10,7 @@ import (
"html/template" "html/template"
"io" "io"
"io/fs" "io/fs"
"net/url"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@ -304,10 +305,17 @@ func (emailer *Emailer) confirmationValues(code, username, key string, app *appC
} else { } else {
message := app.config.Section("messages").Key("message").String() message := app.config.Section("messages").Key("message").String()
inviteLink := app.config.Section("invite_emails").Key("url_base").String() inviteLink := app.config.Section("invite_emails").Key("url_base").String()
if !strings.HasSuffix(inviteLink, "/invite") { if code == "" { // Personal email change
inviteLink += "/invite" 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, url.PathEscape(key))
} }
inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key)
template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": username}) template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": username})
template["confirmationURL"] = inviteLink template["confirmationURL"] = inviteLink
template["message"] = message template["message"] = message

View File

@ -17,6 +17,22 @@
<title>{{ .lang.Strings.pageTitle }}</title> <title>{{ .lang.Strings.pageTitle }}</title>
</head> </head>
<body class="max-w-full overflow-x-hidden section"> <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">&times;</span></span>
<p class="content mb-4">{{ .strings.confirmationRequiredMessage }}</p>
</div>
</div>
</div>
<div id="notification-box"></div> <div id="notification-box"></div>
<div class="top-4 left-4 absolute"> <div class="top-4 left-4 absolute">
<span class="dropdown" tabindex="0" id="lang-dropdown"> <span class="dropdown" tabindex="0" id="lang-dropdown">
@ -54,7 +70,7 @@
</div> </div>
<div class="card @low dark:~d_neutral flex-col" id="card-contact"> <div class="card @low dark:~d_neutral flex-col" id="card-contact">
<span class="heading mb-2">{{ .strings.contactMethods }}</span> <span class="heading mb-2">{{ .strings.contactMethods }}</span>
<div class="content flex justify-between flex-col"></div> <div class="content flex justify-between flex-col h-100"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -18,7 +18,6 @@
"create": "Opret", "create": "Opret",
"apply": "Anvend", "apply": "Anvend",
"delete": "Slet", "delete": "Slet",
"add": "Tilføj",
"select": "Vælg", "select": "Vælg",
"name": "Navn", "name": "Navn",
"date": "Dato", "date": "Dato",
@ -46,7 +45,6 @@
"conditionals": "Betingelser", "conditionals": "Betingelser",
"preview": "Eksempel", "preview": "Eksempel",
"reset": "Nulstil", "reset": "Nulstil",
"edit": "Rediger",
"donate": "Doner", "donate": "Doner",
"contactThrough": "Kontakt gennem:", "contactThrough": "Kontakt gennem:",
"extendExpiry": "Forlæng udløb", "extendExpiry": "Forlæng udløb",

View File

@ -66,7 +66,6 @@
"variables": "Variablen", "variables": "Variablen",
"preview": "Vorschau", "preview": "Vorschau",
"reset": "Zurücksetzen", "reset": "Zurücksetzen",
"edit": "Bearbeiten",
"customizeMessages": "Benachrichtigungen anpassen", "customizeMessages": "Benachrichtigungen anpassen",
"customizeMessagesDescription": "Wenn du jfa-go's E-Mail-Vorlagen nicht benutzen willst, kannst du deinen eigenen unter Verwendung von Markdown erstellen.", "customizeMessagesDescription": "Wenn du jfa-go's E-Mail-Vorlagen nicht benutzen willst, kannst du deinen eigenen unter Verwendung von Markdown erstellen.",
"announce": "Ankündigen", "announce": "Ankündigen",
@ -87,7 +86,6 @@
"contactThrough": "Kontakt über:", "contactThrough": "Kontakt über:",
"sendPIN": "Bitte den Benutzer, die unten stehende PIN an den Bot zu senden.", "sendPIN": "Bitte den Benutzer, die unten stehende PIN an den Bot zu senden.",
"inviteMonths": "Monate", "inviteMonths": "Monate",
"add": "Hinzufügen",
"select": "Auswählen", "select": "Auswählen",
"searchDiscordUser": "Gib den Discord-Benutzername ein, um den Benutzer zu finden.", "searchDiscordUser": "Gib den Discord-Benutzername ein, um den Benutzer zu finden.",
"findDiscordUser": "Suche Discord-Benutzer", "findDiscordUser": "Suche Discord-Benutzer",

View File

@ -66,7 +66,6 @@
"variables": "Μεταβλητές", "variables": "Μεταβλητές",
"preview": "Προεπισκόπηση", "preview": "Προεπισκόπηση",
"reset": "Επαναφορά", "reset": "Επαναφορά",
"edit": "Επεξεργασία",
"customizeMessages": "Παραμετροποίηση Emails", "customizeMessages": "Παραμετροποίηση Emails",
"advancedSettings": "Προχωρημένες Ρυθμίσεις", "advancedSettings": "Προχωρημένες Ρυθμίσεις",
"customizeMessagesDescription": "Αν δεν θέλετε να ζρησιμοποιήσετε τα πρότυπα email του jfa-go, μπορείτε να δημιουργήσετε τα δικά σας με χρήση Markdown.", "customizeMessagesDescription": "Αν δεν θέλετε να ζρησιμοποιήσετε τα πρότυπα email του jfa-go, μπορείτε να δημιουργήσετε τα δικά σας με χρήση Markdown.",

View File

@ -93,12 +93,10 @@
"contactThrough": "Contact through:", "contactThrough": "Contact through:",
"select": "Select", "select": "Select",
"date": "Date", "date": "Date",
"edit": "Edit",
"extendExpiry": "Extend expiry", "extendExpiry": "Extend expiry",
"sendPWR": "Send Password Reset", "sendPWR": "Send Password Reset",
"inviteMonths": "Months", "inviteMonths": "Months",
"inviteDuration": "Invite Duration", "inviteDuration": "Invite Duration",
"add": "Add",
"update": "Update", "update": "Update",
"user": "User", "user": "User",
"userExpiryDescription": "A specified amount of time after each signup, jfa-go will delete/disable the account. You can change this behaviour in settings.", "userExpiryDescription": "A specified amount of time after each signup, jfa-go will delete/disable the account. You can change this behaviour in settings.",

View File

@ -18,7 +18,6 @@
"create": "Create", "create": "Create",
"apply": "Apply", "apply": "Apply",
"delete": "Delete", "delete": "Delete",
"add": "Add",
"select": "Select", "select": "Select",
"name": "Name", "name": "Name",
"date": "Date", "date": "Date",
@ -51,7 +50,6 @@
"conditionals": "Conditionals", "conditionals": "Conditionals",
"preview": "Preview", "preview": "Preview",
"reset": "Reset", "reset": "Reset",
"edit": "Edit",
"donate": "Donate", "donate": "Donate",
"unlink": "Unlink Account", "unlink": "Unlink Account",
"sendPWR": "Send Password Reset", "sendPWR": "Send Password Reset",

View File

@ -43,7 +43,6 @@
"variables": "Variables", "variables": "Variables",
"preview": "Vista previa", "preview": "Vista previa",
"reset": "Reiniciar", "reset": "Reiniciar",
"edit": "Editar",
"extendExpiry": "Extender el vencimiento", "extendExpiry": "Extender el vencimiento",
"customizeMessages": "Personalizar mensajes", "customizeMessages": "Personalizar mensajes",
"customizeMessagesDescription": "Si no desea utilizar las plantillas de mensajes de jfa-go, puede crear las suyas con Markdown.", "customizeMessagesDescription": "Si no desea utilizar las plantillas de mensajes de jfa-go, puede crear las suyas con Markdown.",
@ -85,7 +84,6 @@
"notifyUserCreation": "Sobre la creación de usuarios", "notifyUserCreation": "Sobre la creación de usuarios",
"conditionals": "Condicionales", "conditionals": "Condicionales",
"donate": "Donar", "donate": "Donar",
"add": "Agregar",
"templates": "Plantillas", "templates": "Plantillas",
"contactThrough": "Contactar a través de:", "contactThrough": "Contactar a través de:",
"select": "Seleccionar", "select": "Seleccionar",

View File

@ -73,7 +73,6 @@
"variables": "Variables", "variables": "Variables",
"preview": "Aperçu", "preview": "Aperçu",
"reset": "Réinitialisation", "reset": "Réinitialisation",
"edit": "Éditer",
"customizeMessages": "Personnaliser les e-mails", "customizeMessages": "Personnaliser les e-mails",
"inviteDuration": "Durée de l'invitation", "inviteDuration": "Durée de l'invitation",
"advancedSettings": "Paramètres avancés", "advancedSettings": "Paramètres avancés",
@ -88,7 +87,6 @@
"extendExpiry": "Prolonger l'expiration", "extendExpiry": "Prolonger l'expiration",
"contactThrough": "Contacté par :", "contactThrough": "Contacté par :",
"sendPIN": "Demandez à l'utilisateur d'envoyer le code PIN ci-dessous au bot.", "sendPIN": "Demandez à l'utilisateur d'envoyer le code PIN ci-dessous au bot.",
"add": "Ajouter",
"select": "Sélectionner", "select": "Sélectionner",
"findDiscordUser": "Trouver l'utilisateur Discord", "findDiscordUser": "Trouver l'utilisateur Discord",
"linkMatrixDescription": "Entrez le nom d'utilisateur et le mot de passe de l'utilisateur pour lutilisateur comme bot. Une fois soumis, l'application va redémarrer.", "linkMatrixDescription": "Entrez le nom d'utilisateur et le mot de passe de l'utilisateur pour lutilisateur comme bot. Une fois soumis, l'application va redémarrer.",

View File

@ -18,7 +18,6 @@
"create": "Létrehozás", "create": "Létrehozás",
"apply": "Alkalmaz", "apply": "Alkalmaz",
"delete": "Törlés", "delete": "Törlés",
"add": "Hozzáadás",
"select": "Kiválasztás", "select": "Kiválasztás",
"name": "Név", "name": "Név",
"date": "Dátum", "date": "Dátum",
@ -49,7 +48,6 @@
"conditionals": "Feltételek", "conditionals": "Feltételek",
"preview": "Előnézet", "preview": "Előnézet",
"reset": "Visszaállítás", "reset": "Visszaállítás",
"edit": "Szerkesztés",
"donate": "Támogatás", "donate": "Támogatás",
"sendPWR": "Jelszó visszaállítás küldése", "sendPWR": "Jelszó visszaállítás küldése",
"contactThrough": "", "contactThrough": "",

View File

@ -66,7 +66,6 @@
"variables": "Variabel", "variables": "Variabel",
"preview": "Pratinjau", "preview": "Pratinjau",
"reset": "Setel ulang", "reset": "Setel ulang",
"edit": "Edit",
"customizeMessages": "Sesuaikan Email", "customizeMessages": "Sesuaikan Email",
"customizeMessagesDescription": "Jika Anda tidak ingin menggunakan templat email jfa-go, Anda dapat membuatnya sendiri menggunakan Markdown.", "customizeMessagesDescription": "Jika Anda tidak ingin menggunakan templat email jfa-go, Anda dapat membuatnya sendiri menggunakan Markdown.",
"announce": "Mengumumkan", "announce": "Mengumumkan",

View File

@ -71,7 +71,6 @@
"customizeMessagesDescription": "Als je de e-mailsjablonen van jfa-go niet wilt gebruiken, kun je met gebruik van Markdown je eigen aanmaken.", "customizeMessagesDescription": "Als je de e-mailsjablonen van jfa-go niet wilt gebruiken, kun je met gebruik van Markdown je eigen aanmaken.",
"preview": "Voorbeeld", "preview": "Voorbeeld",
"reset": "Reset", "reset": "Reset",
"edit": "Bewerken",
"customizeMessages": "E-mails aanpassen", "customizeMessages": "E-mails aanpassen",
"inviteDuration": "Geldigheidsduur uitnodiging", "inviteDuration": "Geldigheidsduur uitnodiging",
"userExpiryDescription": "Een bepaalde tijd na elke aanmelding, wordt de account verwijderd/uitgeschakeld door jfa-go. Dit kan aangepast worden in de instellingen.", "userExpiryDescription": "Een bepaalde tijd na elke aanmelding, wordt de account verwijderd/uitgeschakeld door jfa-go. Dit kan aangepast worden in de instellingen.",
@ -87,7 +86,6 @@
"donate": "Doneer", "donate": "Doneer",
"contactThrough": "Stuur bericht via:", "contactThrough": "Stuur bericht via:",
"sendPIN": "Vraag de gebruiker om onderstaande pincode naar de bot te sturen.", "sendPIN": "Vraag de gebruiker om onderstaande pincode naar de bot te sturen.",
"add": "Voeg toe",
"searchDiscordUser": "Begin de Discord gebruikersnaam te typen om de gebruiker te vinden.", "searchDiscordUser": "Begin de Discord gebruikersnaam te typen om de gebruiker te vinden.",
"linkMatrixDescription": "Vul de gebruikersnaam en wachtwoord in van de gebruiker om als bot te gebruiken. De app start zodra ze zijn verstuurd.", "linkMatrixDescription": "Vul de gebruikersnaam en wachtwoord in van de gebruiker om als bot te gebruiken. De app start zodra ze zijn verstuurd.",
"select": "Selecteer", "select": "Selecteer",

View File

@ -18,7 +18,6 @@
"create": "", "create": "",
"apply": "", "apply": "",
"delete": "", "delete": "",
"add": "",
"select": "", "select": "",
"name": "Imię", "name": "Imię",
"date": "Data", "date": "Data",
@ -49,7 +48,6 @@
"conditionals": "", "conditionals": "",
"preview": "", "preview": "",
"reset": "Zresetuj", "reset": "Zresetuj",
"edit": "Edytuj",
"donate": "", "donate": "",
"sendPWR": "", "sendPWR": "",
"contactThrough": "", "contactThrough": "",

View File

@ -71,7 +71,6 @@
"variables": "Variáveis", "variables": "Variáveis",
"preview": "Pre-visualizar", "preview": "Pre-visualizar",
"reset": "Redefinir", "reset": "Redefinir",
"edit": "Editar",
"customizeMessages": "Customizar Emails", "customizeMessages": "Customizar Emails",
"userExpiryDescription": "Após um determinado período de tempo de cada inscrição, o jfa-go apagará/desabilitará a conta. Você pode alterar essa opção nas configurações.", "userExpiryDescription": "Após um determinado período de tempo de cada inscrição, o jfa-go apagará/desabilitará a conta. Você pode alterar essa opção nas configurações.",
"inviteDuration": "Duração do Convite", "inviteDuration": "Duração do Convite",
@ -89,7 +88,6 @@
"sendPIN": "Peça que o usuário envie o PIN abaixo para o bot.", "sendPIN": "Peça que o usuário envie o PIN abaixo para o bot.",
"searchDiscordUser": "Digite o nome de usuário do Discord.", "searchDiscordUser": "Digite o nome de usuário do Discord.",
"findDiscordUser": "Encontrar usuário Discord", "findDiscordUser": "Encontrar usuário Discord",
"add": "Adicionar",
"linkMatrixDescription": "Digite o nome de usuário e a senha para usar como bot. Depois de enviado, o aplicativo será reiniciado.", "linkMatrixDescription": "Digite o nome de usuário e a senha para usar como bot. Depois de enviado, o aplicativo será reiniciado.",
"select": "Selecionar", "select": "Selecionar",
"templates": "Modelos", "templates": "Modelos",

View File

@ -34,7 +34,6 @@
"variables": "Variabler", "variables": "Variabler",
"preview": "Förhandsvisning", "preview": "Förhandsvisning",
"reset": "Återställ", "reset": "Återställ",
"edit": "Redigera",
"customizeMessages": "Anpassa e-post", "customizeMessages": "Anpassa e-post",
"customizeMessagesDescription": "Om du inte vill använda jfa-go's e-postmallar, så kan du skapa dina egna med Markdown.", "customizeMessagesDescription": "Om du inte vill använda jfa-go's e-postmallar, så kan du skapa dina egna med Markdown.",
"markdownSupported": "Markdown stöds.", "markdownSupported": "Markdown stöds.",

View File

@ -18,7 +18,6 @@
"create": "Tạo mới", "create": "Tạo mới",
"apply": "Áp dụng", "apply": "Áp dụng",
"delete": "Xóa", "delete": "Xóa",
"add": "Thêm",
"select": "Chọn", "select": "Chọn",
"name": "Tên", "name": "Tên",
"date": "Ngày", "date": "Ngày",
@ -48,7 +47,6 @@
"conditionals": "Điều kiện", "conditionals": "Điều kiện",
"preview": "Xem trước", "preview": "Xem trước",
"reset": "Đặt lại", "reset": "Đặt lại",
"edit": "Chỉnh sửa",
"donate": "Đóng góp", "donate": "Đóng góp",
"sendPWR": "Gửi Đặt lại Mật khẩu", "sendPWR": "Gửi Đặt lại Mật khẩu",
"contactThrough": "Liên lạc qua:", "contactThrough": "Liên lạc qua:",

View File

@ -18,7 +18,6 @@
"create": "创建", "create": "创建",
"apply": "申请", "apply": "申请",
"delete": "删除", "delete": "删除",
"add": "添加",
"select": "选择", "select": "选择",
"name": "名称", "name": "名称",
"date": "日期", "date": "日期",
@ -47,7 +46,6 @@
"conditionals": "条件性条款", "conditionals": "条件性条款",
"preview": "预览", "preview": "预览",
"reset": "重设", "reset": "重设",
"edit": "编辑",
"donate": "捐助", "donate": "捐助",
"contactThrough": "联系方式:", "contactThrough": "联系方式:",
"extendExpiry": "延长有效期", "extendExpiry": "延长有效期",

View File

@ -18,7 +18,6 @@
"create": "創建", "create": "創建",
"apply": "應用", "apply": "應用",
"delete": "刪除", "delete": "刪除",
"add": "添加",
"select": "選擇", "select": "選擇",
"name": "帳戶名稱", "name": "帳戶名稱",
"date": "日期", "date": "日期",
@ -49,7 +48,6 @@
"conditionals": "條件", "conditionals": "條件",
"preview": "預覽", "preview": "預覽",
"reset": "重設", "reset": "重設",
"edit": "編輯",
"donate": "捐贈", "donate": "捐贈",
"sendPWR": "發送密碼重置", "sendPWR": "發送密碼重置",
"contactThrough": "聯繫方式:", "contactThrough": "聯繫方式:",

View File

@ -32,7 +32,9 @@
"disabled": "Deaktiveret", "disabled": "Deaktiveret",
"reEnable": "Genaktiver", "reEnable": "Genaktiver",
"disable": "Deaktiver", "disable": "Deaktiver",
"expiry": "Udløb" "expiry": "Udløb",
"add": "Tilføj",
"edit": "Rediger"
}, },
"notifications": { "notifications": {
"errorLoginBlank": "Brugernavnet og/eller adgangskoden blev efterladt tomme.", "errorLoginBlank": "Brugernavnet og/eller adgangskoden blev efterladt tomme.",

View File

@ -32,7 +32,9 @@
"disabled": "Deaktiviert", "disabled": "Deaktiviert",
"reEnable": "Wieder aktivieren", "reEnable": "Wieder aktivieren",
"disable": "Deaktivieren", "disable": "Deaktivieren",
"expiry": "Ablaufdatum" "expiry": "Ablaufdatum",
"add": "Hinzufügen",
"edit": "Bearbeiten"
}, },
"notifications": { "notifications": {
"errorLoginBlank": "Der Benutzername und/oder das Passwort wurden nicht ausgefüllt.", "errorLoginBlank": "Der Benutzername und/oder das Passwort wurden nicht ausgefüllt.",

View File

@ -23,7 +23,8 @@
"disabled": "Απενεργοποιημένο", "disabled": "Απενεργοποιημένο",
"reEnable": "Επανα-ενεργοποίηση", "reEnable": "Επανα-ενεργοποίηση",
"disable": "Απενεργοποίηση", "disable": "Απενεργοποίηση",
"expiry": "Λήξη" "expiry": "Λήξη",
"edit": "Επεξεργασία"
}, },
"notifications": { "notifications": {
"errorLoginBlank": "Το όνομα χρήστη και/ή ο κωδικός ήταν κενά.", "errorLoginBlank": "Το όνομα χρήστη και/ή ο κωδικός ήταν κενά.",

View File

@ -32,7 +32,9 @@
"disabled": "Disabled", "disabled": "Disabled",
"reEnable": "Re-enable", "reEnable": "Re-enable",
"disable": "Disable", "disable": "Disable",
"expiry": "Expiry" "expiry": "Expiry",
"add": "Add",
"edit": "Edit"
}, },
"notifications": { "notifications": {
"errorLoginBlank": "The username and/or password was left blank.", "errorLoginBlank": "The username and/or password was left blank.",

View File

@ -33,9 +33,13 @@
"reEnable": "Re-enable", "reEnable": "Re-enable",
"disable": "Disable", "disable": "Disable",
"contactMethods": "Contact Methods", "contactMethods": "Contact Methods",
"addContactMethod": "Add Contact Method",
"editContactMethod": "Edit Contact Method",
"accountStatus": "Account Status", "accountStatus": "Account Status",
"notSet": "Not set", "notSet": "Not set",
"expiry": "Expiry" "expiry": "Expiry",
"add": "Add",
"edit": "Edit"
}, },
"notifications": { "notifications": {
"errorLoginBlank": "The username and/or password were left blank.", "errorLoginBlank": "The username and/or password were left blank.",
@ -58,4 +62,4 @@
"plural": "{n} Days" "plural": "{n} Days"
} }
} }
} }

View File

@ -32,7 +32,9 @@
"disabled": "Desactivado", "disabled": "Desactivado",
"reEnable": "Reactivar", "reEnable": "Reactivar",
"disable": "Desactivar", "disable": "Desactivar",
"expiry": "Expiración" "expiry": "Expiración",
"add": "Agregar",
"edit": "Editar"
}, },
"notifications": { "notifications": {
"errorLoginBlank": "El nombre de usuario y/o la contraseña se dejaron en blanco.", "errorLoginBlank": "El nombre de usuario y/o la contraseña se dejaron en blanco.",

View File

@ -32,7 +32,9 @@
"disabled": "Désactivé", "disabled": "Désactivé",
"reEnable": "Ré-activé", "reEnable": "Ré-activé",
"disable": "Désactivé", "disable": "Désactivé",
"expiry": "Expiration" "expiry": "Expiration",
"add": "Ajouter",
"edit": "Éditer"
}, },
"notifications": { "notifications": {
"errorLoginBlank": "Le nom d'utilisateur et/ou le mot de passe sont vides.", "errorLoginBlank": "Le nom d'utilisateur et/ou le mot de passe sont vides.",

View File

@ -10,7 +10,9 @@
"disabled": "Tiltva", "disabled": "Tiltva",
"reEnable": "Újra engedélyezés", "reEnable": "Újra engedélyezés",
"disable": "Letiltás", "disable": "Letiltás",
"expiry": "Lejárat" "expiry": "Lejárat",
"add": "Hozzáadás",
"edit": "Szerkesztés"
}, },
"notifications": {}, "notifications": {},
"quantityStrings": {} "quantityStrings": {}

View File

@ -17,7 +17,8 @@
"time12h": "Waktu 12 jam", "time12h": "Waktu 12 jam",
"theme": "Tema", "theme": "Tema",
"login": "Masuk", "login": "Masuk",
"logout": "Keluar" "logout": "Keluar",
"edit": "Edit"
}, },
"notifications": { "notifications": {
"errorLoginBlank": "Nama pengguna dan / atau sandi kosong.", "errorLoginBlank": "Nama pengguna dan / atau sandi kosong.",

View File

@ -32,7 +32,9 @@
"disabled": "Uitgeschakeld", "disabled": "Uitgeschakeld",
"reEnable": "Opnieuw inschakelen", "reEnable": "Opnieuw inschakelen",
"disable": "Uitschakelen", "disable": "Uitschakelen",
"expiry": "Verloop" "expiry": "Verloop",
"add": "Voeg toe",
"edit": "Bewerken"
}, },
"notifications": { "notifications": {
"errorLoginBlank": "De gebruikersnaam en/of wachtwoord is leeg.", "errorLoginBlank": "De gebruikersnaam en/of wachtwoord is leeg.",

View File

@ -29,7 +29,8 @@
"enabled": "Włączone", "enabled": "Włączone",
"disabled": "Wyłączone", "disabled": "Wyłączone",
"disable": "Wyłączone", "disable": "Wyłączone",
"expiry": "Wygasa" "expiry": "Wygasa",
"edit": "Edytuj"
}, },
"notifications": { "notifications": {
"errorConnection": "Nie udało się połączyć z jfa-go.", "errorConnection": "Nie udało się połączyć z jfa-go.",

View File

@ -32,7 +32,9 @@
"disabled": "Desativado", "disabled": "Desativado",
"reEnable": "Reativar", "reEnable": "Reativar",
"disable": "Desativar", "disable": "Desativar",
"expiry": "Expira" "expiry": "Expira",
"add": "Adicionar",
"edit": "Editar"
}, },
"notifications": { "notifications": {
"errorLoginBlank": "O nome de usuário e/ou senha foram deixados em branco.", "errorLoginBlank": "O nome de usuário e/ou senha foram deixados em branco.",

View File

@ -20,7 +20,8 @@
"admin": "Admin", "admin": "Admin",
"enabled": "Aktiverad", "enabled": "Aktiverad",
"disabled": "Inaktiverad", "disabled": "Inaktiverad",
"expiry": "Löper ut" "expiry": "Löper ut",
"edit": "Redigera"
}, },
"notifications": { "notifications": {
"errorLoginBlank": "Användarnamnet och/eller lösenordet lämnades tomt.", "errorLoginBlank": "Användarnamnet och/eller lösenordet lämnades tomt.",

View File

@ -10,7 +10,9 @@
"disabled": "Tắt", "disabled": "Tắt",
"reEnable": "Mở lại", "reEnable": "Mở lại",
"disable": "Tắt", "disable": "Tắt",
"expiry": "Hết hạn" "expiry": "Hết hạn",
"add": "Thêm",
"edit": "Chỉnh sửa"
}, },
"notifications": { "notifications": {
"errorConnection": "Không thể kết nối với jfa-go.", "errorConnection": "Không thể kết nối với jfa-go.",

View File

@ -32,7 +32,9 @@
"disabled": "已禁用", "disabled": "已禁用",
"reEnable": "重新启用", "reEnable": "重新启用",
"disable": "禁用", "disable": "禁用",
"expiry": "到期" "expiry": "到期",
"add": "添加",
"edit": "编辑"
}, },
"notifications": { "notifications": {
"errorLoginBlank": "用户名/密码留空。", "errorLoginBlank": "用户名/密码留空。",

View File

@ -32,7 +32,9 @@
"disabled": "已禁用", "disabled": "已禁用",
"reEnable": "重新啟用", "reEnable": "重新啟用",
"disable": "禁用", "disable": "禁用",
"expiry": "到期" "expiry": "到期",
"add": "添加",
"edit": "編輯"
}, },
"notifications": { "notifications": {
"errorLoginBlank": "帳戶名稱和/或密碼留空。", "errorLoginBlank": "帳戶名稱和/或密碼留空。",

View File

@ -391,3 +391,14 @@ type MyDetailsContactMethodsDTO struct {
Value string `json:"value"` Value string `json:"value"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
} }
type ModifyMyEmailDTO struct {
Email string `json:"email"`
}
type ConfirmationTarget int
const (
UserEmailChange ConfirmationTarget = iota
NoOp
)

View File

@ -147,6 +147,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
router.GET(p+"/my/account", app.MyUserPage) router.GET(p+"/my/account", app.MyUserPage)
router.GET(p+"/my/token/login", app.getUserTokenLogin) router.GET(p+"/my/token/login", app.getUserTokenLogin)
router.GET(p+"/my/token/refresh", app.getUserTokenRefresh) router.GET(p+"/my/token/refresh", app.getUserTokenRefresh)
router.GET(p+"/my/confirm/:jwt", app.ConfirmMyAction)
} }
} }
if *SWAGGER { if *SWAGGER {
@ -229,6 +230,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
user.GET(p+"/details", app.MyDetails) user.GET(p+"/details", app.MyDetails)
user.POST(p+"/contact", app.SetMyContactMethods) user.POST(p+"/contact", app.SetMyContactMethods)
user.POST(p+"/logout", app.LogoutUser) user.POST(p+"/logout", app.LogoutUser)
user.POST(p+"/email", app.ModifyMyEmail)
} }
} }
} }

View File

@ -35,7 +35,9 @@
"contactMethods": "common", "contactMethods": "common",
"accountStatus": "common", "accountStatus": "common",
"notSet": "common", "notSet": "common",
"expiry": "admin" "expiry": "common",
"add": "admin",
"edit": "admin"
}, },
"notifications": { "notifications": {
"errorLoginBlank": "common", "errorLoginBlank": "common",

View File

@ -111,6 +111,7 @@ declare interface Modals {
matrix: Modal; matrix: Modal;
sendPWR?: Modal; sendPWR?: Modal;
logs: Modal; logs: Modal;
email?: Modal;
} }
interface Invite { interface Invite {

View File

@ -1,7 +1,7 @@
import { ThemeManager } from "./modules/theme.js"; import { ThemeManager } from "./modules/theme.js";
import { lang, LangFile, loadLangSelector } from "./modules/lang.js"; import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
import { Modal } from "./modules/modal.js"; import { Modal } from "./modules/modal.js";
import { _get, _post, notificationBox, whichAnimationEvent, toDateString } from "./modules/common.js"; import { _get, _post, notificationBox, whichAnimationEvent, toDateString, toggleLoader } from "./modules/common.js";
import { Login } from "./modules/login.js"; import { Login } from "./modules/login.js";
interface userWindow extends Window { interface userWindow extends Window {
@ -25,6 +25,7 @@ window.modals = {} as Modals;
(() => { (() => {
window.modals.login = new Modal(document.getElementById("modal-login"), true); window.modals.login = new Modal(document.getElementById("modal-login"), true);
window.modals.email = new Modal(document.getElementById("modal-email"), false);
})(); })();
window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5); window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5);
@ -73,12 +74,12 @@ class ContactMethods {
this._buttons = {}; this._buttons = {};
} }
append = (name: string, details: MyDetailsContactMethod, icon: string) => { append = (name: string, details: MyDetailsContactMethod, icon: string, addEditFunc?: (add: boolean) => void) => {
const row = document.createElement("div"); const row = document.createElement("div");
row.classList.add("row", "flex-expand", "my-2"); row.classList.add("row", "flex-expand", "my-2");
row.innerHTML = ` let innerHTML = `
<div class="inline align-middle"> <div class="inline align-middle">
<span class="shield ~info" alt="${name}"> <span class="shield ~urge" alt="${name}">
<span class="icon"> <span class="icon">
${icon} ${icon}
</span> </span>
@ -86,12 +87,24 @@ class ContactMethods {
<span class="ml-2 font-bold">${(details.value == "") ? window.lang.strings("notSet") : details.value}</span> <span class="ml-2 font-bold">${(details.value == "") ? window.lang.strings("notSet") : details.value}</span>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<button class="user-contact-enabled-disabled button ~neutral"> <button class="user-contact-enabled-disabled button ~neutral" ${details.value =="" ? "disabled" : ""}>
<input type="checkbox" class="mr-2"> <input type="checkbox" class="mr-2">
<span>${window.lang.strings("enabled")}</span> <span>${window.lang.strings("enabled")}</span>
</button> </button>
`;
if (addEditFunc) {
innerHTML += `
<button class="user-contact-edit button ~info ml-2">
<i class="ri-${details.value == "" ? "add" : "edit"}-fill mr-2"></i>
<span>${details.value == "" ? window.lang.strings("add") : window.lang.strings("edit")}</span>
</button>
`;
}
innerHTML += `
</div> </div>
`; `;
row.innerHTML = innerHTML;
this._buttons[name] = { this._buttons[name] = {
element: row, element: row,
@ -102,11 +115,11 @@ class ContactMethods {
const checkbox = button.querySelector("input[type=checkbox]") as HTMLInputElement; const checkbox = button.querySelector("input[type=checkbox]") as HTMLInputElement;
const setButtonAppearance = () => { const setButtonAppearance = () => {
if (checkbox.checked) { if (checkbox.checked) {
button.classList.add("~info"); button.classList.add("~urge");
button.classList.remove("~neutral"); button.classList.remove("~neutral");
} else { } else {
button.classList.add("~neutral"); button.classList.add("~neutral");
button.classList.remove("~info"); button.classList.remove("~urge");
} }
}; };
const onPress = () => { const onPress = () => {
@ -124,6 +137,11 @@ class ContactMethods {
checkbox.checked = details.enabled; checkbox.checked = details.enabled;
setButtonAppearance(); setButtonAppearance();
if (addEditFunc) {
const addEditButton = row.querySelector(".user-contact-edit") as HTMLButtonElement;
addEditButton.onclick = () => addEditFunc(details.value == "");
}
this._content.appendChild(row); this._content.appendChild(row);
}; };
@ -220,6 +238,36 @@ var expiryCard = new ExpiryCard(statusCard);
var contactMethodList = new ContactMethods(contactCard); var contactMethodList = new ContactMethods(contactCard);
const addEditEmail = (add: boolean): void => {
console.log("call");
const heading = window.modals.email.modal.querySelector(".heading");
heading.innerHTML = (add ? window.lang.strings("addContactMethod") : window.lang.strings("editContactMethod")) + `<span class="modal-close">&times;</span>`;
const input = document.getElementById("modal-email-input") as HTMLInputElement;
input.value = "";
const confirmationRequired = window.modals.email.modal.querySelector(".confirmation-required");
confirmationRequired.classList.add("unfocused");
const content = window.modals.email.modal.querySelector(".content");
content.classList.remove("unfocused");
const submit = window.modals.email.modal.querySelector(".modal-submit") as HTMLButtonElement;
submit.onclick = () => {
toggleLoader(submit);
_post("/my/email", {"email": input.value}, (req: XMLHttpRequest) => {
if (req.readyState == 4 && (req.status == 303 || req.status == 200)) {
window.location.reload();
}
}, true, (req: XMLHttpRequest) => {
if (req.readyState == 4 && req.status == 401) {
content.classList.add("unfocused");
confirmationRequired.classList.remove("unfocused");
}
});
}
window.modals.email.show();
}
document.addEventListener("details-reload", () => { document.addEventListener("details-reload", () => {
_get("/my/details", null, (req: XMLHttpRequest) => { _get("/my/details", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
@ -244,16 +292,16 @@ document.addEventListener("details-reload", () => {
contactMethodList.clear(); contactMethodList.clear();
const contactMethods = [ const contactMethods: { name: string, icon: string, f: (add: boolean) => void }[] = [
["email", `<i class="ri-mail-fill"></i>`], {name: "email", icon: `<i class="ri-mail-fill ri-lg"></i>`, f: addEditEmail},
["discord", `<i class="ri-discord-fill"></i>`], {name: "discord", icon: `<i class="ri-discord-fill ri-lg"></i>`, f: null},
["telegram", `<i class="ri-telegram-fill"></i>`], {name: "telegram", icon: `<i class="ri-telegram-fill ri-lg"></i>`, f: null},
["matrix", `[m]`] {name: "matrix", icon: `<span class="font-bold">[m]</span>`, f: null}
]; ];
for (let method of contactMethods) { for (let method of contactMethods) {
if (method[0] in details) { if (method.name in details) {
contactMethodList.append(method[0], details[method[0]], method[1]); contactMethodList.append(method.name, details[method.name], method.icon, method.f);
} }
} }