mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-29 12:30:11 +00:00
Compare commits
5 Commits
3b3f37365a
...
f07c60afb0
Author | SHA1 | Date | |
---|---|---|---|
f07c60afb0 | |||
6adbba54ce | |||
97db4d714a | |||
12ce669566 | |||
4496e1d509 |
@ -275,6 +275,7 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
|||||||
// @Failure 500 {object} boolResponse
|
// @Failure 500 {object} boolResponse
|
||||||
// @Param invCode path string true "invite Code"
|
// @Param invCode path string true "invite Code"
|
||||||
// @Router /my/discord/invite [get]
|
// @Router /my/discord/invite [get]
|
||||||
|
// @Security Bearer
|
||||||
// @tags User Page
|
// @tags User Page
|
||||||
func (app *appContext) MyDiscordServerInvite(gc *gin.Context) {
|
func (app *appContext) MyDiscordServerInvite(gc *gin.Context) {
|
||||||
if app.discord.inviteChannelName == "" {
|
if app.discord.inviteChannelName == "" {
|
||||||
@ -295,6 +296,7 @@ func (app *appContext) MyDiscordServerInvite(gc *gin.Context) {
|
|||||||
// @Failure 400 {object} stringResponse
|
// @Failure 400 {object} stringResponse
|
||||||
// Param service path string true "discord/telegram"
|
// Param service path string true "discord/telegram"
|
||||||
// @Router /my/pin/{service} [get]
|
// @Router /my/pin/{service} [get]
|
||||||
|
// @Security Bearer
|
||||||
// @tags User Page
|
// @tags User Page
|
||||||
func (app *appContext) GetMyPIN(gc *gin.Context) {
|
func (app *appContext) GetMyPIN(gc *gin.Context) {
|
||||||
service := gc.Param("service")
|
service := gc.Param("service")
|
||||||
@ -319,6 +321,7 @@ func (app *appContext) GetMyPIN(gc *gin.Context) {
|
|||||||
// @Failure 401 {object} boolResponse
|
// @Failure 401 {object} boolResponse
|
||||||
// @Param pin path string true "PIN code to check"
|
// @Param pin path string true "PIN code to check"
|
||||||
// @Router /my/discord/verified/{pin} [get]
|
// @Router /my/discord/verified/{pin} [get]
|
||||||
|
// @Security Bearer
|
||||||
// @tags User Page
|
// @tags User Page
|
||||||
func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
|
func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
|
||||||
pin := gc.Param("pin")
|
pin := gc.Param("pin")
|
||||||
@ -347,6 +350,7 @@ func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
|
|||||||
// @Failure 401 {object} boolResponse
|
// @Failure 401 {object} boolResponse
|
||||||
// @Param pin path string true "PIN code to check"
|
// @Param pin path string true "PIN code to check"
|
||||||
// @Router /my/telegram/verified/{pin} [get]
|
// @Router /my/telegram/verified/{pin} [get]
|
||||||
|
// @Security Bearer
|
||||||
// @tags User Page
|
// @tags User Page
|
||||||
func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
|
func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
|
||||||
pin := gc.Param("pin")
|
pin := gc.Param("pin")
|
||||||
@ -386,6 +390,7 @@ func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
|
|||||||
// @Failure 500 {object} boolResponse
|
// @Failure 500 {object} boolResponse
|
||||||
// @Param MatrixSendPINDTO body MatrixSendPINDTO true "User's Matrix ID."
|
// @Param MatrixSendPINDTO body MatrixSendPINDTO true "User's Matrix ID."
|
||||||
// @Router /my/matrix/user [post]
|
// @Router /my/matrix/user [post]
|
||||||
|
// @Security Bearer
|
||||||
// @tags User Page
|
// @tags User Page
|
||||||
func (app *appContext) MatrixSendMyPIN(gc *gin.Context) {
|
func (app *appContext) MatrixSendMyPIN(gc *gin.Context) {
|
||||||
var req MatrixSendPINDTO
|
var req MatrixSendPINDTO
|
||||||
@ -419,6 +424,7 @@ func (app *appContext) MatrixSendMyPIN(gc *gin.Context) {
|
|||||||
// @Param invCode path string true "invite Code"
|
// @Param invCode path string true "invite Code"
|
||||||
// @Param userID path string true "Matrix User ID"
|
// @Param userID path string true "Matrix User ID"
|
||||||
// @Router /my/matrix/verified/{userID}/{pin} [get]
|
// @Router /my/matrix/verified/{userID}/{pin} [get]
|
||||||
|
// @Security Bearer
|
||||||
// @tags User Page
|
// @tags User Page
|
||||||
func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) {
|
func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) {
|
||||||
userID := gc.Param("userID")
|
userID := gc.Param("userID")
|
||||||
@ -452,7 +458,8 @@ func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} boolResponse
|
// @Success 200 {object} boolResponse
|
||||||
// @Router /my/discord [delete]
|
// @Router /my/discord [delete]
|
||||||
// @Tags Users
|
// @Security Bearer
|
||||||
|
// @Tags User Page
|
||||||
func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
|
func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
|
||||||
app.storage.DeleteDiscordKey(gc.GetString("jfId"))
|
app.storage.DeleteDiscordKey(gc.GetString("jfId"))
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
@ -462,7 +469,8 @@ func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} boolResponse
|
// @Success 200 {object} boolResponse
|
||||||
// @Router /my/telegram [delete]
|
// @Router /my/telegram [delete]
|
||||||
// @Tags Users
|
// @Security Bearer
|
||||||
|
// @Tags User Page
|
||||||
func (app *appContext) UnlinkMyTelegram(gc *gin.Context) {
|
func (app *appContext) UnlinkMyTelegram(gc *gin.Context) {
|
||||||
app.storage.DeleteTelegramKey(gc.GetString("jfId"))
|
app.storage.DeleteTelegramKey(gc.GetString("jfId"))
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
@ -472,7 +480,8 @@ func (app *appContext) UnlinkMyTelegram(gc *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} boolResponse
|
// @Success 200 {object} boolResponse
|
||||||
// @Router /my/matrix [delete]
|
// @Router /my/matrix [delete]
|
||||||
// @Tags Users
|
// @Security Bearer
|
||||||
|
// @Tags User Page
|
||||||
func (app *appContext) UnlinkMyMatrix(gc *gin.Context) {
|
func (app *appContext) UnlinkMyMatrix(gc *gin.Context) {
|
||||||
app.storage.DeleteMatrixKey(gc.GetString("jfId"))
|
app.storage.DeleteMatrixKey(gc.GetString("jfId"))
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
@ -485,7 +494,7 @@ func (app *appContext) UnlinkMyMatrix(gc *gin.Context) {
|
|||||||
// @Failure 400 {object} boolResponse
|
// @Failure 400 {object} boolResponse
|
||||||
// @Failure 500 {object} boolResponse
|
// @Failure 500 {object} boolResponse
|
||||||
// @Router /my/password/reset/{address} [post]
|
// @Router /my/password/reset/{address} [post]
|
||||||
// @tags Users
|
// @Tags User Page
|
||||||
func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
||||||
// All requests should take 1 second, to make it harder to tell if a success occured or not.
|
// All requests should take 1 second, to make it harder to tell if a success occured or not.
|
||||||
timerWait := make(chan bool)
|
timerWait := make(chan bool)
|
||||||
@ -551,3 +560,70 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary Change your password, given the old one and the new one.
|
||||||
|
// @Produce json
|
||||||
|
// @Param ChangeMyPasswordDTO body ChangeMyPasswordDTO true "User's old & new passwords."
|
||||||
|
// @Success 204 {object} boolResponse
|
||||||
|
// @Failure 400 {object} PasswordValidation
|
||||||
|
// @Failure 401 {object} boolResponse
|
||||||
|
// @Failure 500 {object} boolResponse
|
||||||
|
// @Router /my/password [post]
|
||||||
|
// @Security Bearer
|
||||||
|
// @Tags User Page
|
||||||
|
func (app *appContext) ChangeMyPassword(gc *gin.Context) {
|
||||||
|
var req ChangeMyPasswordDTO
|
||||||
|
gc.BindJSON(&req)
|
||||||
|
if req.Old == "" || req.New == "" {
|
||||||
|
respondBool(400, false, gc)
|
||||||
|
}
|
||||||
|
validation := app.validator.validate(req.New)
|
||||||
|
for _, val := range validation {
|
||||||
|
if !val {
|
||||||
|
app.debug.Printf("%s: Change password failed: Invalid password", gc.GetString("jfId"))
|
||||||
|
gc.JSON(400, validation)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user, status, err := app.jf.UserByID(gc.GetString("jfId"), false)
|
||||||
|
if status != 200 || err != nil {
|
||||||
|
app.err.Printf("Failed to change password: couldn't find user (%d): %+v", status, err)
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Authenticate as user to confirm old password.
|
||||||
|
user, status, err = app.authJf.Authenticate(user.Name, req.Old)
|
||||||
|
if status != 200 || err != nil {
|
||||||
|
respondBool(401, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status, err = app.jf.SetPassword(gc.GetString("jfId"), req.Old, req.New)
|
||||||
|
if (status != 200 && status != 204) || err != nil {
|
||||||
|
respondBool(500, false, gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
|
func() {
|
||||||
|
ombiUser, status, err := app.getOmbiUser(gc.GetString("jfId"))
|
||||||
|
if status != 200 || err != nil {
|
||||||
|
app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", user.Name, status, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ombiUser["password"] = req.New
|
||||||
|
status, err = app.ombi.ModifyUser(ombiUser)
|
||||||
|
if status != 200 || err != nil {
|
||||||
|
app.err.Printf("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
cookie, err := gc.Cookie("user-refresh")
|
||||||
|
if err == nil {
|
||||||
|
app.invalidTokens = append(app.invalidTokens, cookie)
|
||||||
|
gc.SetCookie("refresh", "invalid", -1, "/my", gc.Request.URL.Hostname(), true, true)
|
||||||
|
} else {
|
||||||
|
app.debug.Printf("Couldn't get cookies: %s", err)
|
||||||
|
}
|
||||||
|
respondBool(204, true, gc)
|
||||||
|
}
|
||||||
|
@ -463,6 +463,7 @@ func (app *appContext) NewUser(gc *gin.Context) {
|
|||||||
for _, val := range validation {
|
for _, val := range validation {
|
||||||
if !val {
|
if !val {
|
||||||
valid = false
|
valid = false
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !valid {
|
if !valid {
|
||||||
|
1
api.go
1
api.go
@ -143,6 +143,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
respondBool(status, false, gc)
|
respondBool(status, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
delete(app.internalPWRs, req.PIN)
|
||||||
} else {
|
} else {
|
||||||
resp, status, err := app.jf.ResetPassword(req.PIN)
|
resp, status, err := app.jf.ResetPassword(req.PIN)
|
||||||
if status != 200 || err != nil || !resp.Success {
|
if status != 200 || err != nil || !resp.Success {
|
||||||
|
@ -394,7 +394,7 @@
|
|||||||
"value": "",
|
"value": "",
|
||||||
"depends_true": "enabled",
|
"depends_true": "enabled",
|
||||||
"required": "false",
|
"required": "false",
|
||||||
"description": "Jellyfin Login must be enabled to use this feature.",
|
"description": "Jellyfin Login must be enabled to use this feature, and password resets with a link must be enabled for self-service.",
|
||||||
"style": "critical"
|
"style": "critical"
|
||||||
},
|
},
|
||||||
"edit_note": {
|
"edit_note": {
|
||||||
|
@ -124,7 +124,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-initial">
|
<div class="flex-initial">
|
||||||
<div class="card ~neutral @low mb-4">
|
<div class="card ~neutral @low mb-4">
|
||||||
<span class="label supra" for="inv-uses">{{ .strings.passwordRequirementsHeader }}</span>
|
<span class="label supra">{{ .strings.passwordRequirementsHeader }}</span>
|
||||||
<ul>
|
<ul>
|
||||||
{{ range $key, $value := .requirements }}
|
{{ range $key, $value := .requirements }}
|
||||||
<li class="" id="requirement-{{ $key }}" min="{{ $value }}">
|
<li class="" id="requirement-{{ $key }}" min="{{ $value }}">
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
window.matrixEnabled = {{ .matrixEnabled }};
|
window.matrixEnabled = {{ .matrixEnabled }};
|
||||||
window.matrixRequired = {{ .matrixRequired }};
|
window.matrixRequired = {{ .matrixRequired }};
|
||||||
window.matrixUserID = "{{ .matrixUser }}";
|
window.matrixUserID = "{{ .matrixUser }}";
|
||||||
|
window.validationStrings = JSON.parse({{ .validationStrings }});
|
||||||
</script>
|
</script>
|
||||||
{{ template "header.html" . }}
|
{{ template "header.html" . }}
|
||||||
<title>{{ .lang.Strings.myAccount }}</title>
|
<title>{{ .lang.Strings.myAccount }}</title>
|
||||||
@ -113,10 +114,41 @@
|
|||||||
<span class="heading mb-2">{{ .strings.contactMethods }}</span>
|
<span class="heading mb-2">{{ .strings.contactMethods }}</span>
|
||||||
<div class="content flex justify-between flex-col h-100"></div>
|
<div class="content flex justify-between flex-col h-100"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card @low dark:~d_neutral unfocused" id="card-status">
|
<div>
|
||||||
<span class="heading mb-2">{{ .strings.expiry }}</span>
|
<div class="card @low dark:~d_neutral content" id="card-password">
|
||||||
<aside class="aside ~warning user-expiry my-4"></aside>
|
<span class="heading row mb-2">{{ .strings.changePassword }}</span>
|
||||||
<div class="user-expiry-countdown"></div>
|
<div class="">
|
||||||
|
<div class="my-2">
|
||||||
|
<span class="label supra row">{{ .strings.passwordRequirementsHeader }}</span>
|
||||||
|
<ul>
|
||||||
|
{{ range $key, $value := .requirements }}
|
||||||
|
<li class="" id="requirement-{{ $key }}" min="{{ $value }}">
|
||||||
|
<span class="badge lg ~positive requirement-valid"></span> <span class="content requirement-content"></span>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="my-2">
|
||||||
|
<label class="label supra" for="user-old-password">{{ .strings.oldPassword }}</label>
|
||||||
|
<input type="password" class="input ~neutral @low mt-2 mb-4" placeholder="{{ .strings.password }}" id="user-old-password" aria-label="{{ .strings.oldPassword }}">
|
||||||
|
<label class="label supra" for="user-new-password">{{ .strings.newPassword }}</label>
|
||||||
|
<input type="password" class="input ~neutral @low mt-2 mb-4" placeholder="{{ .strings.password }}" id="user-new-password" aria-label="{{ .strings.newPassword }}">
|
||||||
|
|
||||||
|
<label class="label supra" for="user-reenter-password">{{ .strings.reEnterPassword }}</label>
|
||||||
|
<input type="password" class="input ~neutral @low mt-2 mb-4" placeholder="{{ .strings.password }}" id="user-reenter-new-password" aria-label="{{ .strings.reEnterPassword }}">
|
||||||
|
<span class="button ~info @low full-width center mt-4" id="user-password-submit">
|
||||||
|
{{ .strings.changePassword }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="card @low dark:~d_neutral unfocused" id="card-status">
|
||||||
|
<span class="heading mb-2">{{ .strings.expiry }}</span>
|
||||||
|
<aside class="aside ~warning user-expiry my-4"></aside>
|
||||||
|
<div class="user-expiry-countdown"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
"accountDetails": "Details",
|
"accountDetails": "Details",
|
||||||
"emailAddress": "Email",
|
"emailAddress": "Email",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
|
"oldPassword": "Old Password",
|
||||||
|
"newPassword": "New Password",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"reEnterPassword": "Re-enter Password",
|
"reEnterPassword": "Re-enter Password",
|
||||||
"reEnterPasswordInvalid": "Passwords are not the same.",
|
"reEnterPasswordInvalid": "Passwords are not the same.",
|
||||||
@ -31,7 +33,8 @@
|
|||||||
"resetPasswordThroughJellyfin": "To reset your password, visit {jfLink} and press the \"Forgot Password\" button.",
|
"resetPasswordThroughJellyfin": "To reset your password, visit {jfLink} and press the \"Forgot Password\" button.",
|
||||||
"resetPasswordThroughLink": "To reset your password, enter your username, email address or a linked contact method username, and submit. A link will be sent to reset your password.",
|
"resetPasswordThroughLink": "To reset your password, enter your username, email address or a linked contact method username, and submit. A link will be sent to reset your password.",
|
||||||
"resetSent": "Reset Sent.",
|
"resetSent": "Reset Sent.",
|
||||||
"resetSentDescription": "If an account with the given username/contact method exists, a password reset link has been sent via all contact methods available. The code will expire in 30 minutes."
|
"resetSentDescription": "If an account with the given username/contact method exists, a password reset link has been sent via all contact methods available. The code will expire in 30 minutes.",
|
||||||
|
"changePassword": "Change Password"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"errorUserExists": "User already exists.",
|
"errorUserExists": "User already exists.",
|
||||||
@ -47,6 +50,8 @@
|
|||||||
"errorCaptcha": "Captcha incorrect.",
|
"errorCaptcha": "Captcha incorrect.",
|
||||||
"errorPassword": "Check password requirements.",
|
"errorPassword": "Check password requirements.",
|
||||||
"errorNoMatch": "Passwords don't match.",
|
"errorNoMatch": "Passwords don't match.",
|
||||||
|
"errorOldPassword": "Old password incorrect.",
|
||||||
|
"passwordChanged": "Password Changed.",
|
||||||
"verified": "Account verified."
|
"verified": "Account verified."
|
||||||
},
|
},
|
||||||
"validationStrings": {
|
"validationStrings": {
|
||||||
|
@ -408,3 +408,8 @@ const (
|
|||||||
type GetMyPINDTO struct {
|
type GetMyPINDTO struct {
|
||||||
PIN string `json:"pin"`
|
PIN string `json:"pin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChangeMyPasswordDTO struct {
|
||||||
|
Old string `json:"old"`
|
||||||
|
New string `json:"new"`
|
||||||
|
}
|
||||||
|
@ -241,6 +241,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
user.DELETE(p+"/discord", app.UnlinkMyDiscord)
|
user.DELETE(p+"/discord", app.UnlinkMyDiscord)
|
||||||
user.DELETE(p+"/telegram", app.UnlinkMyTelegram)
|
user.DELETE(p+"/telegram", app.UnlinkMyTelegram)
|
||||||
user.DELETE(p+"/matrix", app.UnlinkMyMatrix)
|
user.DELETE(p+"/matrix", app.UnlinkMyMatrix)
|
||||||
|
user.POST(p+"/password", app.ChangeMyPassword)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
40
ts/form.ts
40
ts/form.ts
@ -2,7 +2,7 @@ import { Modal } from "./modules/modal.js";
|
|||||||
import { notificationBox, whichAnimationEvent } from "./modules/common.js";
|
import { notificationBox, whichAnimationEvent } from "./modules/common.js";
|
||||||
import { _get, _post, toggleLoader, addLoader, removeLoader, toDateString } from "./modules/common.js";
|
import { _get, _post, toggleLoader, addLoader, removeLoader, toDateString } from "./modules/common.js";
|
||||||
import { loadLangSelector } from "./modules/lang.js";
|
import { loadLangSelector } from "./modules/lang.js";
|
||||||
import { initValidator } from "./modules/validator.js";
|
import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js";
|
||||||
import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js";
|
import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js";
|
||||||
|
|
||||||
interface formWindow extends Window {
|
interface formWindow extends Window {
|
||||||
@ -69,7 +69,7 @@ if (window.telegramEnabled) {
|
|||||||
const radio = document.getElementById("contact-via-telegram") as HTMLInputElement;
|
const radio = document.getElementById("contact-via-telegram") as HTMLInputElement;
|
||||||
radio.parentElement.classList.remove("unfocused");
|
radio.parentElement.classList.remove("unfocused");
|
||||||
radio.checked = true;
|
radio.checked = true;
|
||||||
validatorFunc();
|
validator.validate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ if (window.discordEnabled) {
|
|||||||
const radio = document.getElementById("contact-via-discord") as HTMLInputElement;
|
const radio = document.getElementById("contact-via-discord") as HTMLInputElement;
|
||||||
radio.parentElement.classList.remove("unfocused")
|
radio.parentElement.classList.remove("unfocused")
|
||||||
radio.checked = true;
|
radio.checked = true;
|
||||||
validatorFunc();
|
validator.validate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -133,7 +133,7 @@ if (window.matrixEnabled) {
|
|||||||
const radio = document.getElementById("contact-via-matrix") as HTMLInputElement;
|
const radio = document.getElementById("contact-via-matrix") as HTMLInputElement;
|
||||||
radio.parentElement.classList.remove("unfocused");
|
radio.parentElement.classList.remove("unfocused");
|
||||||
radio.checked = true;
|
radio.checked = true;
|
||||||
validatorFunc();
|
validator.validate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -162,7 +162,7 @@ if (window.userExpiryEnabled) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const form = document.getElementById("form-create") as HTMLFormElement;
|
const form = document.getElementById("form-create") as HTMLFormElement;
|
||||||
const submitButton = form.querySelector("input[type=submit]") as HTMLInputElement;
|
const submitInput = form.querySelector("input[type=submit]") as HTMLInputElement;
|
||||||
const submitSpan = form.querySelector("span.submit") as HTMLSpanElement;
|
const submitSpan = form.querySelector("span.submit") as HTMLSpanElement;
|
||||||
const submitText = submitSpan.textContent;
|
const submitText = submitSpan.textContent;
|
||||||
let usernameField = document.getElementById("create-username") as HTMLInputElement;
|
let usernameField = document.getElementById("create-username") as HTMLInputElement;
|
||||||
@ -242,17 +242,19 @@ interface GreCAPTCHA {
|
|||||||
|
|
||||||
declare var grecaptcha: GreCAPTCHA
|
declare var grecaptcha: GreCAPTCHA
|
||||||
|
|
||||||
let r = initValidator(passwordField, rePasswordField, submitButton, submitSpan, baseValidator);
|
let validatorConf: ValidatorConf = {
|
||||||
var requirements = r[0];
|
passwordField: passwordField,
|
||||||
var validatorFunc = r[1] as () => void;
|
rePasswordField: rePasswordField,
|
||||||
|
submitInput: submitInput,
|
||||||
|
submitButton: submitSpan,
|
||||||
|
validatorFunc: baseValidator
|
||||||
|
};
|
||||||
|
|
||||||
|
let validator = new Validator(validatorConf);
|
||||||
|
var requirements = validator.requirements;
|
||||||
|
|
||||||
if (window.emailRequired) {
|
if (window.emailRequired) {
|
||||||
emailField.addEventListener("keyup", validatorFunc)
|
emailField.addEventListener("keyup", validator.validate)
|
||||||
}
|
|
||||||
|
|
||||||
interface respDTO {
|
|
||||||
response: boolean;
|
|
||||||
error: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface sendDTO {
|
interface sendDTO {
|
||||||
@ -287,7 +289,7 @@ const genCaptcha = () => {
|
|||||||
if (window.captcha && !window.reCAPTCHA) {
|
if (window.captcha && !window.reCAPTCHA) {
|
||||||
genCaptcha();
|
genCaptcha();
|
||||||
(document.getElementById("captcha-regen") as HTMLSpanElement).onclick = genCaptcha;
|
(document.getElementById("captcha-regen") as HTMLSpanElement).onclick = genCaptcha;
|
||||||
captchaInput.onkeyup = validatorFunc;
|
captchaInput.onkeyup = validator.validate;
|
||||||
}
|
}
|
||||||
|
|
||||||
const create = (event: SubmitEvent) => {
|
const create = (event: SubmitEvent) => {
|
||||||
@ -333,11 +335,11 @@ const create = (event: SubmitEvent) => {
|
|||||||
}
|
}
|
||||||
_post("/newUser", send, (req: XMLHttpRequest) => {
|
_post("/newUser", send, (req: XMLHttpRequest) => {
|
||||||
if (req.readyState == 4) {
|
if (req.readyState == 4) {
|
||||||
let vals = req.response as respDTO;
|
let vals = req.response as ValidatorRespDTO;
|
||||||
let valid = true;
|
let valid = true;
|
||||||
for (let type in vals) {
|
for (let type in vals) {
|
||||||
if (requirements[type]) { requirements[type].valid = vals[type]; }
|
if (requirements[type]) requirements[type].valid = vals[type];
|
||||||
if (!vals[type]) { valid = false; }
|
if (!vals[type]) valid = false;
|
||||||
}
|
}
|
||||||
if (req.status == 200 && valid) {
|
if (req.status == 200 && valid) {
|
||||||
if (window.redirectToJellyfin == true) {
|
if (window.redirectToJellyfin == true) {
|
||||||
@ -386,6 +388,6 @@ const create = (event: SubmitEvent) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
validatorFunc();
|
validator.validate();
|
||||||
|
|
||||||
form.onsubmit = create;
|
form.onsubmit = create;
|
||||||
|
@ -9,6 +9,11 @@ interface pwValString {
|
|||||||
plural: string;
|
plural: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ValidatorRespDTO {
|
||||||
|
response: boolean;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface pwValStrings {
|
interface pwValStrings {
|
||||||
length: pwValString;
|
length: pwValString;
|
||||||
uppercase: pwValString;
|
uppercase: pwValString;
|
||||||
@ -60,8 +65,21 @@ class Requirement {
|
|||||||
validate = (count: number) => { this.valid = (count >= this._minCount); }
|
validate = (count: number) => { this.valid = (count >= this._minCount); }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initValidator(passwordField: HTMLInputElement, rePasswordField: HTMLInputElement, submitButton: HTMLInputElement, submitSpan: HTMLSpanElement, validatorFunc?: (oncomplete: (valid: boolean) => void) => void): ({ [category: string]: Requirement }|(() => void))[] {
|
export interface ValidatorConf {
|
||||||
var defaultPwValStrings: pwValStrings = {
|
passwordField: HTMLInputElement;
|
||||||
|
rePasswordField: HTMLInputElement;
|
||||||
|
submitInput?: HTMLInputElement;
|
||||||
|
submitButton: HTMLSpanElement;
|
||||||
|
validatorFunc?: (oncomplete: (valid: boolean) => void) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Validation { [name: string]: number }
|
||||||
|
export interface Requirements { [category: string]: Requirement };
|
||||||
|
|
||||||
|
export class Validator {
|
||||||
|
private _conf: ValidatorConf;
|
||||||
|
private _requirements: Requirements = {};
|
||||||
|
private _defaultPwValStrings: pwValStrings = {
|
||||||
length: {
|
length: {
|
||||||
singular: "Must have at least {n} character",
|
singular: "Must have at least {n} character",
|
||||||
plural: "Must have at least {n} characters"
|
plural: "Must have at least {n} characters"
|
||||||
@ -82,39 +100,34 @@ export function initValidator(passwordField: HTMLInputElement, rePasswordField:
|
|||||||
singular: "Must have at least {n} special character",
|
singular: "Must have at least {n} special character",
|
||||||
plural: "Must have at least {n} special characters"
|
plural: "Must have at least {n} special characters"
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private _checkPasswords = () => {
|
||||||
|
return this._conf.passwordField.value == this._conf.rePasswordField.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkPasswords = () => {
|
validate = () => {
|
||||||
return passwordField.value == rePasswordField.value;
|
const pw = this._checkPasswords();
|
||||||
}
|
this._conf.validatorFunc((valid: boolean) => {
|
||||||
|
|
||||||
const checkValidity = () => {
|
|
||||||
const pw = checkPasswords();
|
|
||||||
validatorFunc((valid: boolean) => {
|
|
||||||
if (pw && valid) {
|
if (pw && valid) {
|
||||||
rePasswordField.setCustomValidity("");
|
this._conf.rePasswordField.setCustomValidity("");
|
||||||
submitButton.disabled = false;
|
if (this._conf.submitInput) this._conf.submitInput.disabled = false;
|
||||||
submitSpan.removeAttribute("disabled");
|
this._conf.submitButton.removeAttribute("disabled");
|
||||||
} else if (!pw) {
|
} else if (!pw) {
|
||||||
rePasswordField.setCustomValidity(window.invalidPassword);
|
this._conf.rePasswordField.setCustomValidity(window.invalidPassword);
|
||||||
submitButton.disabled = true;
|
if (this._conf.submitInput) this._conf.submitInput.disabled = true;
|
||||||
submitSpan.setAttribute("disabled", "");
|
this._conf.submitButton.setAttribute("disabled", "");
|
||||||
} else {
|
} else {
|
||||||
rePasswordField.setCustomValidity("");
|
this._conf.rePasswordField.setCustomValidity("");
|
||||||
submitButton.disabled = true;
|
if (this._conf.submitInput) this._conf.submitInput.disabled = true;
|
||||||
submitSpan.setAttribute("disabled", "");
|
this._conf.submitButton.setAttribute("disabled", "");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
rePasswordField.addEventListener("keyup", checkValidity);
|
private _isInt = (s: string): boolean => { return (s >= '0' && s <= '9'); }
|
||||||
passwordField.addEventListener("keyup", checkValidity);
|
|
||||||
|
private _testStrings = (f: pwValString): boolean => {
|
||||||
|
|
||||||
// Incredible code right here
|
|
||||||
const isInt = (s: string): boolean => { return (s in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]); }
|
|
||||||
|
|
||||||
const testStrings = (f: pwValString): boolean => {
|
|
||||||
const testString = (s: string): boolean => {
|
const testString = (s: string): boolean => {
|
||||||
if (s == "" || !s.includes("{n}")) { return false; }
|
if (s == "" || !s.includes("{n}")) { return false; }
|
||||||
return true;
|
return true;
|
||||||
@ -122,14 +135,12 @@ export function initValidator(passwordField: HTMLInputElement, rePasswordField:
|
|||||||
return testString(f.singular) && testString(f.plural);
|
return testString(f.singular) && testString(f.plural);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Validation { [name: string]: number }
|
private _validate = (s: string): Validation => {
|
||||||
|
|
||||||
const validate = (s: string): Validation => {
|
|
||||||
let v: Validation = {};
|
let v: Validation = {};
|
||||||
for (let criteria of ["length", "lowercase", "uppercase", "number", "special"]) { v[criteria] = 0; }
|
for (let criteria of ["length", "lowercase", "uppercase", "number", "special"]) { v[criteria] = 0; }
|
||||||
v["length"] = s.length;
|
v["length"] = s.length;
|
||||||
for (let c of s) {
|
for (let c of s) {
|
||||||
if (isInt(c)) { v["number"]++; }
|
if (this._isInt(c)) { v["number"]++; }
|
||||||
else {
|
else {
|
||||||
const upper = c.toUpperCase();
|
const upper = c.toUpperCase();
|
||||||
if (upper == c.toLowerCase()) { v["special"]++; }
|
if (upper == c.toLowerCase()) { v["special"]++; }
|
||||||
@ -141,27 +152,37 @@ export function initValidator(passwordField: HTMLInputElement, rePasswordField:
|
|||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
passwordField.addEventListener("keyup", () => {
|
|
||||||
const v = validate(passwordField.value);
|
private _bindRequirements = () => {
|
||||||
for (let criteria in requirements) {
|
|
||||||
requirements[criteria].validate(v[criteria]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var requirements: { [category: string]: Requirement } = {};
|
|
||||||
|
|
||||||
if (!window.validationStrings) {
|
|
||||||
window.validationStrings = defaultPwValStrings;
|
|
||||||
} else {
|
|
||||||
for (let category in window.validationStrings) {
|
for (let category in window.validationStrings) {
|
||||||
if (!testStrings(window.validationStrings[category])) {
|
if (!this._testStrings(window.validationStrings[category])) {
|
||||||
window.validationStrings[category] = defaultPwValStrings[category];
|
window.validationStrings[category] = this._defaultPwValStrings[category];
|
||||||
}
|
}
|
||||||
const el = document.getElementById("requirement-" + category);
|
const el = document.getElementById("requirement-" + category);
|
||||||
if (el) {
|
if (typeof(el) === 'undefined' || el == null) continue;
|
||||||
requirements[category] = new Requirement(category, el as HTMLLIElement);
|
this._requirements[category] = new Requirement(category, el as HTMLLIElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
get requirements(): Requirements { return this._requirements };
|
||||||
|
|
||||||
|
constructor(conf: ValidatorConf) {
|
||||||
|
this._conf = conf;
|
||||||
|
if (!(this._conf.validatorFunc)) {
|
||||||
|
this._conf.validatorFunc = (oncomplete: (valid: boolean) => void) => { oncomplete(true); };
|
||||||
|
}
|
||||||
|
this._conf.rePasswordField.addEventListener("keyup", this.validate);
|
||||||
|
this._conf.passwordField.addEventListener("keyup", this.validate);
|
||||||
|
this._conf.passwordField.addEventListener("keyup", () => {
|
||||||
|
const v = this._validate(this._conf.passwordField.value);
|
||||||
|
for (let criteria in this._requirements) {
|
||||||
|
this._requirements[criteria].validate(v[criteria]);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
if (!window.validationStrings) {
|
||||||
|
window.validationStrings = this._defaultPwValStrings;
|
||||||
|
} else {
|
||||||
|
this._bindRequirements();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [requirements, checkValidity]
|
|
||||||
}
|
}
|
||||||
|
16
ts/pwr.ts
16
ts/pwr.ts
@ -1,5 +1,5 @@
|
|||||||
import { Modal } from "./modules/modal.js";
|
import { Modal } from "./modules/modal.js";
|
||||||
import { initValidator } from "./modules/validator.js";
|
import { Validator, ValidatorConf } from "./modules/validator.js";
|
||||||
import { _post, addLoader, removeLoader } from "./modules/common.js";
|
import { _post, addLoader, removeLoader } from "./modules/common.js";
|
||||||
import { loadLangSelector } from "./modules/lang.js";
|
import { loadLangSelector } from "./modules/lang.js";
|
||||||
|
|
||||||
@ -35,14 +35,22 @@ loadLangSelector("pwr");
|
|||||||
declare var window: formWindow;
|
declare var window: formWindow;
|
||||||
|
|
||||||
const form = document.getElementById("form-create") as HTMLFormElement;
|
const form = document.getElementById("form-create") as HTMLFormElement;
|
||||||
const submitButton = form.querySelector("input[type=submit]") as HTMLInputElement;
|
const submitInput = form.querySelector("input[type=submit]") as HTMLInputElement;
|
||||||
const submitSpan = form.querySelector("span.submit") as HTMLSpanElement;
|
const submitSpan = form.querySelector("span.submit") as HTMLSpanElement;
|
||||||
const passwordField = document.getElementById("create-password") as HTMLInputElement;
|
const passwordField = document.getElementById("create-password") as HTMLInputElement;
|
||||||
const rePasswordField = document.getElementById("create-reenter-password") as HTMLInputElement;
|
const rePasswordField = document.getElementById("create-reenter-password") as HTMLInputElement;
|
||||||
|
|
||||||
window.successModal = new Modal(document.getElementById("modal-success"), true);
|
window.successModal = new Modal(document.getElementById("modal-success"), true);
|
||||||
|
|
||||||
var requirements = initValidator(passwordField, rePasswordField, submitButton, submitSpan)
|
let validatorConf: ValidatorConf = {
|
||||||
|
passwordField: passwordField,
|
||||||
|
rePasswordField: rePasswordField,
|
||||||
|
submitInput: submitInput,
|
||||||
|
submitButton: submitSpan
|
||||||
|
};
|
||||||
|
|
||||||
|
var validator = new Validator(validatorConf);
|
||||||
|
var requirements = validator.requirements;
|
||||||
|
|
||||||
interface sendDTO {
|
interface sendDTO {
|
||||||
pin: string;
|
pin: string;
|
||||||
@ -81,3 +89,5 @@ form.onsubmit = (event: Event) => {
|
|||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
validator.validate();
|
||||||
|
96
ts/user.ts
96
ts/user.ts
@ -1,9 +1,10 @@
|
|||||||
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, _delete, notificationBox, whichAnimationEvent, toDateString, toggleLoader } from "./modules/common.js";
|
import { _get, _post, _delete, notificationBox, whichAnimationEvent, toDateString, toggleLoader, addLoader, removeLoader } from "./modules/common.js";
|
||||||
import { Login } from "./modules/login.js";
|
import { Login } from "./modules/login.js";
|
||||||
import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js";
|
import { Discord, Telegram, Matrix, ServiceConfiguration, MatrixConfiguration } from "./modules/account-linking.js";
|
||||||
|
import { Validator, ValidatorConf, ValidatorRespDTO } from "./modules/validator.js";
|
||||||
|
|
||||||
interface userWindow extends Window {
|
interface userWindow extends Window {
|
||||||
jellyfinID: string;
|
jellyfinID: string;
|
||||||
@ -88,6 +89,7 @@ const grid = document.querySelector(".grid");
|
|||||||
var rootCard = document.getElementById("card-user");
|
var rootCard = document.getElementById("card-user");
|
||||||
var contactCard = document.getElementById("card-contact");
|
var contactCard = document.getElementById("card-contact");
|
||||||
var statusCard = document.getElementById("card-status");
|
var statusCard = document.getElementById("card-status");
|
||||||
|
var passwordCard = document.getElementById("card-password");
|
||||||
|
|
||||||
interface MyDetailsContactMethod {
|
interface MyDetailsContactMethod {
|
||||||
value: string;
|
value: string;
|
||||||
@ -383,7 +385,52 @@ const matrixConf: MatrixConfiguration = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let matrix = new Matrix(matrixConf);
|
let matrix = new Matrix(matrixConf);
|
||||||
|
|
||||||
|
|
||||||
|
const oldPasswordField = document.getElementById("user-old-password") as HTMLInputElement;
|
||||||
|
const newPasswordField = document.getElementById("user-new-password") as HTMLInputElement;
|
||||||
|
const rePasswordField = document.getElementById("user-reenter-new-password") as HTMLInputElement;
|
||||||
|
const changePasswordButton = document.getElementById("user-password-submit") as HTMLSpanElement;
|
||||||
|
|
||||||
|
let baseValidator = (oncomplete: (valid: boolean) => void): void => {
|
||||||
|
if (oldPasswordField.value.length == 0) return oncomplete(false);
|
||||||
|
oncomplete(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
let validatorConf: ValidatorConf = {
|
||||||
|
passwordField: newPasswordField,
|
||||||
|
rePasswordField: rePasswordField,
|
||||||
|
submitButton: changePasswordButton,
|
||||||
|
validatorFunc: baseValidator
|
||||||
|
};
|
||||||
|
|
||||||
|
let validator = new Validator(validatorConf);
|
||||||
|
// let requirements = validator.requirements;
|
||||||
|
|
||||||
|
oldPasswordField.addEventListener("keyup", validator.validate);
|
||||||
|
changePasswordButton.addEventListener("click", () => {
|
||||||
|
addLoader(changePasswordButton);
|
||||||
|
_post("/my/password", { old: oldPasswordField.value, new: newPasswordField.value }, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState != 4) return;
|
||||||
|
removeLoader(changePasswordButton);
|
||||||
|
if (req.status == 400) {
|
||||||
|
window.notifications.customError("errorPassword", window.lang.notif("errorPassword"));
|
||||||
|
} else if (req.status == 500) {
|
||||||
|
window.notifications.customError("errorUnknown", window.lang.notif("errorUnknown"));
|
||||||
|
} else if (req.status == 204) {
|
||||||
|
window.notifications.customSuccess("passwordChanged", window.lang.notif("passwordChanged"));
|
||||||
|
setTimeout(() => { window.location.reload() }, 2000);
|
||||||
|
}
|
||||||
|
}, true, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState != 4) return;
|
||||||
|
if (req.status == 401) {
|
||||||
|
removeLoader(changePasswordButton);
|
||||||
|
window.notifications.customError("oldPasswordError", window.lang.notif("errorOldPassword"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// FIXME: Submit & Validate
|
||||||
|
|
||||||
document.addEventListener("details-reload", () => {
|
document.addEventListener("details-reload", () => {
|
||||||
_get("/my/details", null, (req: XMLHttpRequest) => {
|
_get("/my/details", null, (req: XMLHttpRequest) => {
|
||||||
@ -448,20 +495,10 @@ document.addEventListener("details-reload", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof(messageCard) != "undefined" && messageCard != null) {
|
if (typeof(messageCard) != "undefined" && messageCard != null) {
|
||||||
let largestNonMessageCardHeight = 0;
|
setBestRowSpan(messageCard, false);
|
||||||
const cards = grid.querySelectorAll(".card") as NodeListOf<HTMLElement>;
|
// contactCard.querySelector(".content").classList.add("h-100");
|
||||||
for (let i = 0; i < cards.length; i++) {
|
} else if (!statusCard.classList.contains("unfocused")) {
|
||||||
if (cards[i].id == "card-message") continue;
|
setBestRowSpan(passwordCard, true);
|
||||||
if (computeRealHeight(cards[i]) > largestNonMessageCardHeight) {
|
|
||||||
largestNonMessageCardHeight = computeRealHeight(cards[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let rowSpan = Math.ceil(computeRealHeight(messageCard) / largestNonMessageCardHeight);
|
|
||||||
|
|
||||||
if (rowSpan > 0)
|
|
||||||
messageCard.style.gridRow = `span ${rowSpan}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -474,11 +511,36 @@ login.onLogin = () => {
|
|||||||
document.dispatchEvent(new CustomEvent("details-reload"));
|
document.dispatchEvent(new CustomEvent("details-reload"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setBestRowSpan = (el: HTMLElement, setOnParent: boolean) => {
|
||||||
|
let largestNonMessageCardHeight = 0;
|
||||||
|
const cards = grid.querySelectorAll(".card") as NodeListOf<HTMLElement>;
|
||||||
|
for (let i = 0; i < cards.length; i++) {
|
||||||
|
if (cards[i].id == el.id) continue;
|
||||||
|
if (computeRealHeight(cards[i]) > largestNonMessageCardHeight) {
|
||||||
|
largestNonMessageCardHeight = computeRealHeight(cards[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rowSpan = Math.ceil(computeRealHeight(el) / largestNonMessageCardHeight);
|
||||||
|
|
||||||
|
if (rowSpan > 0)
|
||||||
|
(setOnParent ? el.parentElement : el).style.gridRow = `span ${rowSpan}`;
|
||||||
|
};
|
||||||
|
|
||||||
const computeRealHeight = (el: HTMLElement): number => {
|
const computeRealHeight = (el: HTMLElement): number => {
|
||||||
let children = el.children as HTMLCollectionOf<HTMLElement>;
|
let children = el.children as HTMLCollectionOf<HTMLElement>;
|
||||||
let total = 0;
|
let total = 0;
|
||||||
for (let i = 0; i < children.length; i++) {
|
for (let i = 0; i < children.length; i++) {
|
||||||
total += children[i].offsetHeight;
|
// Cope with the contact method card expanding to fill, by counting each contact method individually
|
||||||
|
if (el.id == "card-contact" && children[i].classList.contains("content")) {
|
||||||
|
// console.log("FOUND CARD_CONTACT, OG:", total + children[i].offsetHeight);
|
||||||
|
for (let j = 0; j < children[i].children.length; j++) {
|
||||||
|
total += (children[i].children[j] as HTMLElement).offsetHeight;
|
||||||
|
}
|
||||||
|
// console.log("NEW:", total);
|
||||||
|
} else {
|
||||||
|
total += children[i].offsetHeight;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
3
views.go
3
views.go
@ -183,10 +183,11 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
|
|||||||
"notifications": notificationsEnabled,
|
"notifications": notificationsEnabled,
|
||||||
"username": !app.config.Section("email").Key("no_username").MustBool(false),
|
"username": !app.config.Section("email").Key("no_username").MustBool(false),
|
||||||
"strings": app.storage.lang.User[lang].Strings,
|
"strings": app.storage.lang.User[lang].Strings,
|
||||||
"validationStrings": app.storage.lang.User[lang].ValidationStrings,
|
"validationStrings": app.storage.lang.User[lang].validationStringsJSON,
|
||||||
"language": app.storage.lang.User[lang].JSON,
|
"language": app.storage.lang.User[lang].JSON,
|
||||||
"langName": lang,
|
"langName": lang,
|
||||||
"jfLink": app.config.Section("ui").Key("redirect_url").String(),
|
"jfLink": app.config.Section("ui").Key("redirect_url").String(),
|
||||||
|
"requirements": app.validator.getCriteria(),
|
||||||
}
|
}
|
||||||
if telegramEnabled {
|
if telegramEnabled {
|
||||||
data["telegramUsername"] = app.telegram.username
|
data["telegramUsername"] = app.telegram.username
|
||||||
|
Loading…
Reference in New Issue
Block a user