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

Compare commits

...

6 Commits

Author SHA1 Message Date
609039baeb
userpage: change email (+ confirmation)
edit/add button added for email address. Confirmation works too.
2023-06-18 19:38:09 +01:00
03f1a3dbc0
userpage: expand contact card to fill height 2023-06-18 13:04:22 +01:00
75dc9d4d1d
userpage: store refresh token separately
stored as "user-refresh" fixes weird issues when two accounts are logged
in.
2023-06-18 12:30:23 +01:00
5beeeb958b
userpage: show expiry 2023-06-18 12:27:18 +01:00
a22f032924
userpage: show and allow modification of contact methods 2023-06-17 17:27:44 +01:00
3e034c85d6
auth: provide error message if account is disabled 2023-06-17 13:57:48 +01:00
54 changed files with 946 additions and 229 deletions

View File

@ -336,6 +336,10 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
respondBool(400, false, gc)
return
}
app.setContactMethods(req, gc)
}
func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Context) {
if tgUser, ok := app.storage.telegram[req.ID]; ok {
change := tgUser.Contact != req.Telegram
tgUser.Contact = req.Telegram

View File

@ -1,6 +1,14 @@
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.
// @Produce json
@ -19,6 +27,234 @@ func (app *appContext) MyDetails(gc *gin.Context) {
return
}
resp.Username = user.Name
resp.Admin = user.Policy.IsAdministrator
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.emails[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.discord[user.ID]; ok {
resp.Discord.Value = RenderDiscordUsername(discord)
resp.Discord.Enabled = discord.Contact
}
}
if telegramEnabled {
resp.Telegram = &MyDetailsContactMethodsDTO{}
if telegram, ok := app.storage.telegram[user.ID]; ok {
resp.Telegram.Value = telegram.Username
resp.Telegram.Enabled = telegram.Contact
}
}
if matrixEnabled {
resp.Matrix = &MyDetailsContactMethodsDTO{}
if matrix, ok := app.storage.matrix[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.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
}

11
auth.go
View File

@ -158,6 +158,11 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc
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
@ -232,9 +237,9 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
gc.JSON(200, getTokenDTO{token})
}
func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context) (claims jwt.MapClaims, ok bool) {
func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName string) (claims jwt.MapClaims, ok bool) {
ok = false
cookie, err := gc.Cookie("refresh")
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)
@ -280,7 +285,7 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context) (claims jwt.
// @tags Auth
func (app *appContext) getTokenRefresh(gc *gin.Context) {
app.debug.Println("Token requested (refresh token)")
claims, ok := app.decodeValidateRefreshCookie(gc)
claims, ok := app.decodeValidateRefreshCookie(gc, "refresh")
if !ok {
return
}

View File

@ -10,6 +10,7 @@ import (
"html/template"
"io"
"io/fs"
"net/url"
"os"
"strconv"
"strings"
@ -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 !strings.HasSuffix(inviteLink, "/invite") {
inviteLink += "/invite"
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, url.PathEscape(key))
}
inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key)
template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": username})
template["confirmationURL"] = inviteLink
template["message"] = message

View File

@ -17,6 +17,22 @@
<title>{{ .lang.Strings.pageTitle }}</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">&times;</span></span>
<p class="content mb-4">{{ .strings.confirmationRequiredMessage }}</p>
</div>
</div>
</div>
<div id="notification-box"></div>
<div class="top-4 left-4 absolute">
<span class="dropdown" tabindex="0" id="lang-dropdown">
@ -44,7 +60,18 @@
{{ template "login-modal.html" . }}
<div class="page-container">
<div class="card @low dark:~d_neutral mb-4" id="card-user">
<span class="heading"></span>
<span class="heading mb-2"></span>
</div>
<div class="grid grid-cols-2 gap-4">
<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 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>
<script src="{{ .urlBase }}/js/user.js" type="module"></script>

View File

@ -26,9 +26,10 @@ func (ls *adminLangs) getOptions() [][2]string {
type commonLangs map[string]commonLang
type commonLang struct {
Meta langMeta `json:"meta"`
Strings langSection `json:"strings"`
Notifications langSection `json:"notifications"`
Meta langMeta `json:"meta"`
Strings langSection `json:"strings"`
Notifications langSection `json:"notifications"`
QuantityStrings map[string]quantityString `json:"quantityStrings"`
}
type adminLang struct {
@ -58,6 +59,7 @@ type userLang struct {
notificationsJSON string
ValidationStrings map[string]quantityString `json:"validationStrings"`
validationStringsJSON string
QuantityStrings map[string]quantityString `json:"quantityStrings"`
JSON string
}

View File

@ -18,15 +18,9 @@
"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",
@ -35,7 +29,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",
@ -52,7 +45,6 @@
"conditionals": "Betingelser",
"preview": "Eksempel",
"reset": "Nulstil",
"edit": "Rediger",
"donate": "Doner",
"contactThrough": "Kontakt gennem:",
"extendExpiry": "Forlæng udløb",
@ -136,7 +128,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.",

View File

@ -66,7 +66,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",
@ -77,23 +76,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",
@ -129,7 +121,6 @@
"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.",

View File

@ -66,7 +66,6 @@
"variables": "Μεταβλητές",
"preview": "Προεπισκόπηση",
"reset": "Επαναφορά",
"edit": "Επεξεργασία",
"customizeMessages": "Παραμετροποίηση Emails",
"advancedSettings": "Προχωρημένες Ρυθμίσεις",
"customizeMessagesDescription": "Αν δεν θέλετε να ζρησιμοποιήσετε τα πρότυπα email του jfa-go, μπορείτε να δημιουργήσετε τα δικά σας με χρήση Markdown.",
@ -75,10 +74,6 @@
"download": "Λήψη",
"search": "Αναζήτηση",
"inviteDuration": "Διάρκεια Πρόσκλησης",
"enabled": "Ενεργοποιημένο",
"disabled": "Απενεργοποιημένο",
"admin": "Διαχειριστής",
"expiry": "Λήξη",
"userExpiry": "Λήξη Χρήστη",
"userExpiryDescription": "Μετά απο ένα καθορισμένο χρόνο μετά απο κάθε εγγραφή, το jfa-go θα διαγράφει/απενεργοποιεί τον λογαριασμό. Μπορείτε να αλλάξετε αυτή την συμπεριφορά στις ρυθμίσεις.",
"announce": "Ανακοίνωση",
@ -86,8 +81,6 @@
"message": "Μήνυμα",
"extendExpiry": "Παράταση λήξης",
"markdownSupported": "Το Markdown υποστυρίζεται.",
"reEnable": "Επανα-ενεργοποίηση",
"disable": "Απενεργοποίηση",
"inviteMonths": "Μήνες"
},
"notifications": {
@ -105,7 +98,6 @@
"errorCreateProfile": "Αποτυχία δημιουργίας του προφίλ {n}",
"errorSetDefaultProfile": "Αποτυχία ορισμού του προκαθορισμένου προφίλ.",
"errorLoadUsers": "Αποτυχία φόρτωσης χρηστών.",
"errorSaveSettings": "Αποτυχία αποθήκευσης ρυθμίσεων.",
"errorLoadSettings": "Αποτυχία φόρτωσης ρυθμίσεων.",
"errorSetOmbiDefaults": "Αποτυχία αποθήκευσης προκαθορισμένων ρυθμίσεων για το Ombi.",
"errorLoadOmbiUsers": "Αποτυχία φόρτωσης χρηστών Ombi.",

View File

@ -71,7 +71,6 @@
"apply": "Apply",
"delete": "Delete",
"updates": "Updates",
"expiry": "Expiry",
"variables": "Variables",
"preview": "Preview",
"markdownSupported": "Markdown is supported.",
@ -94,16 +93,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.",
@ -148,7 +141,6 @@
"settingsApplyRestartLater": "Apply, restart later",
"subject": "Subject",
"setExpiry": "Set expiry",
"admin": "Admin",
"download": "Download",
"search": "Search",
"advancedSettings": "Advanced Settings",
@ -187,7 +179,6 @@
"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)",

View File

@ -18,16 +18,10 @@
"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",
@ -38,7 +32,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",
@ -57,7 +50,6 @@
"conditionals": "Conditionals",
"preview": "Preview",
"reset": "Reset",
"edit": "Edit",
"donate": "Donate",
"unlink": "Unlink Account",
"sendPWR": "Send Password Reset",
@ -147,7 +139,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.",

View File

@ -20,11 +20,6 @@
"delete": "Eliminar",
"name": "Nombre",
"date": "Fecha",
"enabled": "Activado",
"disabled": "Desactivado",
"reEnable": "Reactivar",
"disable": "Desactivar",
"admin": "Administrador",
"updates": "Actualizaciones",
"update": "Actualizar",
"download": "Descargar",
@ -33,7 +28,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",
@ -49,7 +43,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.",
@ -91,7 +84,6 @@
"notifyUserCreation": "Sobre la creación de usuarios",
"conditionals": "Condicionales",
"donate": "Donar",
"add": "Agregar",
"templates": "Plantillas",
"contactThrough": "Contactar a través de:",
"select": "Seleccionar",
@ -133,7 +125,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.",

View File

@ -73,15 +73,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",
@ -94,7 +87,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 lutilisateur comme bot. Une fois soumis, l'application va redémarrer.",
@ -130,7 +122,6 @@
"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.",

View File

@ -18,16 +18,10 @@
"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",
@ -36,7 +30,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",
@ -55,7 +48,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": "",
@ -134,7 +126,6 @@
"errorCreateProfile": "",
"errorSetDefaultProfile": "",
"errorLoadUsers": "",
"errorSaveSettings": "",
"errorLoadSettings": "",
"errorSetOmbiProfile": "",
"errorLoadOmbiUsers": "",

View File

@ -66,7 +66,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",
@ -89,7 +88,6 @@
"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.",

View File

@ -71,14 +71,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",
@ -87,13 +82,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",
@ -129,7 +121,6 @@
"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.",

View File

@ -18,16 +18,10 @@
"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",
@ -36,7 +30,6 @@
"lastActiveTime": "Ostatnia aktywność",
"from": "Od",
"user": "Użytkownik",
"expiry": "Wygasa",
"userExpiry": "Użytkownik wygasa",
"userExpiryDescription": "",
"aboutProgram": "O",
@ -55,7 +48,6 @@
"conditionals": "",
"preview": "",
"reset": "Zresetuj",
"edit": "Edytuj",
"donate": "",
"sendPWR": "",
"contactThrough": "",
@ -136,7 +128,6 @@
"errorCreateProfile": "",
"errorSetDefaultProfile": "",
"errorLoadUsers": "",
"errorSaveSettings": "",
"errorLoadSettings": "",
"errorSetOmbiProfile": "",
"errorLoadOmbiUsers": "",

View File

@ -71,14 +71,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",
@ -87,15 +82,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",
@ -129,7 +121,6 @@
"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.",

View File

@ -34,7 +34,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.",
@ -73,11 +72,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"
@ -100,7 +95,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.",

View File

@ -18,16 +18,10 @@
"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ề",
@ -36,7 +30,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",
@ -54,7 +47,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:",
@ -135,7 +127,6 @@
"errorCreateProfile": "",
"errorSetDefaultProfile": "",
"errorLoadUsers": "",
"errorSaveSettings": "",
"errorLoadSettings": "",
"errorSetOmbiProfile": "",
"errorLoadOmbiUsers": "",

View File

@ -18,15 +18,9 @@
"create": "创建",
"apply": "申请",
"delete": "删除",
"add": "添加",
"select": "选择",
"name": "名称",
"date": "日期",
"enabled": "已启用",
"disabled": "已禁用",
"reEnable": "重新启用",
"disable": "禁用",
"admin": "管理员",
"updates": "更新",
"update": "更新",
"download": "下载",
@ -35,7 +29,6 @@
"lastActiveTime": "上次活动",
"from": "从",
"user": "用户",
"expiry": "到期",
"userExpiry": "用户到期",
"userExpiryDescription": "每次注册后的指定时间jfa-go 将删除/禁用该帐户。您可以在设置中更改此行为。",
"aboutProgram": "关于",
@ -53,7 +46,6 @@
"conditionals": "条件性条款",
"preview": "预览",
"reset": "重设",
"edit": "编辑",
"donate": "捐助",
"contactThrough": "联系方式:",
"extendExpiry": "延长有效期",
@ -137,7 +129,6 @@
"errorCreateProfile": "创建配置文件{n}失败",
"errorSetDefaultProfile": "设置默认配置文件失败。",
"errorLoadUsers": "加载用户列表失败。",
"errorSaveSettings": "无法保存设置。",
"errorLoadSettings": "加载配置列表失败。",
"errorSetOmbiDefaults": "存储Ombi默认值失败。",
"errorLoadOmbiUsers": "加载ombi用户列表失败。",

View File

@ -18,16 +18,10 @@
"create": "創建",
"apply": "應用",
"delete": "刪除",
"add": "添加",
"select": "選擇",
"name": "帳戶名稱",
"date": "日期",
"enabled": "已啟用",
"disabled": "已禁用",
"reEnable": "重新啟用",
"setExpiry": "設置到期時間",
"disable": "禁用",
"admin": "管理員",
"updates": "更新",
"update": "更新",
"download": "下載",
@ -36,7 +30,6 @@
"lastActiveTime": "上次啟用時間",
"from": "從",
"user": "帳戶",
"expiry": "到期",
"userExpiry": "帳戶到期",
"userExpiryDescription": "每次註冊后指定的時間jfa-go 將刪除/禁用該帳戶。您可以在設定中更改此行為。",
"aboutProgram": "關於",
@ -55,7 +48,6 @@
"conditionals": "條件",
"preview": "預覽",
"reset": "重設",
"edit": "編輯",
"donate": "捐贈",
"sendPWR": "發送密碼重置",
"contactThrough": "聯繫方式:",
@ -136,7 +128,6 @@
"errorCreateProfile": "無法創建設置文件 {n}",
"errorSetDefaultProfile": "無法設置預設設置文件。",
"errorLoadUsers": "無法讀取帳戶。",
"errorSaveSettings": "無法儲存設置。",
"errorLoadSettings": "無法讀取設置。",
"errorSetOmbiProfile": "無法儲存 ombi 設置文件。",
"errorLoadOmbiUsers": "無法讀取 ombi 帳戶。",

View File

@ -3,5 +3,6 @@
"name": "العربية (AR)"
},
"strings": {},
"notifications": {}
"notifications": {},
"quantityStrings": {}
}

View File

@ -26,12 +26,22 @@
"refresh": "Opdater",
"required": "Påkrævet",
"login": "Log på",
"logout": "Log ud"
"logout": "Log ud",
"admin": "Administrator",
"enabled": "Aktiveret",
"disabled": "Deaktiveret",
"reEnable": "Genaktiver",
"disable": "Deaktiver",
"expiry": "Udløb",
"add": "Tilføj",
"edit": "Rediger"
},
"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."
}
"error401Unauthorized": "Adgang nægtet. Prøv at genindlæse siden.",
"errorSaveSettings": "Kunne ikke gemme indstillingerne."
},
"quantityStrings": {}
}

