mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-28 03:50:10 +00:00
Compare commits
9 Commits
7f518f55b2
...
49d8c6f8e4
Author | SHA1 | Date | |
---|---|---|---|
49d8c6f8e4 | |||
278588ca39 | |||
ab05c07469 | |||
6e205760c3 | |||
82032b98a8 | |||
e8666d5bf2 | |||
d1affe271c | |||
ea109c7b63 | |||
cb5a8c1c23 |
@ -503,7 +503,7 @@ func (app *appContext) NewUser(gc *gin.Context) {
|
|||||||
var req newUserDTO
|
var req newUserDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
app.debug.Printf("%s: New user attempt", req.Code)
|
app.debug.Printf("%s: New user attempt", req.Code)
|
||||||
if app.config.Section("captcha").Key("enabled").MustBool(false) && !app.verifyCaptcha(req.Code, req.CaptchaID, req.CaptchaText) {
|
if app.config.Section("captcha").Key("enabled").MustBool(false) && !app.verifyCaptcha(req.Code, req.CaptchaID, req.CaptchaText, false) {
|
||||||
app.info.Printf("%s: New user failed: Captcha Incorrect", req.Code)
|
app.info.Printf("%s: New user failed: Captcha Incorrect", req.Code)
|
||||||
respond(400, "errorCaptcha", gc)
|
respond(400, "errorCaptcha", gc)
|
||||||
return
|
return
|
||||||
|
10
api.go
10
api.go
@ -114,6 +114,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
var req ResetPasswordDTO
|
var req ResetPasswordDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
validation := app.validator.validate(req.Password)
|
validation := app.validator.validate(req.Password)
|
||||||
|
captcha := app.config.Section("captcha").Key("enabled").MustBool(false)
|
||||||
valid := true
|
valid := true
|
||||||
for _, val := range validation {
|
for _, val := range validation {
|
||||||
if !val {
|
if !val {
|
||||||
@ -121,12 +122,18 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !valid || req.PIN == "" {
|
if !valid || req.PIN == "" {
|
||||||
// 200 bcs idk what i did in js
|
|
||||||
app.info.Printf("%s: Password reset failed: Invalid password", req.PIN)
|
app.info.Printf("%s: Password reset failed: Invalid password", req.PIN)
|
||||||
gc.JSON(400, validation)
|
gc.JSON(400, validation)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isInternal := false
|
isInternal := false
|
||||||
|
|
||||||
|
if captcha && !app.verifyCaptcha(req.PIN, req.PIN, req.CaptchaText, true) {
|
||||||
|
app.info.Printf("%s: PWR Failed: Captcha Incorrect", req.PIN)
|
||||||
|
respond(400, "errorCaptcha", gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var userID, username string
|
var userID, username string
|
||||||
if reset, ok := app.internalPWRs[req.PIN]; ok {
|
if reset, ok := app.internalPWRs[req.PIN]; ok {
|
||||||
isInternal = true
|
isInternal = true
|
||||||
@ -138,6 +145,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
userID = reset.ID
|
userID = reset.ID
|
||||||
username = reset.Username
|
username = reset.Username
|
||||||
|
|
||||||
status, err := app.jf.ResetPasswordAdmin(userID)
|
status, err := app.jf.ResetPasswordAdmin(userID)
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf("Password Reset failed (%d): %v", status, err)
|
app.err.Printf("Password Reset failed (%d): %v", status, err)
|
||||||
|
6
auth.go
6
auth.go
@ -233,7 +233,8 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
|||||||
respond(500, "Couldn't generate token", gc)
|
respond(500, "Couldn't generate token", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gc.SetCookie("refresh", refresh, (3600 * 24), "/", gc.Request.URL.Hostname(), true, true)
|
host := gc.Request.URL.Hostname()
|
||||||
|
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", host, true, true)
|
||||||
gc.JSON(200, getTokenDTO{token})
|
gc.JSON(200, getTokenDTO{token})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,6 +298,7 @@ func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
|||||||
respond(500, "Couldn't generate token", gc)
|
respond(500, "Couldn't generate token", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", gc.Request.URL.Hostname(), true, true)
|
host := gc.Request.URL.Hostname()
|
||||||
|
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", host, true, true)
|
||||||
gc.JSON(200, getTokenDTO{jwt})
|
gc.JSON(200, getTokenDTO{jwt})
|
||||||
}
|
}
|
||||||
|
@ -575,7 +575,6 @@ input[type="checkbox" i], [class^="ri-"], [class*=" ri-"], .ri-refresh-line:befo
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.g-recaptcha {
|
.g-recaptcha {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 296px;
|
width: 296px;
|
||||||
@ -587,3 +586,8 @@ input[type="checkbox" i], [class^="ri-"], [class*=" ri-"], .ri-refresh-line:befo
|
|||||||
.g-recaptcha iframe {
|
.g-recaptcha iframe {
|
||||||
margin: -2px 0px 0px -4px;
|
margin: -2px 0px 0px -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-manual-toggle {
|
||||||
|
margin-bottom: -0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
.tooltip .content {
|
.tooltip .content {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
max-width: 10rem;
|
max-width: 10rem;
|
||||||
min-width: 6rem;
|
min-width: 6rem;
|
||||||
background-color: rgba(0, 0, 0, 0.6);
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
@ -13,12 +14,23 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
transition: opacity 100ms;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
top: -1rem;
|
top: -1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip.below .content {
|
||||||
|
top: 2.5rem;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.darker .content {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
.tooltip.right .content {
|
.tooltip.right .content {
|
||||||
left: 120%;
|
left: 120%;
|
||||||
}
|
}
|
||||||
@ -31,6 +43,10 @@
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip:hover .content {
|
.tooltip:hover .content,
|
||||||
|
.tooltip:focus .content,
|
||||||
|
.tooltip:focus-within .content
|
||||||
|
{
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
15
daemon.go
15
daemon.go
@ -74,6 +74,17 @@ func (app *appContext) clearTelegram() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *appContext) clearPWRCaptchas() {
|
||||||
|
app.debug.Println("Housekeeping: Clearing old PWR Captchas")
|
||||||
|
captchas := map[string]Captcha{}
|
||||||
|
for k, capt := range app.pwrCaptchas {
|
||||||
|
if capt.Generated.Add(CAPTCHA_VALIDITY * time.Second).After(time.Now()) {
|
||||||
|
captchas[k] = capt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.pwrCaptchas = captchas
|
||||||
|
}
|
||||||
|
|
||||||
func (app *appContext) clearActivities() {
|
func (app *appContext) clearActivities() {
|
||||||
app.debug.Println("Housekeeping: Cleaning up Activity log...")
|
app.debug.Println("Housekeeping: Cleaning up Activity log...")
|
||||||
keepCount := app.config.Section("activity_log").Key("keep_n_records").MustInt(1000)
|
keepCount := app.config.Section("activity_log").Key("keep_n_records").MustInt(1000)
|
||||||
@ -136,6 +147,7 @@ func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemo
|
|||||||
clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false)
|
clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false)
|
||||||
clearTelegram := app.config.Section("telegram").Key("require_unique").MustBool(false)
|
clearTelegram := app.config.Section("telegram").Key("require_unique").MustBool(false)
|
||||||
clearMatrix := app.config.Section("matrix").Key("require_unique").MustBool(false)
|
clearMatrix := app.config.Section("matrix").Key("require_unique").MustBool(false)
|
||||||
|
clearPWR := app.config.Section("captcha").Key("enabled").MustBool(false) && !app.config.Section("captcha").Key("recaptcha").MustBool(false)
|
||||||
|
|
||||||
if clearEmail || clearDiscord || clearTelegram || clearMatrix {
|
if clearEmail || clearDiscord || clearTelegram || clearMatrix {
|
||||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.jf.CacheExpiry = time.Now() })
|
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.jf.CacheExpiry = time.Now() })
|
||||||
@ -153,6 +165,9 @@ func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemo
|
|||||||
if clearMatrix {
|
if clearMatrix {
|
||||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearMatrix() })
|
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearMatrix() })
|
||||||
}
|
}
|
||||||
|
if clearPWR {
|
||||||
|
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearPWRCaptchas() })
|
||||||
|
}
|
||||||
|
|
||||||
return &daemon
|
return &daemon
|
||||||
}
|
}
|
||||||
|
144
html/admin.html
144
html/admin.html
@ -26,7 +26,7 @@
|
|||||||
<body class="max-w-full overflow-x-hidden section">
|
<body class="max-w-full overflow-x-hidden section">
|
||||||
{{ template "login-modal.html" . }}
|
{{ template "login-modal.html" . }}
|
||||||
<div id="modal-add-user" class="modal">
|
<div id="modal-add-user" class="modal">
|
||||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-add-user" href="">
|
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-add-user" href="">
|
||||||
<span class="heading">{{ .strings.newUser }} <span class="modal-close">×</span></span>
|
<span class="heading">{{ .strings.newUser }} <span class="modal-close">×</span></span>
|
||||||
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="add-user-user">
|
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="add-user-user">
|
||||||
<input type="email" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.emailAddress }}">
|
<input type="email" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.emailAddress }}">
|
||||||
@ -43,7 +43,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-about" class="modal">
|
<div id="modal-about" class="modal">
|
||||||
<div class="relative mx-auto my-[10%] w-4/5 lg:w-1/3 content card">
|
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 content card">
|
||||||
<img src="{{ .urlBase }}/banner.svg" class="banner header" alt="jfa-go banner">
|
<img src="{{ .urlBase }}/banner.svg" class="banner header" alt="jfa-go banner">
|
||||||
<span class="heading"><span class="modal-close">×</span></span>
|
<span class="heading"><span class="modal-close">×</span></span>
|
||||||
<p>{{ .strings.version }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .version }}</span></p>
|
<p>{{ .strings.version }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .version }}</span></p>
|
||||||
@ -80,7 +80,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-modify-user" class="modal">
|
<div id="modal-modify-user" class="modal">
|
||||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-modify-user" href="">
|
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-modify-user" href="">
|
||||||
<span class="heading"><span id="header-modify-user"></span> <span class="modal-close">×</span></span>
|
<span class="heading"><span id="header-modify-user"></span> <span class="modal-close">×</span></span>
|
||||||
<p class="content my-4">{{ .strings.modifySettingsDescription }}</p>
|
<p class="content my-4">{{ .strings.modifySettingsDescription }}</p>
|
||||||
<div class="flex flex-row mb-4">
|
<div class="flex flex-row mb-4">
|
||||||
@ -111,7 +111,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ if .referralsEnabled }}
|
{{ if .referralsEnabled }}
|
||||||
<div id="modal-enable-referrals-user" class="modal">
|
<div id="modal-enable-referrals-user" class="modal">
|
||||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-enable-referrals-user" href="">
|
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-enable-referrals-user" href="">
|
||||||
<span class="heading"><span id="header-enable-referrals-user"></span> <span class="modal-close">×</span></span>
|
<span class="heading"><span id="header-enable-referrals-user"></span> <span class="modal-close">×</span></span>
|
||||||
<p class="content my-4">{{ .strings.enableReferralsDescription }}</p>
|
<p class="content my-4">{{ .strings.enableReferralsDescription }}</p>
|
||||||
<div class="flex flex-row mb-4">
|
<div class="flex flex-row mb-4">
|
||||||
@ -142,7 +142,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-enable-referrals-profile" class="modal">
|
<div id="modal-enable-referrals-profile" class="modal">
|
||||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-enable-referrals-profile" href="">
|
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-enable-referrals-profile" href="">
|
||||||
<span class="heading"><span id="header-enable-referrals-profile">{{ .strings.enableReferrals }}</span> <span class="modal-close">×</span></span>
|
<span class="heading"><span id="header-enable-referrals-profile">{{ .strings.enableReferrals }}</span> <span class="modal-close">×</span></span>
|
||||||
<p class="content my-4">{{ .strings.enableReferralsProfileDescription }}</p>
|
<p class="content my-4">{{ .strings.enableReferralsProfileDescription }}</p>
|
||||||
<label class="supra" for="enable-referrals-profile-invites">{{ .strings.invite }}</label>
|
<label class="supra" for="enable-referrals-profile-invites">{{ .strings.invite }}</label>
|
||||||
@ -162,7 +162,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div id="modal-delete-user" class="modal">
|
<div id="modal-delete-user" class="modal">
|
||||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-delete-user" href="">
|
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-delete-user" href="">
|
||||||
<span class="heading"><span id="header-delete-user"></span> <span class="modal-close">×</span></span>
|
<span class="heading"><span id="header-delete-user"></span> <span class="modal-close">×</span></span>
|
||||||
<div class="content mt-8">
|
<div class="content mt-8">
|
||||||
<label class="switch mb-4">
|
<label class="switch mb-4">
|
||||||
@ -178,7 +178,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-extend-expiry" class="modal">
|
<div id="modal-extend-expiry" class="modal">
|
||||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-extend-expiry" href="">
|
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-extend-expiry" href="">
|
||||||
<span class="heading"><span id="header-extend-expiry"></span> <span class="modal-close">×</span></span>
|
<span class="heading"><span id="header-extend-expiry"></span> <span class="modal-close">×</span></span>
|
||||||
<div class="content mt-8">
|
<div class="content mt-8">
|
||||||
<aside class="aside sm ~urge dark:~d_info mb-2 @low row unfocused" id="extend-expiry-date"></aside>
|
<aside class="aside sm ~urge dark:~d_info mb-2 @low row unfocused" id="extend-expiry-date"></aside>
|
||||||
@ -242,7 +242,7 @@
|
|||||||
<div id="modal-announce" class="modal">
|
<div id="modal-announce" class="modal">
|
||||||
<form class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content card" id="form-announce" href="">
|
<form class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content card" id="form-announce" href="">
|
||||||
<span class="heading"><span id="header-announce"></span> <span class="modal-close">×</span></span>
|
<span class="heading"><span id="header-announce"></span> <span class="modal-close">×</span></span>
|
||||||
<div class="row">
|
<div class="flex flex-col md:flex-row">
|
||||||
<div class="col card ~neutral @low">
|
<div class="col card ~neutral @low">
|
||||||
<div id="announce-details">
|
<div id="announce-details">
|
||||||
<span class="label supra" for="editor-variables" id="label-editor-variables">{{ .strings.variables }}</span>
|
<span class="label supra" for="editor-variables" id="label-editor-variables">{{ .strings.variables }}</span>
|
||||||
@ -275,7 +275,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-customize" class="modal">
|
<div id="modal-customize" class="modal">
|
||||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3">
|
||||||
<span class="heading">{{ .strings.customizeMessages }} <span class="modal-close">×</span></span>
|
<span class="heading">{{ .strings.customizeMessages }} <span class="modal-close">×</span></span>
|
||||||
<p class="content my-4">{{ .strings.customizeMessagesDescription }}</p>
|
<p class="content my-4">{{ .strings.customizeMessagesDescription }}</p>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@ -319,7 +319,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-restart" class="modal">
|
<div id="modal-restart" class="modal">
|
||||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~critical @low">
|
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 ~critical @low">
|
||||||
<span class="heading">{{ .strings.settingsRestartRequired }} <span class="modal-close">×</span></span>
|
<span class="heading">{{ .strings.settingsRestartRequired }} <span class="modal-close">×</span></span>
|
||||||
<p class="content my-4">{{ .strings.settingsRestartRequiredDescription }}</p>
|
<p class="content my-4">{{ .strings.settingsRestartRequiredDescription }}</p>
|
||||||
<div class="float-right">
|
<div class="float-right">
|
||||||
@ -329,7 +329,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-backups" class="modal">
|
<div id="modal-backups" class="modal">
|
||||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3">
|
||||||
<span class="heading">{{ .strings.backups }} <span class="modal-close">×</span></span>
|
<span class="heading">{{ .strings.backups }} <span class="modal-close">×</span></span>
|
||||||
<div class="content my-4">
|
<div class="content my-4">
|
||||||
{{ .strings.backupsDescription }}
|
{{ .strings.backupsDescription }}
|
||||||
@ -345,7 +345,7 @@
|
|||||||
<input id="backups-file" name="backups-file" type="file" hidden>
|
<input id="backups-file" name="backups-file" type="file" hidden>
|
||||||
<button class="button ~neutral @low mr-2 mb-2" id="settings-backups-sort-direction">{{ .strings.sortDirection }}</button>
|
<button class="button ~neutral @low mr-2 mb-2" id="settings-backups-sort-direction">{{ .strings.sortDirection }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto text-xs md:text-sm">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -360,7 +360,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-backed-up" class="modal">
|
<div id="modal-backed-up" class="modal">
|
||||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~neutral @low">
|
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 ~neutral @low">
|
||||||
<span class="heading">{{ .strings.backupCreated }} <span class="modal-close">×</span></span>
|
<span class="heading">{{ .strings.backupCreated }} <span class="modal-close">×</span></span>
|
||||||
<p class="content my-4" id="settings-backed-up-location"></p>
|
<p class="content my-4" id="settings-backed-up-location"></p>
|
||||||
<p class="content my-4">{{ .strings.backupCanDownload }}</p>
|
<p class="content my-4">{{ .strings.backupCanDownload }}</p>
|
||||||
@ -370,20 +370,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-refresh" class="modal">
|
<div id="modal-refresh" class="modal">
|
||||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~neutral @low">
|
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 ~neutral @low">
|
||||||
<span class="heading">{{ .strings.settingsApplied }}</span>
|
<span class="heading">{{ .strings.settingsApplied }}</span>
|
||||||
<p class="content">{{ .strings.settingsRefreshPage }}</p>
|
<p class="content">{{ .strings.settingsRefreshPage }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-send-pwr" class="modal">
|
<div id="modal-send-pwr" class="modal">
|
||||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~neutral @low">
|
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 ~neutral @low">
|
||||||
<span class="heading">{{ .strings.sendPWR }}</span>
|
<span class="heading">{{ .strings.sendPWR }}</span>
|
||||||
<p class="content my-2" id="send-pwr-note"></p>
|
<p class="content my-2" id="send-pwr-note"></p>
|
||||||
<span class="button ~urge @low mt-2" id="send-pwr-link">{{ .strings.copy }}</span>
|
<span class="button ~urge @low mt-2" id="send-pwr-link">{{ .strings.copy }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-ombi-profile" class="modal">
|
<div id="modal-ombi-profile" class="modal">
|
||||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-ombi-defaults" href="">
|
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-ombi-defaults" href="">
|
||||||
<span class="heading">{{ .strings.ombiProfile }} <span class="modal-close">×</span></span>
|
<span class="heading">{{ .strings.ombiProfile }} <span class="modal-close">×</span></span>
|
||||||
<p class="content my-4">{{ .strings.ombiUserDefaultsDescription }}</p>
|
<p class="content my-4">{{ .strings.ombiUserDefaultsDescription }}</p>
|
||||||
<div class="select ~neutral @low mb-4">
|
<div class="select ~neutral @low mb-4">
|
||||||
@ -422,7 +422,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-add-profile" class="modal">
|
<div id="modal-add-profile" class="modal">
|
||||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-add-profile" href="">
|
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-add-profile" href="">
|
||||||
<span class="heading">{{ .strings.addProfile }} <span class="modal-close">×</span></span>
|
<span class="heading">{{ .strings.addProfile }} <span class="modal-close">×</span></span>
|
||||||
<p class="content my-4">{{ .strings.addProfileDescription }}</p>
|
<p class="content my-4">{{ .strings.addProfileDescription }}</p>
|
||||||
<label>
|
<label>
|
||||||
@ -461,7 +461,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ if .telegramEnabled }}
|
{{ if .telegramEnabled }}
|
||||||
<div id="modal-telegram" class="modal">
|
<div id="modal-telegram" class="modal">
|
||||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3">
|
||||||
<span class="heading mb-4">{{ .strings.linkTelegram }}</span>
|
<span class="heading mb-4">{{ .strings.linkTelegram }}</span>
|
||||||
<p class="content mb-4">{{ .strings.sendPIN }}</p>
|
<p class="content mb-4">{{ .strings.sendPIN }}</p>
|
||||||
<h1 class="ac" id="telegram-pin"></h1>
|
<h1 class="ac" id="telegram-pin"></h1>
|
||||||
@ -479,7 +479,7 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .discordEnabled }}
|
{{ if .discordEnabled }}
|
||||||
<div id="modal-discord" class="modal">
|
<div id="modal-discord" class="modal">
|
||||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
<div class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3">
|
||||||
<span class="heading mb-4"><span id="discord-header"></span><span class="modal-close">×</span></span>
|
<span class="heading mb-4"><span id="discord-header"></span><span class="modal-close">×</span></span>
|
||||||
<p class="content mb-4" id="discord-description"></p>
|
<p class="content mb-4" id="discord-description"></p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -490,7 +490,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div id="modal-matrix" class="modal">
|
<div id="modal-matrix" class="modal">
|
||||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-matrix" href="">
|
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-matrix" href="">
|
||||||
<span class="heading">{{ .strings.linkMatrix }}</span>
|
<span class="heading">{{ .strings.linkMatrix }}</span>
|
||||||
<p class="content my-4">{{ .strings.linkMatrixDescription }}</p>
|
<p class="content my-4">{{ .strings.linkMatrixDescription }}</p>
|
||||||
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.matrixHomeServer }}" id="matrix-homeserver">
|
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.matrixHomeServer }}" id="matrix-homeserver">
|
||||||
@ -503,7 +503,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="notification-box"></div>
|
<div id="notification-box"></div>
|
||||||
<div class="top-4 left-4 absolute">
|
<div class="top-4 left-4 absolute flex flex-row gap-2">
|
||||||
<span class="dropdown z-[11]" tabindex="0" id="lang-dropdown">
|
<span class="dropdown z-[11]" tabindex="0" id="lang-dropdown">
|
||||||
<span class="button ~urge dropdown-button">
|
<span class="button ~urge dropdown-button">
|
||||||
<i class="ri-global-line"></i>
|
<i class="ri-global-line"></i>
|
||||||
@ -532,8 +532,8 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<header class="flex flex-wrap items-center justify-between">
|
<header>
|
||||||
<div>
|
<div class="flex flex-row overflow-x-scroll items-center">
|
||||||
<span id="button-tab-invites" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.invites }}</span>
|
<span id="button-tab-invites" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.invites }}</span>
|
||||||
<span id="button-tab-accounts" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.accounts }}</span>
|
<span id="button-tab-accounts" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.accounts }}</span>
|
||||||
<span id="button-tab-activity" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.activity }}</span>
|
<span id="button-tab-activity" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.activity }}</span>
|
||||||
@ -547,7 +547,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-invites">
|
<div id="tab-invites">
|
||||||
<div class="card @low invites dark:~d_neutral mb-4">
|
<div class="card @low invites dark:~d_neutral mb-4 overflow-visible">
|
||||||
<span class="heading">{{ .strings.invites }}</span>
|
<span class="heading">{{ .strings.invites }}</span>
|
||||||
<div id="invites"></div>
|
<div id="invites"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -696,29 +696,33 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="tab-accounts" class="unfocused">
|
<div id="tab-accounts" class="unfocused">
|
||||||
<div class="card @low dark:~d_neutral accounts mb-4 overflow-visible">
|
<div class="card @low dark:~d_neutral accounts mb-4 overflow-visible">
|
||||||
<div class="flex-expand align-middle">
|
<div id="accounts-filter-dropdown" class="dropdown manual z-10 w-100">
|
||||||
|
<div class="flex flex-col md:flex-row align-middle gap-2">
|
||||||
|
<div class="flex flex-row align-middle justify-between md:justify-normal">
|
||||||
<span class="text-3xl font-bold mr-4">{{ .strings.accounts }}</span>
|
<span class="text-3xl font-bold mr-4">{{ .strings.accounts }}</span>
|
||||||
<div id="accounts-filter-dropdown" class="dropdown z-10" tabindex="0">
|
<span class="dropdown-manual-toggle"><button class="h-100 button ~neutral @low center" id="accounts-filter-button" tabindex="0">{{ .strings.filters }}</button></span>
|
||||||
<span class="h-100 button ~neutral @low center" id="accounts-filter-button">{{ .strings.filters }}</span>
|
</div>
|
||||||
<div class="dropdown-display">
|
<div class="flex flex-row align-middle w-100">
|
||||||
<div class="card ~neutral @low mt-2" id="accounts-filter-list">
|
<input type="search" class="field ~neutral @low input search mr-2" id="accounts-search" placeholder="{{ .strings.search }}">
|
||||||
|
<span class="button ~neutral @low center ml-[-2.64rem] rounded-s-none accounts-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-display max-w-full">
|
||||||
|
<div class="card ~neutral @low mt-2 overflow-x-scroll" id="accounts-filter-list">
|
||||||
<p class="supra pb-2">{{ .strings.filters }}</p>
|
<p class="supra pb-2">{{ .strings.filters }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="search" class="field ~neutral @low input search ml-2 mr-2" id="accounts-search" placeholder="{{ .strings.search }}">
|
|
||||||
<span class="button ~neutral @low center ml-[-2.64rem] rounded-s-none accounts-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
|
|
||||||
</div>
|
|
||||||
<div class="supra py-1 sm hidden" id="accounts-search-options-header">{{ .strings.searchOptions }}</div>
|
<div class="supra py-1 sm hidden" id="accounts-search-options-header">{{ .strings.searchOptions }}</div>
|
||||||
<div class="row -mx-2 mb-2">
|
<div class="row -mx-2 mb-2">
|
||||||
<button type="button" class="button ~neutral @low center mx-2 hidden"><span id="accounts-sort-by-field"></span> <i class="ri-close-line ml-2 text-2xl"></i></button>
|
<button type="button" class="button ~neutral @low center mx-2 hidden"><span id="accounts-sort-by-field"></span> <i class="ri-close-line ml-2 text-2xl"></i></button>
|
||||||
<span id="accounts-filter-area"></span>
|
<span id="accounts-filter-area"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="supra py-1 sm">{{ .strings.actions }}</div>
|
<div class="supra pt-1 pb-2 sm">{{ .strings.actions }}</div>
|
||||||
<div class="row -mx-2">
|
<div class="flex flex-row flex-wrap gap-3 mb-4">
|
||||||
<span class="col button ~neutral @low center max-w-[20%]" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
|
<span class="button ~neutral @low center " id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
|
||||||
<div id="accounts-announce-dropdown" class="col dropdown pb-0i max-w-[20%]" tabindex="0">
|
<div id="accounts-announce-dropdown" class="dropdown pb-0i " tabindex="0">
|
||||||
<span class="w-100 button ~info @low center" id="accounts-announce">{{ .strings.announce }}</span>
|
<span class="w-100 button ~info @low center items-baseline" id="accounts-announce">{{ .strings.announce }}</span>
|
||||||
<div class="dropdown-display">
|
<div class="dropdown-display">
|
||||||
<div class="card ~neutral @low">
|
<div class="card ~neutral @low">
|
||||||
<span class="supra sm">{{ .strings.templates }}</span>
|
<span class="supra sm">{{ .strings.templates }}</span>
|
||||||
@ -726,12 +730,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="col button ~urge @low center max-w-[20%]" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
|
<span class="button ~urge @low center " id="accounts-modify-user">{{ .strings.modifySettings }}</span>
|
||||||
{{ if .referralsEnabled }}
|
{{ if .referralsEnabled }}
|
||||||
<span class="col button ~urge @low center max-w-[20%]" id="accounts-enable-referrals">{{ .strings.enableReferrals }}</span>
|
<span class="button ~urge @low center " id="accounts-enable-referrals">{{ .strings.enableReferrals }}</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div id="accounts-expiry-dropdown" class="col dropdown pb-0i max-w-[20%]" tabindex="0">
|
<div id="accounts-expiry-dropdown" class="dropdown pb-0i " tabindex="0">
|
||||||
<span class="w-100 button ~positive @low center" id="accounts-expiry-dropdown-button">{{ .strings.expiry }} <i class="ri-arrow-down-s-line ml-2"></i></span>
|
<span class="w-100 button ~positive @low center items-baseline" id="accounts-expiry-dropdown-button">{{ .strings.expiry }} <i class="ri-arrow-down-s-line ml-2"></i></span>
|
||||||
<div class="dropdown-display">
|
<div class="dropdown-display">
|
||||||
<div class="card ~neutral @low">
|
<div class="card ~neutral @low">
|
||||||
<span class="button ~warning full-width @low center" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span>
|
<span class="button ~warning full-width @low center" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span>
|
||||||
@ -739,7 +743,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="accounts-disable-enable-dropdown" class="col dropdown manual pb-0i max-w-[20%]" tabindex="0">
|
<div id="accounts-disable-enable-dropdown" class="dropdown manual pb-0i " tabindex="0">
|
||||||
<span class="w-100 button ~positive @low center" id="accounts-disable-enable">{{ .strings.disable }}</span>
|
<span class="w-100 button ~positive @low center" id="accounts-disable-enable">{{ .strings.disable }}</span>
|
||||||
<div class="dropdown-display">
|
<div class="dropdown-display">
|
||||||
<div class="card ~neutral @low">
|
<div class="card ~neutral @low">
|
||||||
@ -747,8 +751,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="col button ~info @low center unfocused max-w-[20%]" id="accounts-send-pwr">{{ .strings.sendPWR }}</span>
|
<span class="button ~info @low center unfocused " id="accounts-send-pwr">{{ .strings.sendPWR }}</span>
|
||||||
<span class="col button ~critical @low center max-w-[20%]" id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
|
<span class="button ~critical @low center " id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card @low accounts-header table-responsive mt-2">
|
<div class="card @low accounts-header table-responsive mt-2">
|
||||||
<table class="table text-base leading-4">
|
<table class="table text-base leading-4">
|
||||||
@ -791,27 +795,33 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="tab-activity" class="unfocused">
|
<div id="tab-activity" class="unfocused">
|
||||||
<div class="card @low dark:~d_neutral activity mb-4 overflow-visible">
|
<div class="card @low dark:~d_neutral activity mb-4 overflow-visible">
|
||||||
<div class="flex-expand align-middle">
|
<div id="activity-filter-dropdown" class="dropdown manual z-10 w-100" tabindex="0">
|
||||||
|
<div class="flex flex-col md:flex-row align-middle gap-2">
|
||||||
|
<div class="flex flex-row align-middle justify-between md:justify-normal">
|
||||||
<span class="text-3xl font-bold mr-4">{{ .strings.activity }}</span>
|
<span class="text-3xl font-bold mr-4">{{ .strings.activity }}</span>
|
||||||
<div id="activity-filter-dropdown" class="dropdown z-10" tabindex="0">
|
<div class="flex flex-row align-middle">
|
||||||
<span class="h-100 button ~neutral @low center" id="activity-filter-button">{{ .strings.filters }}</span>
|
<span class="dropdown-manual-toggle"><button class="h-100 button ~neutral @low center" id="activity-filter-button">{{ .strings.filters }}</button></span>
|
||||||
<div class="dropdown-display">
|
<button class="button ~neutral @low ml-2" id="activity-sort-direction">{{ .strings.sortDirection }}</button>
|
||||||
<div class="card ~neutral @low mt-2" id="activity-filter-list">
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row align-middle w-100">
|
||||||
|
<input type="search" class="field ~neutral @low input search mr-2" id="activity-search" placeholder="{{ .strings.search }}">
|
||||||
|
<span class="button ~neutral @low center ml-[-2.64rem] rounded-s-none activity-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
|
||||||
|
<button class="button ~info @low ml-2" id="activity-refresh" aria-label="{{ .strings.refresh }}" disabled><i class="ri-refresh-line"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-display max-w-full">
|
||||||
|
<div class="card ~neutral @low mt-2 overflow-x-scroll" id="activity-filter-list">
|
||||||
<p class="supra pb-2">{{ .strings.filters }}</p>
|
<p class="supra pb-2">{{ .strings.filters }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="button ~neutral @low ml-2" id="activity-sort-direction">{{ .strings.sortDirection }}</button>
|
<div class="flex flex-row justify-between pt-3 pb-2">
|
||||||
<input type="search" class="field ~neutral @low input search ml-2 mr-2" id="activity-search" placeholder="{{ .strings.search }}">
|
|
||||||
<span class="button ~neutral @low center ml-[-2.64rem] rounded-s-none activity-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
|
|
||||||
<button class="button ~info @low ml-2" id="activity-refresh" aria-label="{{ .strings.refresh }}" disabled><i class="ri-refresh-line"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row justify-between py-2">
|
|
||||||
<div class="supra sm hidden" id="activity-search-options-header">{{ .strings.searchOptions }}</div>
|
<div class="supra sm hidden" id="activity-search-options-header">{{ .strings.searchOptions }}</div>
|
||||||
<div class="supra sm">
|
<div class="supra sm flex flex-row gap-2">
|
||||||
<span id="activity-total-records" class="mx-2"></span>
|
<span id="activity-total-records"></span>
|
||||||
<span id="activity-loaded-records" class="mx-2"></span>
|
<span id="activity-loaded-records"></span>
|
||||||
<span id="activity-shown-records" class="mx-2"></span>
|
<span id="activity-shown-records"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row -mx-2 mb-2">
|
<div class="row -mx-2 mb-2">
|
||||||
@ -842,19 +852,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="tab-settings" class="unfocused">
|
<div id="tab-settings" class="unfocused">
|
||||||
<div class="card @low dark:~d_neutral settings overflow">
|
<div class="card @low dark:~d_neutral settings overflow">
|
||||||
<div class="flex-expand">
|
<div class="flex flex-col md:flex-row align-middle gap-2">
|
||||||
<div class="flex-row">
|
<div class="flex flex-row align-middle justify-between md:justify-normal">
|
||||||
<span class="heading">{{ .strings.settings }}</span>
|
<span class="heading">{{ .strings.settings }}</span>
|
||||||
<label for="settings-advanced-enabled" class="button ~neutral @low ml-2 my-2">
|
<label for="settings-advanced-enabled" class="button ~neutral @low ml-2">
|
||||||
<input type="checkbox" id="settings-advanced-enabled" aria-label="Advanced settings enabled">
|
<input type="checkbox" id="settings-advanced-enabled" aria-label="Advanced settings enabled">
|
||||||
<span class="ml-2">{{ .strings.advancedSettings }} </span>
|
<span class="ml-2">{{ .strings.advancedSettings }} </span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex flex-row justify-start md:justify-end gap-2 w-100">
|
||||||
<span class="button ~neutral @low my-1" id="settings-logs">{{ .strings.logs }}</span>
|
<span class="button ~neutral @low" id="settings-logs">{{ .strings.logs }}</span>
|
||||||
<span class="button ~info @low my-1" id="settings-backups">{{ .strings.backups }}</span>
|
<span class="button ~info @low" id="settings-backups">{{ .strings.backups }}</span>
|
||||||
<span class="button ~neutral @low my-1" id="settings-restart">{{ .strings.settingsRestart }}</span>
|
<span class="button ~neutral @low" id="settings-restart">{{ .strings.settingsRestart }}</span>
|
||||||
<span class="button ~urge @low unfocused my-1" id="settings-save">{{ .strings.settingsSave }}</span>
|
<span class="button ~urge @low unfocused" id="settings-save">{{ .strings.settingsSave }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col md:flex-row gap-3">
|
<div class="flex flex-col md:flex-row gap-3">
|
||||||
|
@ -34,8 +34,12 @@
|
|||||||
</script>
|
</script>
|
||||||
{{ if .passwordReset }}
|
{{ if .passwordReset }}
|
||||||
<script src="js/pwr.js" type="module"></script>
|
<script src="js/pwr.js" type="module"></script>
|
||||||
|
<script>
|
||||||
|
window.pwrPIN = "{{ .pwrPIN }}";
|
||||||
|
</script>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<script src="js/form.js" type="module"></script>
|
<script src="js/form.js" type="module"></script>
|
||||||
|
{{ end }}
|
||||||
{{ if .reCAPTCHA }}
|
{{ if .reCAPTCHA }}
|
||||||
<script>
|
<script>
|
||||||
var reCAPTCHACallback = () => {
|
var reCAPTCHACallback = () => {
|
||||||
@ -49,4 +53,3 @@
|
|||||||
<script src="https://www.google.com/recaptcha/api.js?onload=reCAPTCHACallback&render=explicit" async defer></script>
|
<script src="https://www.google.com/recaptcha/api.js?onload=reCAPTCHACallback&render=explicit" async defer></script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
<div id="modal-login" class="modal">
|
<div id="modal-login" class="modal">
|
||||||
<div class="my-[10%] row items-stretch relative mx-auto w-[40%] lg:w-[60%]">
|
<div class="my-[10%] row items-stretch relative mx-auto w-11/12 sm:w-4/5 lg:w-1/2">
|
||||||
{{ if index . "LoginMessageEnabled" }}
|
{{ if index . "LoginMessageEnabled" }}
|
||||||
{{ if .LoginMessageEnabled }}
|
{{ if .LoginMessageEnabled }}
|
||||||
<div class="card mx-2 flex-initial w-[100%] xl:w-[35%] mb-4 xl:mb-0 dark:~d_neutral @low content">
|
<div class="card mx-2 flex-initial w-[100%] lg:w-[35%] mb-4 lg:mb-0 dark:~d_neutral @low content">
|
||||||
{{ .LoginMessageContent }}
|
{{ .LoginMessageContent }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if index . "userPageEnabled" }}
|
{{ if index . "userPageEnabled" }}
|
||||||
{{ if and .userPageEnabled .showUserPageLink }}
|
{{ if and .userPageEnabled .showUserPageLink }}
|
||||||
<div class="card mx-2 flex-initial w-[100%] xl:w-[35%] mb-4 xl:mb-0 dark:~d_neutral @low content">
|
<div class="card mx-2 flex-initial w-[100%] lg:w-[35%] mb-4 lg:mb-0 dark:~d_neutral @low content">
|
||||||
<span class="heading row">{{ .strings.loginNotAdmin }}</span>
|
<span class="heading row">{{ .strings.loginNotAdmin }}</span>
|
||||||
<a class="button ~info h-12 w-100" href="{{ .urlBase }}/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
|
<a class="button ~info h-12 w-100" href="{{ .urlBase }}/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<form class="card mx-2 flex-auto form-login w-[100%] xl:w-[55%] mb-0" href="">
|
<form class="card mx-2 flex-auto form-login w-[100%] lg:w-[55%] mb-0" href="">
|
||||||
<span class="heading">{{ .strings.login }}</span>
|
<span class="heading">{{ .strings.login }}</span>
|
||||||
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="login-user">
|
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="login-user">
|
||||||
<input type="password" class="field input ~neutral @high mb-4" placeholder="{{ .strings.password }}" id="login-password">
|
<input type="password" class="field input ~neutral @high mb-4" placeholder="{{ .strings.password }}" id="login-password">
|
||||||
|
1
main.go
1
main.go
@ -120,6 +120,7 @@ type appContext struct {
|
|||||||
proxyTransport *http.Transport
|
proxyTransport *http.Transport
|
||||||
proxyConfig easyproxy.ProxyConfig
|
proxyConfig easyproxy.ProxyConfig
|
||||||
internalPWRs map[string]InternalPWR
|
internalPWRs map[string]InternalPWR
|
||||||
|
pwrCaptchas map[string]Captcha
|
||||||
ConfirmationKeys map[string]map[string]newUserDTO // Map of invite code to jwt to request
|
ConfirmationKeys map[string]map[string]newUserDTO // Map of invite code to jwt to request
|
||||||
confirmationKeysLock sync.Mutex
|
confirmationKeysLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
@ -334,6 +334,7 @@ type MatrixLoginDTO struct {
|
|||||||
type ResetPasswordDTO struct {
|
type ResetPasswordDTO struct {
|
||||||
PIN string `json:"pin"`
|
PIN string `json:"pin"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
CaptchaText string `json:"captcha_text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdminPasswordResetDTO struct {
|
type AdminPasswordResetDTO struct {
|
||||||
|
@ -7,7 +7,7 @@ import { accountsList } from "./modules/accounts.js";
|
|||||||
import { settingsList } from "./modules/settings.js";
|
import { settingsList } from "./modules/settings.js";
|
||||||
import { activityList } from "./modules/activity.js";
|
import { activityList } from "./modules/activity.js";
|
||||||
import { ProfileEditor } from "./modules/profiles.js";
|
import { ProfileEditor } from "./modules/profiles.js";
|
||||||
import { _get, _post, notificationBox, whichAnimationEvent } from "./modules/common.js";
|
import { _get, _post, notificationBox, whichAnimationEvent, bindManualDropdowns } from "./modules/common.js";
|
||||||
import { Updater } from "./modules/update.js";
|
import { Updater } from "./modules/update.js";
|
||||||
import { Login } from "./modules/login.js";
|
import { Login } from "./modules/login.js";
|
||||||
|
|
||||||
@ -216,6 +216,8 @@ login.onLogin = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bindManualDropdowns();
|
||||||
|
|
||||||
login.bindLogout(document.getElementById("logout-button"));
|
login.bindLogout(document.getElementById("logout-button"));
|
||||||
|
|
||||||
login.login("", "");
|
login.login("", "");
|
||||||
|
72
ts/form.ts
72
ts/form.ts
@ -4,6 +4,7 @@ import { _get, _post, toggleLoader, addLoader, removeLoader, toDateString } from
|
|||||||
import { loadLangSelector } from "./modules/lang.js";
|
import { loadLangSelector } from "./modules/lang.js";
|
||||||
import { Validator, ValidatorConf, ValidatorRespDTO } 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";
|
||||||
|
import { Captcha, GreCAPTCHA } from "./modules/captcha.js";
|
||||||
|
|
||||||
interface formWindow extends Window {
|
interface formWindow extends Window {
|
||||||
invalidPassword: string;
|
invalidPassword: string;
|
||||||
@ -172,35 +173,7 @@ if (!window.usernameEnabled) { usernameField.parentElement.remove(); usernameFie
|
|||||||
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;
|
||||||
|
|
||||||
let captchaVerified = false;
|
let captcha = new Captcha(window.code, window.captcha, window.reCAPTCHA, false);
|
||||||
let captchaID = "";
|
|
||||||
let captchaInput = document.getElementById("captcha-input") as HTMLInputElement;
|
|
||||||
const captchaCheckbox = document.getElementById("captcha-success") as HTMLSpanElement;
|
|
||||||
let prevCaptcha = "";
|
|
||||||
|
|
||||||
let baseValidator = (oncomplete: (valid: boolean) => void): void => {
|
|
||||||
if (window.captcha && !window.reCAPTCHA && (captchaInput.value != prevCaptcha)) {
|
|
||||||
prevCaptcha = captchaInput.value;
|
|
||||||
_post("/captcha/verify/" + window.code + "/" + captchaID + "/" + captchaInput.value, null, (req: XMLHttpRequest) => {
|
|
||||||
if (req.readyState == 4) {
|
|
||||||
if (req.status == 204) {
|
|
||||||
captchaCheckbox.innerHTML = `<i class="ri-check-line"></i>`;
|
|
||||||
captchaCheckbox.classList.add("~positive");
|
|
||||||
captchaCheckbox.classList.remove("~critical");
|
|
||||||
captchaVerified = true;
|
|
||||||
} else {
|
|
||||||
captchaCheckbox.innerHTML = `<i class="ri-close-line"></i>`;
|
|
||||||
captchaCheckbox.classList.add("~critical");
|
|
||||||
captchaCheckbox.classList.remove("~positive");
|
|
||||||
captchaVerified = false;
|
|
||||||
}
|
|
||||||
_baseValidator(oncomplete, captchaVerified);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_baseValidator(oncomplete, captchaVerified);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: boolean): void {
|
function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: boolean): void {
|
||||||
if (window.emailRequired) {
|
if (window.emailRequired) {
|
||||||
@ -228,20 +201,9 @@ function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: bool
|
|||||||
oncomplete(true);
|
oncomplete(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GreCAPTCHA {
|
let baseValidator = captcha.baseValidatorWrapper(_baseValidator);
|
||||||
render: (container: HTMLDivElement, parameters: {
|
|
||||||
sitekey?: string,
|
|
||||||
theme?: string,
|
|
||||||
size?: string,
|
|
||||||
tabindex?: number,
|
|
||||||
"callback"?: () => void,
|
|
||||||
"expired-callback"?: () => void,
|
|
||||||
"error-callback"?: () => void
|
|
||||||
}) => void;
|
|
||||||
getResponse: (opt_widget_id?: HTMLDivElement) => string;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare var grecaptcha: GreCAPTCHA
|
declare var grecaptcha: GreCAPTCHA;
|
||||||
|
|
||||||
let validatorConf: ValidatorConf = {
|
let validatorConf: ValidatorConf = {
|
||||||
passwordField: passwordField,
|
passwordField: passwordField,
|
||||||
@ -273,29 +235,15 @@ interface sendDTO {
|
|||||||
captcha_text?: string;
|
captcha_text?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const genCaptcha = () => {
|
|
||||||
_get("/captcha/gen/"+window.code, null, (req: XMLHttpRequest) => {
|
|
||||||
if (req.readyState == 4) {
|
|
||||||
if (req.status == 200) {
|
|
||||||
captchaID = req.response["id"];
|
|
||||||
document.getElementById("captcha-img").innerHTML = `
|
|
||||||
<img class="w-100" src="${window.location.toString().substring(0, window.location.toString().lastIndexOf("/invite"))}/captcha/img/${window.code}/${captchaID}"></img>
|
|
||||||
`;
|
|
||||||
captchaInput.value = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (window.captcha && !window.reCAPTCHA) {
|
if (window.captcha && !window.reCAPTCHA) {
|
||||||
genCaptcha();
|
captcha.generate();
|
||||||
(document.getElementById("captcha-regen") as HTMLSpanElement).onclick = genCaptcha;
|
(document.getElementById("captcha-regen") as HTMLSpanElement).onclick = captcha.generate;
|
||||||
captchaInput.onkeyup = validator.validate;
|
captcha.input.onkeyup = validator.validate;
|
||||||
}
|
}
|
||||||
|
|
||||||
const create = (event: SubmitEvent) => {
|
const create = (event: SubmitEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (window.captcha && !window.reCAPTCHA && !captchaVerified) {
|
if (window.captcha && !window.reCAPTCHA && !captcha.verified) {
|
||||||
|
|
||||||
}
|
}
|
||||||
addLoader(submitSpan);
|
addLoader(submitSpan);
|
||||||
@ -330,8 +278,8 @@ const create = (event: SubmitEvent) => {
|
|||||||
if (window.reCAPTCHA) {
|
if (window.reCAPTCHA) {
|
||||||
send.captcha_text = grecaptcha.getResponse();
|
send.captcha_text = grecaptcha.getResponse();
|
||||||
} else {
|
} else {
|
||||||
send.captcha_id = captchaID;
|
send.captcha_id = captcha.captchaID;
|
||||||
send.captcha_text = captchaInput.value;
|
send.captcha_text = captcha.input.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_post("/newUser", send, (req: XMLHttpRequest) => {
|
_post("/newUser", send, (req: XMLHttpRequest) => {
|
||||||
|
@ -188,7 +188,7 @@ class user implements User, SearchableItem {
|
|||||||
if (!telegram && !discord && !matrix && !email) return;
|
if (!telegram && !discord && !matrix && !email) return;
|
||||||
let innerHTML = `
|
let innerHTML = `
|
||||||
<i class="icon ri-settings-2-line ml-2 dropdown-button"></i>
|
<i class="icon ri-settings-2-line ml-2 dropdown-button"></i>
|
||||||
<div class="dropdown over-top manual">
|
<div class="dropdown manual">
|
||||||
<div class="dropdown-display lg">
|
<div class="dropdown-display lg">
|
||||||
<div class="card ~neutral @low">
|
<div class="card ~neutral @low">
|
||||||
<div class="supra sm mb-2">${window.lang.strings("contactThrough")}</div>
|
<div class="supra sm mb-2">${window.lang.strings("contactThrough")}</div>
|
||||||
|
81
ts/modules/captcha.ts
Normal file
81
ts/modules/captcha.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { _get, _post } from "./common.js";
|
||||||
|
|
||||||
|
export class Captcha {
|
||||||
|
isPWR = false;
|
||||||
|
enabled = true;
|
||||||
|
verified = false;
|
||||||
|
captchaID = "";
|
||||||
|
input = document.getElementById("captcha-input") as HTMLInputElement;
|
||||||
|
checkbox = document.getElementById("captcha-success") as HTMLSpanElement;
|
||||||
|
previous = "";
|
||||||
|
reCAPTCHA = false;
|
||||||
|
code = "";
|
||||||
|
|
||||||
|
get value(): string { return this.input.value; }
|
||||||
|
|
||||||
|
hasChanged = (): boolean => { return this.value != this.previous; }
|
||||||
|
|
||||||
|
baseValidatorWrapper = (_baseValidator: (oncomplete: (valid: boolean) => void, captchaValid: boolean) => void) => {
|
||||||
|
return (oncomplete: (valid: boolean) => void): void => {
|
||||||
|
if (this.enabled && !this.reCAPTCHA && this.hasChanged()) {
|
||||||
|
this.previous = this.value;
|
||||||
|
this.verify(() => {
|
||||||
|
_baseValidator(oncomplete, this.verified);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_baseValidator(oncomplete, this.verified);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
verify = (callback: () => void) => _post("/captcha/verify/" + this.code + "/" + this.captchaID + "/" + this.input.value + (this.isPWR ? "?pwr=true" : ""), null, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
if (req.status == 204) {
|
||||||
|
this.checkbox.innerHTML = `<i class="ri-check-line"></i>`;
|
||||||
|
this.checkbox.classList.add("~positive");
|
||||||
|
this.checkbox.classList.remove("~critical");
|
||||||
|
this.verified = true;
|
||||||
|
} else {
|
||||||
|
this.checkbox.innerHTML = `<i class="ri-close-line"></i>`;
|
||||||
|
this.checkbox.classList.add("~critical");
|
||||||
|
this.checkbox.classList.remove("~positive");
|
||||||
|
this.verified = false;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
generate = () => _get("/captcha/gen/"+this.code+(this.isPWR ? "?pwr=true" : ""), null, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
if (req.status == 200) {
|
||||||
|
this.captchaID = this.isPWR ? this.code : req.response["id"];
|
||||||
|
// the Math.random() appearance below is used for PWRs, since they don't have a unique captchaID. The parameter is ignored by the server, but tells the browser to reload the image.
|
||||||
|
document.getElementById("captcha-img").innerHTML = `
|
||||||
|
<img class="w-100" src="${window.location.toString().substring(0, window.location.toString().lastIndexOf("/invite"))}/captcha/img/${this.code}/${this.isPWR ? Math.random() : this.captchaID}${this.isPWR ? "?pwr=true" : ""}"></img>
|
||||||
|
`;
|
||||||
|
this.input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(code: string, enabled: boolean, reCAPTCHA: boolean, isPWR: boolean) {
|
||||||
|
this.code = code;
|
||||||
|
this.enabled = enabled;
|
||||||
|
this.reCAPTCHA = reCAPTCHA;
|
||||||
|
this.isPWR = isPWR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GreCAPTCHA {
|
||||||
|
render: (container: HTMLDivElement, parameters: {
|
||||||
|
sitekey?: string,
|
||||||
|
theme?: string,
|
||||||
|
size?: string,
|
||||||
|
tabindex?: number,
|
||||||
|
"callback"?: () => void,
|
||||||
|
"expired-callback"?: () => void,
|
||||||
|
"error-callback"?: () => void
|
||||||
|
}) => void;
|
||||||
|
getResponse: (opt_widget_id?: HTMLDivElement) => string;
|
||||||
|
}
|
||||||
|
|
@ -263,3 +263,31 @@ export function insertText(textarea: HTMLTextAreaElement, text: string) {
|
|||||||
textarea.focus();
|
textarea.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function bindManualDropdowns() {
|
||||||
|
const buttons = Array.from(document.getElementsByClassName("dropdown-manual-toggle") as HTMLCollectionOf<HTMLSpanElement>);
|
||||||
|
for (let button of buttons) {
|
||||||
|
const parent = button.closest(".dropdown.manual");
|
||||||
|
const display = parent.querySelector(".dropdown-display");
|
||||||
|
const mousein = () => parent.classList.add("selected");
|
||||||
|
const mouseout = () => parent.classList.remove("selected");
|
||||||
|
button.addEventListener("mouseover", mousein);
|
||||||
|
button.addEventListener("mouseout", mouseout);
|
||||||
|
display.addEventListener("mouseover", mousein);
|
||||||
|
display.addEventListener("mouseout", mouseout);
|
||||||
|
button.onclick = () => {
|
||||||
|
parent.classList.add("selected");
|
||||||
|
document.addEventListener("click", outerClickListener);
|
||||||
|
button.removeEventListener("mouseout", mouseout);
|
||||||
|
display.removeEventListener("mouseout", mouseout);
|
||||||
|
};
|
||||||
|
const outerClickListener = (event: Event) => {
|
||||||
|
if (!(event.target instanceof HTMLElement && (display.contains(event.target) || button.contains(event.target)))) {
|
||||||
|
parent.classList.remove("selected");
|
||||||
|
document.removeEventListener("click", outerClickListener);
|
||||||
|
button.addEventListener("mouseout", mouseout);
|
||||||
|
display.addEventListener("mouseout", mouseout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -78,7 +78,7 @@ class DOMInvite implements Invite {
|
|||||||
get expiresIn(): string { return this._expiresIn }
|
get expiresIn(): string { return this._expiresIn }
|
||||||
set expiresIn(expiry: string) {
|
set expiresIn(expiry: string) {
|
||||||
this._expiresIn = expiry;
|
this._expiresIn = expiry;
|
||||||
this._infoArea.querySelector("span.inv-duration").textContent = expiry;
|
this._codeArea.querySelector("span.inv-duration").textContent = expiry;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _userExpiry: string;
|
private _userExpiry: string;
|
||||||
@ -110,15 +110,15 @@ class DOMInvite implements Invite {
|
|||||||
const chip = container.querySelector("span.inv-email-chip");
|
const chip = container.querySelector("span.inv-email-chip");
|
||||||
const tooltip = container.querySelector("span.content") as HTMLSpanElement;
|
const tooltip = container.querySelector("span.content") as HTMLSpanElement;
|
||||||
if (address == "") {
|
if (address == "") {
|
||||||
container.classList.remove("mr-4");
|
|
||||||
icon.classList.remove("ri-mail-line");
|
icon.classList.remove("ri-mail-line");
|
||||||
icon.classList.remove("ri-mail-close-line");
|
icon.classList.remove("ri-mail-close-line");
|
||||||
chip.classList.remove("~neutral");
|
chip.classList.remove("~neutral");
|
||||||
chip.classList.remove("~critical");
|
chip.classList.remove("~critical");
|
||||||
chip.classList.remove("chip");
|
chip.classList.remove("button");
|
||||||
|
chip.parentElement.classList.remove("h-100");
|
||||||
} else {
|
} else {
|
||||||
container.classList.add("mr-4");
|
chip.classList.add("button");
|
||||||
chip.classList.add("chip");
|
chip.parentElement.classList.add("h-100");
|
||||||
if (address.includes("Failed")) {
|
if (address.includes("Failed")) {
|
||||||
icon.classList.remove("ri-mail-line");
|
icon.classList.remove("ri-mail-line");
|
||||||
icon.classList.add("ri-mail-close-line");
|
icon.classList.add("ri-mail-close-line");
|
||||||
@ -266,18 +266,21 @@ class DOMInvite implements Invite {
|
|||||||
constructor(invite: Invite) {
|
constructor(invite: Invite) {
|
||||||
// first create the invite structure, then use our setter methods to fill in the data.
|
// first create the invite structure, then use our setter methods to fill in the data.
|
||||||
this._container = document.createElement('div') as HTMLDivElement;
|
this._container = document.createElement('div') as HTMLDivElement;
|
||||||
this._container.classList.add("inv");
|
this._container.classList.add("inv", "overflow-y-visible");
|
||||||
|
|
||||||
this._header = document.createElement('div') as HTMLDivElement;
|
this._header = document.createElement('div') as HTMLDivElement;
|
||||||
this._container.appendChild(this._header);
|
this._container.appendChild(this._header);
|
||||||
this._header.classList.add("card", "dark:~d_neutral", "@low", "inv-header", "elem-pad", "no-pad", "flex-expand", "row", "mt-2", "overflow-y");
|
this._header.classList.add("card", "dark:~d_neutral", "@low", "inv-header", "flex", "flex-row", "justify-between", "mt-2", "overflow-visible", "gap-2");
|
||||||
|
|
||||||
this._codeArea = document.createElement('div') as HTMLDivElement;
|
this._codeArea = document.createElement('div') as HTMLDivElement;
|
||||||
this._header.appendChild(this._codeArea);
|
this._header.appendChild(this._codeArea);
|
||||||
this._codeArea.classList.add("inv-codearea");
|
this._codeArea.classList.add("flex", "flex-row", "flex-wrap", "justify-between", "w-100", "items-baseline", "gap-2", "truncate");
|
||||||
this._codeArea.innerHTML = `
|
this._codeArea.innerHTML = `
|
||||||
<a class="invite-link text-black dark:text-white font-mono bg-inherit mr-4" href=""></a>
|
<div class="flex items-baseline gap-x-4 gap-y-2 truncate">
|
||||||
|
<a class="invite-link text-black dark:text-white font-mono bg-inherit" href=""></a>
|
||||||
<span class="button ~info @low" title="${window.lang.strings("copy")}"><i class="ri-file-copy-line"></i></span>
|
<span class="button ~info @low" title="${window.lang.strings("copy")}"><i class="ri-file-copy-line"></i></span>
|
||||||
|
</div>
|
||||||
|
<span class="inv-duration"></span>
|
||||||
`;
|
`;
|
||||||
const copyButton = this._codeArea.querySelector("span.button") as HTMLSpanElement;
|
const copyButton = this._codeArea.querySelector("span.button") as HTMLSpanElement;
|
||||||
copyButton.onclick = () => {
|
copyButton.onclick = () => {
|
||||||
@ -297,14 +300,13 @@ class DOMInvite implements Invite {
|
|||||||
|
|
||||||
this._infoArea = document.createElement('div') as HTMLDivElement;
|
this._infoArea = document.createElement('div') as HTMLDivElement;
|
||||||
this._header.appendChild(this._infoArea);
|
this._header.appendChild(this._infoArea);
|
||||||
this._infoArea.classList.add("inv-infoarea");
|
this._infoArea.classList.add("inv-infoarea", "flex", "flex-row", "items-baseline", "gap-2");
|
||||||
this._infoArea.innerHTML = `
|
this._infoArea.innerHTML = `
|
||||||
<div class="tooltip left">
|
<div class="tooltip below darker" tabindex="0">
|
||||||
<span class="inv-email-chip"><i></i></span>
|
<span class="inv-email-chip h-100"><i></i></span>
|
||||||
<span class="content sm"></span>
|
<span class="content sm p-1"></span>
|
||||||
</div>
|
</div>
|
||||||
<span class="inv-duration mr-4"></span>
|
<span class="button ~critical @low inv-delete h-100">${window.lang.strings("delete")}</span>
|
||||||
<span class="button ~critical @low inv-delete">${window.lang.strings("delete")}</span>
|
|
||||||
<label>
|
<label>
|
||||||
<i class="icon clickable ri-arrow-down-s-line not-rotated"></i>
|
<i class="icon clickable ri-arrow-down-s-line not-rotated"></i>
|
||||||
<input class="inv-toggle-details unfocused" type="checkbox">
|
<input class="inv-toggle-details unfocused" type="checkbox">
|
||||||
@ -315,11 +317,13 @@ class DOMInvite implements Invite {
|
|||||||
|
|
||||||
const toggle = (this._infoArea.querySelector("input.inv-toggle-details") as HTMLInputElement);
|
const toggle = (this._infoArea.querySelector("input.inv-toggle-details") as HTMLInputElement);
|
||||||
toggle.onchange = () => { this.expanded = !this.expanded; };
|
toggle.onchange = () => { this.expanded = !this.expanded; };
|
||||||
this._header.onclick = (event: Event) => {
|
const toggleDetails = (event: Event) => {
|
||||||
if (event.target == this._header) {
|
if (event.target == this._header || event.target == this._codeArea || event.target == this._infoArea) {
|
||||||
this.expanded = !this.expanded;
|
this.expanded = !this.expanded;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
this._header.onclick = toggleDetails;
|
||||||
|
|
||||||
|
|
||||||
this._details = document.createElement('div') as HTMLDivElement;
|
this._details = document.createElement('div') as HTMLDivElement;
|
||||||
this._container.appendChild(this._details);
|
this._container.appendChild(this._details);
|
||||||
|
@ -311,7 +311,7 @@ export class Search {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const container = document.createElement("span") as HTMLSpanElement;
|
const container = document.createElement("span") as HTMLSpanElement;
|
||||||
container.classList.add("button", "button-xl", "~neutral", "@low", "mb-1", "mr-2");
|
container.classList.add("button", "button-xl", "~neutral", "@low", "mb-1", "mr-2", "align-bottom");
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="flex flex-col mr-2">
|
<div class="flex flex-col mr-2">
|
||||||
<span>${query.name}</span>
|
<span>${query.name}</span>
|
||||||
|
@ -22,13 +22,18 @@ export class Tabs implements Tabs {
|
|||||||
|
|
||||||
switch = (tabID: string, noRun: boolean = false, keepURL: boolean = false) => {
|
switch = (tabID: string, noRun: boolean = false, keepURL: boolean = false) => {
|
||||||
this._current = tabID;
|
this._current = tabID;
|
||||||
|
let baseOffset = -1;
|
||||||
for (let t of this.tabs) {
|
for (let t of this.tabs) {
|
||||||
|
if (baseOffset == -1) baseOffset = t.buttonEl.offsetLeft;
|
||||||
if (t.tabID == tabID) {
|
if (t.tabID == tabID) {
|
||||||
t.buttonEl.classList.add("active", "~urge");
|
t.buttonEl.classList.add("active", "~urge");
|
||||||
if (t.preFunc && !noRun) { t.preFunc(); }
|
if (t.preFunc && !noRun) { t.preFunc(); }
|
||||||
t.tabEl.classList.remove("unfocused");
|
t.tabEl.classList.remove("unfocused");
|
||||||
if (t.postFunc && !noRun) { t.postFunc(); }
|
if (t.postFunc && !noRun) { t.postFunc(); }
|
||||||
document.dispatchEvent(new CustomEvent("tab-change", { detail: keepURL ? "" : tabID }));
|
document.dispatchEvent(new CustomEvent("tab-change", { detail: keepURL ? "" : tabID }));
|
||||||
|
// t.buttonEl.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' })
|
||||||
|
|
||||||
|
t.buttonEl.parentElement.scrollTo(t.buttonEl.offsetLeft-baseOffset, 0);
|
||||||
} else {
|
} else {
|
||||||
t.buttonEl.classList.remove("active");
|
t.buttonEl.classList.remove("active");
|
||||||
t.buttonEl.classList.remove("~urge");
|
t.buttonEl.classList.remove("~urge");
|
||||||
|
48
ts/pwr.ts
48
ts/pwr.ts
@ -2,6 +2,7 @@ import { Modal } from "./modules/modal.js";
|
|||||||
import { Validator, ValidatorConf } 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";
|
||||||
|
import { Captcha, GreCAPTCHA } from "./modules/captcha.js";
|
||||||
|
|
||||||
interface formWindow extends Window {
|
interface formWindow extends Window {
|
||||||
invalidPassword: string;
|
invalidPassword: string;
|
||||||
@ -28,6 +29,10 @@ interface formWindow extends Window {
|
|||||||
userExpiryHours: number;
|
userExpiryHours: number;
|
||||||
userExpiryMinutes: number;
|
userExpiryMinutes: number;
|
||||||
userExpiryMessage: string;
|
userExpiryMessage: string;
|
||||||
|
captcha: boolean;
|
||||||
|
reCAPTCHA: boolean;
|
||||||
|
reCAPTCHASiteKey: string;
|
||||||
|
pwrPIN: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadLangSelector("pwr");
|
loadLangSelector("pwr");
|
||||||
@ -42,11 +47,26 @@ const rePasswordField = document.getElementById("create-reenter-password") as HT
|
|||||||
|
|
||||||
window.successModal = new Modal(document.getElementById("modal-success"), true);
|
window.successModal = new Modal(document.getElementById("modal-success"), true);
|
||||||
|
|
||||||
|
function _baseValidator(oncomplete: (valid: boolean) => void, captchaValid: boolean): void {
|
||||||
|
if (window.captcha && !window.reCAPTCHA && !captchaValid) {
|
||||||
|
oncomplete(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
oncomplete(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let captcha = new Captcha(window.pwrPIN, window.captcha, window.reCAPTCHA, true);
|
||||||
|
|
||||||
|
declare var grecaptcha: GreCAPTCHA;
|
||||||
|
|
||||||
|
let baseValidator = captcha.baseValidatorWrapper(_baseValidator);
|
||||||
|
|
||||||
let validatorConf: ValidatorConf = {
|
let validatorConf: ValidatorConf = {
|
||||||
passwordField: passwordField,
|
passwordField: passwordField,
|
||||||
rePasswordField: rePasswordField,
|
rePasswordField: rePasswordField,
|
||||||
submitInput: submitInput,
|
submitInput: submitInput,
|
||||||
submitButton: submitSpan
|
submitButton: submitSpan,
|
||||||
|
validatorFunc: baseValidator
|
||||||
};
|
};
|
||||||
|
|
||||||
var validator = new Validator(validatorConf);
|
var validator = new Validator(validatorConf);
|
||||||
@ -55,6 +75,13 @@ var requirements = validator.requirements;
|
|||||||
interface sendDTO {
|
interface sendDTO {
|
||||||
pin: string;
|
pin: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
captcha_text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.captcha && !window.reCAPTCHA) {
|
||||||
|
captcha.generate();
|
||||||
|
(document.getElementById("captcha-regen") as HTMLSpanElement).onclick = captcha.generate;
|
||||||
|
captcha.input.onkeyup = validator.validate;
|
||||||
}
|
}
|
||||||
|
|
||||||
form.onsubmit = (event: Event) => {
|
form.onsubmit = (event: Event) => {
|
||||||
@ -65,13 +92,32 @@ form.onsubmit = (event: Event) => {
|
|||||||
pin: params.get("pin"),
|
pin: params.get("pin"),
|
||||||
password: passwordField.value
|
password: passwordField.value
|
||||||
};
|
};
|
||||||
|
if (window.captcha) {
|
||||||
|
if (window.reCAPTCHA) {
|
||||||
|
send.captcha_text = grecaptcha.getResponse();
|
||||||
|
} else {
|
||||||
|
send.captcha_text = captcha.input.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
_post("/reset", send, (req: XMLHttpRequest) => {
|
_post("/reset", send, (req: XMLHttpRequest) => {
|
||||||
if (req.readyState == 4) {
|
if (req.readyState == 4) {
|
||||||
removeLoader(submitSpan);
|
removeLoader(submitSpan);
|
||||||
if (req.status == 400) {
|
if (req.status == 400) {
|
||||||
|
if (req.response["error"] as string) {
|
||||||
|
const old = submitSpan.textContent;
|
||||||
|
submitSpan.textContent = window.messages[req.response["error"]];
|
||||||
|
submitSpan.classList.add("~critical");
|
||||||
|
submitSpan.classList.remove("~urge");
|
||||||
|
setTimeout(() => {
|
||||||
|
submitSpan.classList.add("~urge");
|
||||||
|
submitSpan.classList.remove("~critical");
|
||||||
|
submitSpan.textContent = old;
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
for (let type in req.response) {
|
for (let type in req.response) {
|
||||||
if (requirements[type]) { requirements[type].valid = req.response[type] as boolean; }
|
if (requirements[type]) { requirements[type].valid = req.response[type] as boolean; }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
} else if (req.status != 200) {
|
} else if (req.status != 200) {
|
||||||
const old = submitSpan.textContent;
|
const old = submitSpan.textContent;
|
||||||
|
64
views.go
64
views.go
@ -296,6 +296,10 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
|||||||
data["telegramEnabled"] = false
|
data["telegramEnabled"] = false
|
||||||
data["discordEnabled"] = false
|
data["discordEnabled"] = false
|
||||||
data["matrixEnabled"] = false
|
data["matrixEnabled"] = false
|
||||||
|
data["captcha"] = app.config.Section("captcha").Key("enabled").MustBool(false)
|
||||||
|
data["reCAPTCHA"] = app.config.Section("captcha").Key("recaptcha").MustBool(false)
|
||||||
|
data["reCAPTCHASiteKey"] = app.config.Section("captcha").Key("recaptcha_site_key").MustString("")
|
||||||
|
data["pwrPIN"] = pin
|
||||||
gcHTML(gc, http.StatusOK, "form-loader.html", data)
|
gcHTML(gc, http.StatusOK, "form-loader.html", data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -393,8 +397,13 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
|||||||
// @Router /captcha/img/{code}/{captchaID} [get]
|
// @Router /captcha/img/{code}/{captchaID} [get]
|
||||||
func (app *appContext) GetCaptcha(gc *gin.Context) {
|
func (app *appContext) GetCaptcha(gc *gin.Context) {
|
||||||
code := gc.Param("invCode")
|
code := gc.Param("invCode")
|
||||||
|
isPWR := gc.Query("pwr") == "true"
|
||||||
captchaID := gc.Param("captchaID")
|
captchaID := gc.Param("captchaID")
|
||||||
inv, ok := app.storage.GetInvitesKey(code)
|
var inv Invite
|
||||||
|
var capt Captcha
|
||||||
|
ok := true
|
||||||
|
if !isPWR {
|
||||||
|
inv, ok = app.storage.GetInvitesKey(code)
|
||||||
if !ok {
|
if !ok {
|
||||||
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
||||||
"urlBase": app.getURLBase(gc),
|
"urlBase": app.getURLBase(gc),
|
||||||
@ -403,10 +412,13 @@ func (app *appContext) GetCaptcha(gc *gin.Context) {
|
|||||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
var capt Captcha
|
|
||||||
ok = true
|
|
||||||
if inv.Captchas != nil {
|
if inv.Captchas != nil {
|
||||||
capt, ok = inv.Captchas[captchaID]
|
capt, ok = inv.Captchas[captchaID]
|
||||||
|
} else {
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
capt, ok = app.pwrCaptchas[code]
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
@ -425,7 +437,13 @@ func (app *appContext) GetCaptcha(gc *gin.Context) {
|
|||||||
// @tags Users
|
// @tags Users
|
||||||
func (app *appContext) GenCaptcha(gc *gin.Context) {
|
func (app *appContext) GenCaptcha(gc *gin.Context) {
|
||||||
code := gc.Param("invCode")
|
code := gc.Param("invCode")
|
||||||
inv, ok := app.storage.GetInvitesKey(code)
|
isPWR := gc.Query("pwr") == "true"
|
||||||
|
var inv Invite
|
||||||
|
ok := true
|
||||||
|
if !isPWR {
|
||||||
|
inv, ok = app.storage.GetInvitesKey(code)
|
||||||
|
}
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
||||||
"urlBase": app.getURLBase(gc),
|
"urlBase": app.getURLBase(gc),
|
||||||
@ -440,7 +458,7 @@ func (app *appContext) GenCaptcha(gc *gin.Context) {
|
|||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if inv.Captchas == nil {
|
if !isPWR && inv.Captchas == nil {
|
||||||
inv.Captchas = map[string]Captcha{}
|
inv.Captchas = map[string]Captcha{}
|
||||||
}
|
}
|
||||||
captchaID := genAuthToken()
|
captchaID := genAuthToken()
|
||||||
@ -450,26 +468,43 @@ func (app *appContext) GenCaptcha(gc *gin.Context) {
|
|||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if isPWR {
|
||||||
|
if app.pwrCaptchas == nil {
|
||||||
|
app.pwrCaptchas = map[string]Captcha{}
|
||||||
|
}
|
||||||
|
app.pwrCaptchas[code] = Captcha{
|
||||||
|
Answer: capt.Text,
|
||||||
|
Image: buf.Bytes(),
|
||||||
|
Generated: time.Now(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
inv.Captchas[captchaID] = Captcha{
|
inv.Captchas[captchaID] = Captcha{
|
||||||
Answer: capt.Text,
|
Answer: capt.Text,
|
||||||
Image: buf.Bytes(),
|
Image: buf.Bytes(),
|
||||||
Generated: time.Now(),
|
Generated: time.Now(),
|
||||||
}
|
}
|
||||||
app.storage.SetInvitesKey(code, inv)
|
app.storage.SetInvitesKey(code, inv)
|
||||||
|
}
|
||||||
gc.JSON(200, genCaptchaDTO{captchaID})
|
gc.JSON(200, genCaptchaDTO{captchaID})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) verifyCaptcha(code, id, text string) bool {
|
func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool {
|
||||||
reCAPTCHA := app.config.Section("captcha").Key("recaptcha").MustBool(false)
|
reCAPTCHA := app.config.Section("captcha").Key("recaptcha").MustBool(false)
|
||||||
if !reCAPTCHA {
|
if !reCAPTCHA {
|
||||||
// internal CAPTCHA
|
// internal CAPTCHA
|
||||||
|
var c Captcha
|
||||||
|
ok := true
|
||||||
|
if !isPWR {
|
||||||
inv, ok := app.storage.GetInvitesKey(code)
|
inv, ok := app.storage.GetInvitesKey(code)
|
||||||
if !ok || inv.Captchas == nil {
|
if !ok || (!isPWR && inv.Captchas == nil) {
|
||||||
app.debug.Printf("Couldn't find invite \"%s\"", code)
|
app.debug.Printf("Couldn't find invite \"%s\"", code)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
c, ok := inv.Captchas[id]
|
c, ok = inv.Captchas[id]
|
||||||
|
} else {
|
||||||
|
c, ok = app.pwrCaptchas[code]
|
||||||
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
app.debug.Printf("Couldn't find Captcha \"%s\"", id)
|
app.debug.Printf("Couldn't find Captcha \"%s\"", id)
|
||||||
return false
|
return false
|
||||||
@ -529,9 +564,14 @@ func (app *appContext) verifyCaptcha(code, id, text string) bool {
|
|||||||
// @Router /captcha/verify/{code}/{captchaID}/{text} [get]
|
// @Router /captcha/verify/{code}/{captchaID}/{text} [get]
|
||||||
func (app *appContext) VerifyCaptcha(gc *gin.Context) {
|
func (app *appContext) VerifyCaptcha(gc *gin.Context) {
|
||||||
code := gc.Param("invCode")
|
code := gc.Param("invCode")
|
||||||
|
isPWR := gc.Query("pwr") == "true"
|
||||||
captchaID := gc.Param("captchaID")
|
captchaID := gc.Param("captchaID")
|
||||||
text := gc.Param("text")
|
text := gc.Param("text")
|
||||||
inv, ok := app.storage.GetInvitesKey(code)
|
var inv Invite
|
||||||
|
var capt Captcha
|
||||||
|
var ok bool
|
||||||
|
if !isPWR {
|
||||||
|
inv, ok = app.storage.GetInvitesKey(code)
|
||||||
if !ok {
|
if !ok {
|
||||||
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
||||||
"urlBase": app.getURLBase(gc),
|
"urlBase": app.getURLBase(gc),
|
||||||
@ -541,9 +581,13 @@ func (app *appContext) VerifyCaptcha(gc *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var capt Captcha
|
|
||||||
if inv.Captchas != nil {
|
if inv.Captchas != nil {
|
||||||
capt, ok = inv.Captchas[captchaID]
|
capt, ok = inv.Captchas[captchaID]
|
||||||
|
} else {
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
capt, ok = app.pwrCaptchas[code]
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
|
Loading…
Reference in New Issue
Block a user