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");