View File

@ -26,12 +26,22 @@
"refresh": "Aktualisieren",
"required": "Erforderlich",
"login": "Anmelden",
"logout": "Abmelden"
"logout": "Abmelden",
"admin": "Admin",
"enabled": "Aktiviert",
"disabled": "Deaktiviert",
"reEnable": "Wieder aktivieren",
"disable": "Deaktivieren",
"expiry": "Ablaufdatum",
"add": "Hinzufügen",
"edit": "Bearbeiten"
},
"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."
}
"error401Unauthorized": "Unberechtigt. Versuch, die Seite zu aktualisieren.",
"errorSaveSettings": "Einstellungen konnten nicht gespeichert werden."
},
"quantityStrings": {}
}

View File

@ -17,12 +17,21 @@
"time12h": "12 Ώρες",
"theme": "Θέμα",
"login": "Σύνδεση",
"logout": "Αποσύνδεση"
"logout": "Αποσύνδεση",
"admin": "Διαχειριστής",
"enabled": "Ενεργοποιημένο",
"disabled": "Απενεργοποιημένο",
"reEnable": "Επανα-ενεργοποίηση",
"disable": "Απενεργοποίηση",
"expiry": "Λήξη",
"edit": "Επεξεργασία"
},
"notifications": {
"errorLoginBlank": "Το όνομα χρήστη και/ή ο κωδικός ήταν κενά.",
"errorConnection": "Δεν μπόρεσε να συνδεθεί με το jfa-go.",
"errorUnknown": "Άγνωστο σφάλμα.",
"error401Unauthorized": "Ανεξουσιοδότητος. Προσπαθήστε να κάνετε επαναφόρτωση την σελίδα."
}
"error401Unauthorized": "Ανεξουσιοδότητος. Προσπαθήστε να κάνετε επαναφόρτωση την σελίδα.",
"errorSaveSettings": "Αποτυχία αποθήκευσης ρυθμίσεων."
},
"quantityStrings": {}
}

