From 213b1e7f9e04a42bc85c1b51f4226f2a3a9fb3b4 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 20 Dec 2023 17:20:59 +0000 Subject: [PATCH] accounts: allow setting exact expiry date set with a text input field which uses the same date parsing library as the search function. Parsed expiry date will appear once you've typed something in, so you can make sure it's right. --- api-users.go | 9 +++- html/admin.html | 68 +++++++++++++++++------------- lang/admin/en-us.json | 3 ++ models.go | 11 ++--- ts/modules/accounts.ts | 96 +++++++++++++++++++++++++++++++++++++++--- 5 files changed, 146 insertions(+), 41 deletions(-) diff --git a/api-users.go b/api-users.go index cb6725f..138c287 100644 --- a/api-users.go +++ b/api-users.go @@ -719,7 +719,7 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) { var req extendExpiryDTO gc.BindJSON(&req) app.info.Printf("Expiry extension requested for %d user(s)", len(req.Users)) - if req.Months <= 0 && req.Days <= 0 && req.Hours <= 0 && req.Minutes <= 0 { + if req.Months <= 0 && req.Days <= 0 && req.Hours <= 0 && req.Minutes <= 0 && req.Timestamp <= 0 { respondBool(400, false, gc) return } @@ -731,7 +731,12 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) { } else { app.debug.Printf("Created expiry for \"%s\"", id) } - expiry := UserExpiry{Expiry: base.AddDate(0, req.Months, req.Days).Add(time.Duration(((60 * req.Hours) + req.Minutes)) * time.Minute)} + expiry := UserExpiry{} + if req.Timestamp != 0 { + expiry.Expiry = time.Unix(req.Timestamp, 0) + } else { + expiry.Expiry = base.AddDate(0, req.Months, req.Days).Add(time.Duration(((60 * req.Hours) + req.Minutes)) * time.Minute) + } app.storage.SetUserExpiryKey(id, expiry) } respondBool(204, true, gc) diff --git a/html/admin.html b/html/admin.html index 7a5a04d..a035767 100644 --- a/html/admin.html +++ b/html/admin.html @@ -181,39 +181,49 @@
×
-
-
- -
- -
-
-
- -
- -
+ +
+ {{ .strings.setExpiry }} +
+
-
-
- -
- +
+ {{ .strings.extendExpiry }} +
+
+ +
+ +
+
+
+ +
+ +
-
- -
- +
+
+ +
+ +
+
+
+ +
+ +
diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json index e52ce21..c1a4c55 100644 --- a/lang/admin/en-us.json +++ b/lang/admin/en-us.json @@ -64,6 +64,7 @@ "extendExpiry": "Extend expiry", "setExpiry": "Set expiry", "removeExpiry": "Remove expiry", + "enterExpiry": "Enter an expiry", "sendPWRManual": "User {n} has no method of contact, press copy to get a link to send to them.", "sendPWRSuccess": "Password reset link sent.", "sendPWRSuccessManual": "If the user hasn't received it, press copy to get a link to manually send to them.", @@ -149,6 +150,7 @@ "accountDisabled": "Account disabled: {user}", "accountReEnabled": "Account re-enabled: {user}", "accountExpired": "Account expired: {user}", + "accountWillExpire": "Account will expire on {date}", "userDeleted": "User was deleted.", "userDisabled": "User was disabled", "inviteCreated": "Invite created: {invite}", @@ -219,6 +221,7 @@ "errorCheckUpdate": "Failed to check for update.", "errorNoReferralTemplate": "Profile doesn't contain referral template, add one in settings.", "errorLoadActivities": "Failed to load activities.", + "errorInvalidDate": "Date is invalid.", "updateAvailable": "A new update is available, check settings.", "noUpdatesAvailable": "No new updates available." }, diff --git a/models.go b/models.go index 416f7b6..becc66d 100644 --- a/models.go +++ b/models.go @@ -261,11 +261,12 @@ type customEmailDTO struct { } type extendExpiryDTO struct { - Users []string `json:"users"` // List of user IDs to apply to. - Months int `json:"months" example:"1"` // Number of months to add. - Days int `json:"days" example:"1"` // Number of days to add. - Hours int `json:"hours" example:"2"` // Number of hours to add. - Minutes int `json:"minutes" example:"3"` // Number of minutes to add. + Users []string `json:"users"` // List of user IDs to apply to. + Months int `json:"months" example:"1"` // Number of months to add. + Days int `json:"days" example:"1"` // Number of days to add. + Hours int `json:"hours" example:"2"` // Number of hours to add. + Minutes int `json:"minutes" example:"3"` // Number of minutes to add. + Timestamp int64 `json:"timestamp"` // Optional, exact time to expire at. Overrides other fields. } type checkUpdateDTO struct { diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index 9216f8d..55a07cd 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -771,6 +771,12 @@ export class accountsList { private _deleteReason = document.getElementById("textarea-delete-user") as HTMLTextAreaElement; private _expiryDropdown = document.getElementById("accounts-expiry-dropdown") as HTMLElement; private _extendExpiry = document.getElementById("accounts-extend-expiry") as HTMLSpanElement; + private _extendExpiryForm = document.getElementById("form-extend-expiry") as HTMLFormElement; + private _extendExpiryTextInput = document.getElementById("extend-expiry-text") as HTMLInputElement; + private _extendExpiryFieldInputs = document.getElementById("extend-expiry-field-inputs") as HTMLElement; + private _usingExtendExpiryTextInput = true; + + private _extendExpiryDate = document.getElementById("extend-expiry-date") as HTMLElement; private _removeExpiry = document.getElementById("accounts-remove-expiry") as HTMLSpanElement; private _enableExpiryNotify = document.getElementById("expiry-extend-enable") as HTMLInputElement; private _enableExpiryReason = document.getElementById("textarea-extend-enable") as HTMLTextAreaElement; @@ -1625,6 +1631,45 @@ export class accountsList { this.reload(); } + _displayExpiryDate = () => { + let date: Date; + let invalid = false; + if (this._usingExtendExpiryTextInput) { + date = (Date as any).fromString(this._extendExpiryTextInput.value) as Date; + invalid = "invalid" in (date as any); + } else { + let fields: Array = [ + document.getElementById("extend-expiry-months") as HTMLSelectElement, + document.getElementById("extend-expiry-days") as HTMLSelectElement, + document.getElementById("extend-expiry-hours") as HTMLSelectElement, + document.getElementById("extend-expiry-minutes") as HTMLSelectElement + ]; + invalid = fields[0].value == "0" && fields[1].value == "0" && fields[2].value == "0" && fields[3].value == "0"; + let id = this._collectUsers().length == 1 ? this._collectUsers()[0] : ""; + if (!id) invalid = true; + else { + date = new Date(this._users[id].expiry*1000); + if (this._users[id].expiry == 0) date = new Date(); + date.setMonth(date.getMonth() + (+fields[0].value)) + date.setDate(date.getDate() + (+fields[1].value)); + date.setHours(date.getHours() + (+fields[2].value)); + date.setMinutes(date.getMinutes() + (+fields[3].value)); + } + } + const submit = this._extendExpiryForm.querySelector(`input[type="submit"]`) as HTMLInputElement; + const submitSpan = submit.nextElementSibling; + if (invalid) { + submit.disabled = true; + submitSpan.classList.add("opacity-60"); + this._extendExpiryDate.classList.add("unfocused"); + } else { + submit.disabled = false; + submitSpan.classList.remove("opacity-60"); + this._extendExpiryDate.textContent = window.lang.strings("accountWillExpire").replace("{date}", toDateString(date)); + this._extendExpiryDate.classList.remove("unfocused"); + } + } + extendExpiry = (enableUser?: boolean) => { const list = this._collectUsers(); let applyList: string[] = []; @@ -1647,10 +1692,20 @@ export class accountsList { } document.getElementById("header-extend-expiry").textContent = header; const extend = () => { - let send = { "users": applyList } - for (let field of ["months", "days", "hours", "minutes"]) { - send[field] = +(document.getElementById("extend-expiry-"+field) as HTMLSelectElement).value; + let send = { "users": applyList, "timestamp": 0 } + if (this._usingExtendExpiryTextInput) { + let date = (Date as any).fromString(this._extendExpiryTextInput.value) as Date; + send["timestamp"] = Math.floor(date.getTime() / 1000); + if ("invalid" in (date as any)) { + window.notifications.customError("extendExpiryError", window.lang.notif("errorInvalidDate")); + return; + } + } else { + for (let field of ["months", "days", "hours", "minutes"]) { + send[field] = +(document.getElementById("extend-expiry-"+field) as HTMLSelectElement).value; + } } + _post("/users/extend", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status != 200 && req.status != 204) { @@ -1663,8 +1718,7 @@ export class accountsList { } }); }; - const form = document.getElementById("form-extend-expiry") as HTMLFormElement; - form.onsubmit = (event: Event) => { + this._extendExpiryForm.onsubmit = (event: Event) => { event.preventDefault(); if (enableUser) { this._enableDisableUsers(applyList, true, this._enableExpiryNotify.checked, this._enableExpiryNotify ? this._enableExpiryReason.value : null, (req: XMLHttpRequest) => { @@ -1685,6 +1739,7 @@ export class accountsList { extend(); } } + this._extendExpiryTextInput.value = ""; window.modals.extendExpiry.show(); } @@ -1821,6 +1876,37 @@ export class accountsList { this._extendExpiry.onclick = () => { this.extendExpiry(); }; this._removeExpiry.onclick = () => { this.removeExpiry(); }; this._expiryDropdown.classList.add("unfocused"); + this._extendExpiryDate.classList.add("unfocused"); + + this._extendExpiryTextInput.onkeyup = () => { + this._extendExpiryTextInput.parentElement.parentElement.classList.remove("opacity-60"); + this._extendExpiryFieldInputs.classList.add("opacity-60"); + this._usingExtendExpiryTextInput = true; + this._displayExpiryDate(); + } + + this._extendExpiryTextInput.onclick = () => { + this._extendExpiryTextInput.parentElement.parentElement.classList.remove("opacity-60"); + this._extendExpiryFieldInputs.classList.add("opacity-60"); + this._usingExtendExpiryTextInput = true; + this._displayExpiryDate(); + }; + + this._extendExpiryFieldInputs.onclick = () => { + this._extendExpiryFieldInputs.classList.remove("opacity-60"); + this._extendExpiryTextInput.parentElement.parentElement.classList.add("opacity-60"); + this._usingExtendExpiryTextInput = false; + this._displayExpiryDate(); + }; + + for (let field of ["months", "days", "hours", "minutes"]) { + (document.getElementById("extend-expiry-"+field) as HTMLSelectElement).onchange = () => { + this._extendExpiryFieldInputs.classList.remove("opacity-60"); + this._extendExpiryTextInput.parentElement.parentElement.classList.add("opacity-60"); + this._usingExtendExpiryTextInput = false; + this._displayExpiryDate(); + }; + } this._disableEnable.onclick = this.enableDisableUsers; this._disableEnable.parentElement.classList.add("unfocused");