View File

@ -26,12 +26,22 @@
"refresh": "Refresh",
"required": "Required",
"login": "Login",
"logout": "Logout"
"logout": "Logout",
"admin": "Admin",
"enabled": "Enabled",
"disabled": "Disabled",
"reEnable": "Re-enable",
"disable": "Disable",
"expiry": "Expiry",
"add": "Add",
"edit": "Edit"
},
"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."
}
"error401Unauthorized": "Unauthorised. Try refreshing the page.",
"errorSaveSettings": "Couldn't save settings."
},
"quantityStrings": {}
}

View File

@ -26,12 +26,40 @@
"refresh": "Refresh",
"required": "Required",
"login": "Login",
"logout": "Logout"
"logout": "Logout",
"admin": "Admin",
"enabled": "Enabled",
"disabled": "Disabled",
"reEnable": "Re-enable",
"disable": "Disable",
"contactMethods": "Contact Methods",
"addContactMethod": "Add Contact Method",
"editContactMethod": "Edit Contact Method",
"accountStatus": "Account Status",
"notSet": "Not set",
"expiry": "Expiry",
"add": "Add",
"edit": "Edit"
},
"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."
"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"
}
}
}
}

View File

@ -26,12 +26,22 @@
"refresh": "Refrescar",
"required": "Requerido",
"login": "Acceso",
"logout": "Cerrar sesión"
"logout": "Cerrar sesión",
"admin": "Administrador",
"enabled": "Activado",
"disabled": "Desactivado",
"reEnable": "Reactivar",
"disable": "Desactivar",
"expiry": "Expiración",
"add": "Agregar",
"edit": "Editar"
},
"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."
}
"error401Unauthorized": "No autorizado. Intente actualizar la página.",
"errorSaveSettings": "No se pudo guardar la configuración."
},
"quantityStrings": {}
}

View File

@ -24,5 +24,6 @@
"contactDiscord": "از طریق دیسکورد تماس بگیرید",
"theme": "موضوع"
},
"notifications": {}
"notifications": {},
"quantityStrings": {}
}

View File

@ -26,12 +26,22 @@
"refresh": "Actualiser",
"required": "Requis",
"login": "S'identifier",
"logout": "Se déconnecter"
"logout": "Se déconnecter",
"admin": "Administrateur",
"enabled": "Activé",
"disabled": "Désactivé",
"reEnable": "Ré-activé",
"disable": "Désactivé",
"expiry": "Expiration",
"add": "Ajouter",
"edit": "Éditer"
},
"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."
}
"error401Unauthorized": "Non autorisé. Essayez d'actualiser la page.",
"errorSaveSettings": "Impossible d'enregistrer les paramètres."
},
"quantityStrings": {}
}

View File

@ -4,7 +4,16 @@
},
"strings": {
"login": "Belépés",
"logout": "Kijelentkezé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"
},
"notifications": {}
"notifications": {},
"quantityStrings": {}
}

View File

@ -17,12 +17,15 @@
"time12h": "Waktu 12 jam",
"theme": "Tema",
"login": "Masuk",
"logout": "Keluar"
"logout": "Keluar",
"edit": "Edit"
},
"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."
}
"error401Unauthorized": "Tidak ter-otorisasi. Coba segarkan halaman.",
"errorSaveSettings": "Tidak dapat menyimpan pengaturan."
},
"quantityStrings": {}
}

View File

@ -26,5 +26,6 @@
"refresh": "Aggiorna",
"required": "Richiesto"
},
"notifications": {}
"notifications": {},
"quantityStrings": {}
}

View File

@ -26,12 +26,22 @@
"refresh": "Ververs",
"required": "Verplicht",
"login": "Inloggen",
"logout": "Uitloggen"
"logout": "Uitloggen",
"admin": "Beheerder",
"enabled": "Ingeschakeld",
"disabled": "Uitgeschakeld",
"reEnable": "Opnieuw inschakelen",
"disable": "Uitschakelen",
"expiry": "Verloop",
"add": "Voeg toe",
"edit": "Bewerken"
},
"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."
}
"error401Unauthorized": "Geen toegang. Probeer de pagina te vernieuwen.",
"errorSaveSettings": "Opslaan van instellingen mislukt."
},
"quantityStrings": {}
}

View File

@ -24,11 +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": {}
}

View File

@ -26,12 +26,22 @@
"refresh": "Atualizar",
"required": "Requeridos",
"login": "Login",
"logout": "Sair"
"logout": "Sair",
"admin": "Admin",
"enabled": "Habilitado",
"disabled": "Desativado",
"reEnable": "Reativar",
"disable": "Desativar",
"expiry": "Expira",
"add": "Adicionar",
"edit": "Editar"
},
"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."
}
"error401Unauthorized": "Não autorizado. Tente atualizar a página.",
"errorSaveSettings": "Não foi possível salvar as configurações."
},
"quantityStrings": {}
}

View File

@ -3,5 +3,6 @@
"name": "Română (ROU)"
},
"strings": {},
"notifications": {}
"notifications": {},
"quantityStrings": {}
}

View File

@ -26,5 +26,6 @@
"refresh": "Osveži",
"required": "Obvezno"
},
"notifications": {}
"notifications": {},
"quantityStrings": {}
}

View File

@ -16,12 +16,19 @@
"time12h": "12 timmarsklocka",
"theme": "Tema",
"login": "Logga in",
"logout": "Logga ut"
"logout": "Logga ut",
"admin": "Admin",
"enabled": "Aktiverad",
"disabled": "Inaktiverad",
"expiry": "Löper ut",
"edit": "Redigera"
},
"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."
}
"error401Unauthorized": "Obehörig. Prova att uppdatera sidan.",
"errorSaveSettings": "Det gick inte att spara inställningarna."
},
"quantityStrings": {}
}

View File

@ -4,10 +4,19 @@
},
"strings": {
"login": "Đăng nhập",
"logout": "Đăng xuất"
"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"
},
"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": {}
}

View File

@ -26,12 +26,22 @@
"refresh": "刷新",
"required": "必需的",
"login": "登录",
"logout": "登出"
"logout": "登出",
"admin": "管理员",
"enabled": "已启用",
"disabled": "已禁用",
"reEnable": "重新启用",
"disable": "禁用",
"expiry": "到期",
"add": "添加",
"edit": "编辑"
},
"notifications": {
"errorLoginBlank": "用户名/密码留空。",
"errorConnection": "无法连接到 jfa-go。",
"errorUnknown": "未知错误。",
"error401Unauthorized": "无授权。尝试刷新页面。"
}
"error401Unauthorized": "无授权。尝试刷新页面。",
"errorSaveSettings": "无法保存设置。"
},
"quantityStrings": {}
}

View File

@ -26,12 +26,22 @@
"refresh": "重新整理",
"required": "必填",
"login": "登錄",
"logout": "登出"
"logout": "登出",
"admin": "管理員",
"enabled": "已啟用",
"disabled": "已禁用",
"reEnable": "重新啟用",
"disable": "禁用",
"expiry": "到期",
"add": "添加",
"edit": "編輯"
},
"notifications": {
"errorLoginBlank": "帳戶名稱和/或密碼留空。",
"errorConnection": "無法連接到 jfa-go。",
"errorUnknown": "未知的錯誤。",
"error401Unauthorized": "未經授權。嘗試重新整理頁面。"
}
"error401Unauthorized": "未經授權。嘗試重新整理頁面。",
"errorSaveSettings": "無法儲存設置。"
},
"quantityStrings": {}
}

View File

@ -20,7 +20,7 @@
"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.",
"welcomeUser": "Welcome, {user}"
"welcomeUser": "Welcome, {user}!"
},
"notifications": {
"errorUserExists": "User already exists.",

View File

@ -376,6 +376,29 @@ type ReCaptchaResponseDTO struct {
// MyDetailsDTO is sent to the user page to personalize it for the user.
type MyDetailsDTO struct {
Id string `json:"id"`
Username string `json:"username"`
Id string `json:"id"`
Username string `json:"username"`
Expiry int64 `json:"expiry"`
Admin bool `json:"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
)

View File

@ -147,6 +147,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
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)
}
}
if *SWAGGER {
@ -227,6 +228,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
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)
}
}
}

View File

@ -0,0 +1,54 @@
{
"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": "admin",
"edit": "admin"
},
"notifications": {
"errorLoginBlank": "common",
"errorConnection": "common",
"errorUnknown": "common",
"error401Unauthorized": "common",
"errorSaveSettings": "common"
},
"quantityStrings": {
"year": "common",
"month": "common",
"day": "common"
}
}

View File

@ -23,13 +23,19 @@
"theme": "common",
"refresh": "common",
"required": "common",
"login": "admin",
"logout": "admin"
"login": "common",
"logout": "common",
"admin": "common",
"enabled": "common",
"disabled": "common",
"reEnable": "common",
"disable": "common"
},
"notifications": {
"errorLoginBlank": "admin",
"errorConnection": "admin",
"errorUnknown": "admin",
"error401Unauthorized": "admin"
"errorLoginBlank": "common",
"errorConnection": "common",
"errorUnknown": "common",
"error401Unauthorized": "common",
"errorSaveSettings": "admin"
}
}

View File

@ -144,6 +144,10 @@ func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
if err != nil {
return
}
err = st.loadLangEmail(filesystems...)
if err != nil {
return
}
err = st.loadLangUser(filesystems...)
if err != nil {
return
@ -152,10 +156,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
}
@ -202,6 +202,25 @@ func (common *commonLangs) patchCommonNotifications(to *langSection, from ...str
}
}
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{}
@ -283,10 +302,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
@ -437,6 +460,12 @@ func (st *Storage) loadLangUser(filesystems ...fs.FS) error {
}
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.User[lang.Meta.Fallback]

View File

@ -177,12 +177,6 @@ login.onLogin = () => {
}
}
login.login("", "");
login.bindLogout(document.getElementById("logout-button"));
(document.getElementById('logout-button') as HTMLButtonElement).onclick = () => _post("/logout", null, (req: XMLHttpRequest): boolean => {
if (req.readyState == 4 && req.status == 200) {
window.token = "";
location.reload();
return false;
}
});
login.login("", "");

View File

@ -1,17 +1,17 @@
import { Modal } from "../modules/modal.js";
import { toggleLoader } from "../modules/common.js";
import { toggleLoader, _post } from "../modules/common.js";
export class Login {
private _modal: Modal;
private _form: HTMLFormElement;
private _url: string;
private _onLogin: (username: string, password: string) => void;
private _logoutButton: HTMLElement = null;
constructor(modal: Modal, endpoint: string) {
this._url = window.URLBase + endpoint;
if (this._url[this._url.length-1] != '/') this._url += "/";
this._url += "token/";
this._modal = modal;
this._form = this._modal.asElement().querySelector(".form-login") as HTMLFormElement;
@ -29,6 +29,18 @@ export class Login {
};
}
bindLogout = (button: HTMLElement) => {
this._logoutButton = button;
this._logoutButton.classList.add("unfocused");
this._logoutButton.onclick = () => _post(this._url + "logout", null, (req: XMLHttpRequest): boolean => {
if (req.readyState == 4 && req.status == 200) {
window.token = "";
location.reload();
return false;
}
});
};
get onLogin() { return this._onLogin; }
set onLogin(f: (username: string, password: string) => void) { this._onLogin = f; }
@ -36,7 +48,7 @@ export class Login {
const req = new XMLHttpRequest();
req.responseType = 'json';
const refresh = (username == "" && password == "");
req.open("GET", this._url + (refresh ? "refresh" : "login"), true);
req.open("GET", this._url + (refresh ? "token/refresh" : "token/login"), true);
if (!refresh) {
req.setRequestHeader("Authorization", "Basic " + btoa(username + ":" + password));
}
@ -46,6 +58,10 @@ export class Login {
let errorMsg = window.lang.notif("errorConnection");
if (req.response) {
errorMsg = req.response["error"];
const langErrorMsg = window.lang.strings(errorMsg);
if (langErrorMsg) {
errorMsg = langErrorMsg;
}
}
if (!errorMsg) {
errorMsg = window.lang.notif("errorUnknown");
@ -62,7 +78,8 @@ export class Login {
this._onLogin(username, password);
}
this._modal.close();
document.getElementById("logout-button").classList.remove("unfocused");
if (this._logoutButton != null)
this._logoutButton.classList.remove("unfocused");
}
if (run) { run(+req.status); }
}

View File

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

View File

@ -1,7 +1,7 @@
import { ThemeManager } from "./modules/theme.js";
import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
import { Modal } from "./modules/modal.js";
import { _get, _post, notificationBox, whichAnimationEvent } from "./modules/common.js";
import { _get, _post, notificationBox, whichAnimationEvent, toDateString, toggleLoader } from "./modules/common.js";
import { Login } from "./modules/login.js";
interface userWindow extends Window {
@ -25,26 +25,299 @@ window.modals = {} as Modals;
(() => {
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);
var rootCard = document.getElementById("card-user");
var contactCard = document.getElementById("card-contact");
var statusCard = document.getElementById("card-status");
const login = new Login(window.modals.login as Modal, "/my/");
login.onLogin = () => {
console.log("Logged in.");
interface MyDetailsContactMethod {
value: string;
enabled: boolean;
}
interface MyDetails {
id: string;
username: string;
expiry: number;
admin: boolean;
disabled: boolean;
email?: MyDetailsContactMethod;
discord?: MyDetailsContactMethod;
telegram?: MyDetailsContactMethod;
matrix?: MyDetailsContactMethod;
}
interface ContactDTO {
email?: boolean;
discord?: boolean;
telegram?: boolean;
matrix?: boolean;
}
class ContactMethods {
private _card: HTMLElement;
private _content: HTMLElement;
private _buttons: { [name: string]: { element: HTMLElement, details: MyDetailsContactMethod } };
constructor (card: HTMLElement) {
this._card = card;
this._content = this._card.querySelector(".content");
this._buttons = {};
}
clear = () => {
this._content.textContent = "";
this._buttons = {};
}
append = (name: string, details: MyDetailsContactMethod, icon: string, addEditFunc?: (add: boolean) => void) => {
const row = document.createElement("div");
row.classList.add("row", "flex-expand", "my-2");
let innerHTML = `
<div class="inline align-middle">
<span class="shield ~urge" alt="${name}">
<span class="icon">
${icon}
</span>
</span>
<span class="ml-2 font-bold">${(details.value == "") ? window.lang.strings("notSet") : details.value}</span>
</div>
<div class="flex items-center">
<button class="user-contact-enabled-disabled button ~neutral" ${details.value =="" ? "disabled" : ""}>
<input type="checkbox" class="mr-2">
<span>${window.lang.strings("enabled")}</span>
</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>
`;
row.innerHTML = innerHTML;
this._buttons[name] = {
element: row,
details: details
};
const button = row.querySelector(".user-contact-enabled-disabled") as HTMLButtonElement;
const checkbox = button.querySelector("input[type=checkbox]") as HTMLInputElement;
const setButtonAppearance = () => {
if (checkbox.checked) {
button.classList.add("~urge");
button.classList.remove("~neutral");
} else {
button.classList.add("~neutral");
button.classList.remove("~urge");
}
};
const onPress = () => {
this._buttons[name].details.enabled = checkbox.checked;
setButtonAppearance();
this._save();
};
checkbox.onchange = onPress;
button.onclick = () => {
checkbox.checked = !checkbox.checked;
onPress();
};
checkbox.checked = details.enabled;
setButtonAppearance();
if (addEditFunc) {
const addEditButton = row.querySelector(".user-contact-edit") as HTMLButtonElement;
addEditButton.onclick = () => addEditFunc(details.value == "");
}
this._content.appendChild(row);
};
private _save = () => {
let data: ContactDTO = {};
for (let method of Object.keys(this._buttons)) {
data[method] = this._buttons[method].details.enabled;
}
_post("/my/contact", data, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
window.notifications.customError("errorSetNotify", window.lang.notif("errorSaveSettings"));
document.dispatchEvent(new CustomEvent("details-reload"));
}
}
});
};
}
class ExpiryCard {
private _card: HTMLElement;
private _expiry: Date;
private _aside: HTMLElement;
private _countdown: HTMLElement;
private _interval: number = null;
constructor(card: HTMLElement) {
this._card = card;
this._aside = this._card.querySelector(".user-expiry") as HTMLElement;
this._countdown = this._card.querySelector(".user-expiry-countdown") as HTMLElement;
}
private _drawCountdown = () => {
let now = new Date();
// Years, Months, Days
let ymd = [0, 0, 0];
while (now.getFullYear() != this._expiry.getFullYear()) {
ymd[0] += 1;
now.setFullYear(now.getFullYear()+1);
}
if (now.getMonth() > this._expiry.getMonth()) {
ymd[0] -=1;
now.setFullYear(now.getFullYear()-1);
}
while (now.getMonth() != this._expiry.getMonth()) {
ymd[1] += 1;
now.setMonth(now.getMonth() + 1);
}
if (now.getDate() > this._expiry.getDate()) {
ymd[1] -=1;
now.setMonth(now.getMonth()-1);
}
while (now.getDate() != this._expiry.getDate()) {
ymd[2] += 1;
now.setDate(now.getDate() + 1);
}
const langKeys = ["year", "month", "day"];
let innerHTML = ``;
for (let i = 0; i < langKeys.length; i++) {
if (ymd[i] == 0) continue;
const words = window.lang.quantity(langKeys[i], ymd[i]).split(" ");
innerHTML += `
<div class="row my-3">
<div class="inline baseline">
<span class="text-2xl">${words[0]}</span> <span class="text-gray-400 text-lg">${words[1]}</span>
</div>
</div>
`;
}
this._countdown.innerHTML = innerHTML;
};
get expiry(): Date { return this._expiry; };
set expiry(expiryUnix: number) {
if (this._interval !== null) {
window.clearInterval(this._interval);
this._interval = null;
}
if (expiryUnix == 0) return;
this._expiry = new Date(expiryUnix * 1000);
this._aside.textContent = window.lang.strings("yourAccountIsValidUntil").replace("{date}", toDateString(this._expiry));
this._card.classList.remove("unfocused");
this._interval = window.setInterval(this._drawCountdown, 60*1000);
this._drawCountdown();
}
}
var expiryCard = new ExpiryCard(statusCard);
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", () => {
_get("/my/details", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
window.notifications.customError("myDetailsError", req.response["error"]);
return;
}
window.jellyfinID = req.response["id"];
window.username = req.response["username"];
rootCard.querySelector(".heading").textContent = window.lang.strings("welcomeUser").replace("{user}", window.username);
const details: MyDetails = req.response as MyDetails;
window.jellyfinID = details.id;
window.username = details.username;
let innerHTML = `
<span>${window.lang.strings("welcomeUser").replace("{user}", window.username)}</span>
`;
if (details.admin) {
innerHTML += `<span class="chip ~info ml-4">${window.lang.strings("admin")}</span>`;
}
if (details.disabled) {
innerHTML += `<span class="chip ~warning ml-4">${window.lang.strings("disabled")}</span>`;
}
rootCard.querySelector(".heading").innerHTML = innerHTML;
contactMethodList.clear();
const contactMethods: { name: string, icon: string, f: (add: boolean) => void }[] = [
{name: "email", icon: `<i class="ri-mail-fill ri-lg"></i>`, f: addEditEmail},
{name: "discord", icon: `<i class="ri-discord-fill ri-lg"></i>`, f: null},
{name: "telegram", icon: `<i class="ri-telegram-fill ri-lg"></i>`, f: null},
{name: "matrix", icon: `<span class="font-bold">[m]</span>`, f: null}
];
for (let method of contactMethods) {
if (method.name in details) {
contactMethodList.append(method.name, details[method.name], method.icon, method.f);
}
}
expiryCard.expiry = details.expiry;
}
});
});
const login = new Login(window.modals.login as Modal, "/my/");
login.onLogin = () => {
console.log("Logged in.");
document.dispatchEvent(new CustomEvent("details-reload"));
};
login.bindLogout(document.getElementById("logout-button"));
login.login("", "");

View File

@ -60,7 +60,7 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) {
}
app.debug.Printf("Token generated for non-admin user \"%s\"", username)
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/my", gc.Request.URL.Hostname(), true, true)
gc.SetCookie("user-refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/my", gc.Request.URL.Hostname(), true, true)
gc.JSON(200, getTokenDTO{token})
}
@ -79,7 +79,7 @@ func (app *appContext) getUserTokenRefresh(gc *gin.Context) {
}
app.info.Println("UserToken request (refresh token)")
claims, ok := app.decodeValidateRefreshCookie(gc)
claims, ok := app.decodeValidateRefreshCookie(gc, "user-refresh")
if !ok {
return
}
@ -93,6 +93,6 @@ func (app *appContext) getUserTokenRefresh(gc *gin.Context) {
return
}
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/my", gc.Request.URL.Hostname(), true, true)
gc.SetCookie("user-refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/my", gc.Request.URL.Hostname(), true, true)
gc.JSON(200, getTokenDTO{jwt})
}