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.
This commit is contained in:
Harvey Tindall 2023-12-20 17:20:59 +00:00
parent 10c8d4ad2f
commit 213b1e7f9e
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
5 changed files with 146 additions and 41 deletions

View File

@ -719,7 +719,7 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) {
var req extendExpiryDTO var req extendExpiryDTO
gc.BindJSON(&req) gc.BindJSON(&req)
app.info.Printf("Expiry extension requested for %d user(s)", len(req.Users)) 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) respondBool(400, false, gc)
return return
} }
@ -731,7 +731,12 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) {
} else { } else {
app.debug.Printf("Created expiry for \"%s\"", id) 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) app.storage.SetUserExpiryKey(id, expiry)
} }
respondBool(204, true, gc) respondBool(204, true, gc)

View File

@ -181,39 +181,49 @@
<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-4/5 lg:w-1/3" id="form-extend-expiry" href="">
<span class="heading"><span id="header-extend-expiry"></span> <span class="modal-close">&times;</span></span> <span class="heading"><span id="header-extend-expiry"></span> <span class="modal-close">&times;</span></span>
<div class="content mt-8"> <div class="content mt-8">
<div class="row"> <aside class="aside sm ~urge dark:~d_info mb-2 @low row unfocused" id="extend-expiry-date"></aside>
<div class="col"> <div>
<label class="label supra" for="extend-expiry-months">{{ .strings.inviteMonths }}</label> <span class="text-xl supra row py-1">{{ .strings.setExpiry }}</span>
<div class="select ~neutral @low mb-2 mt-4"> <div class="row">
<select id="extend-expiry-months"> <input type="text" id="extend-expiry-text" class="input ~neutral @low mb-2 mt-4" placeholder="{{ .strings.enterExpiry }}">
<option>0</option>
</select>
</div>
</div>
<div class="col">
<label class="label supra" for="extend-expiry-days">{{ .strings.inviteDays }}</label>
<div class="select ~neutral @low mb-2 mt-4">
<select id="extend-expiry-days">
<option>0</option>
</select>
</div>
</div> </div>
</div> </div>
<div class="row"> <div id="extend-expiry-field-inputs">
<div class="col"> <span class="text-xl supra row py-1">{{ .strings.extendExpiry }}</span>
<label class="label supra" for="extend-expiry-hours">{{ .strings.inviteHours }}</label> <div class="row">
<div class="select ~neutral @low mb-2 mt-4"> <div class="col">
<select id="extend-expiry-hours"> <label class="label supra" for="extend-expiry-months">{{ .strings.inviteMonths }}</label>
<option>0</option> <div class="select ~neutral @low mb-2 mt-4">
</select> <select id="extend-expiry-months">
<option>0</option>
</select>
</div>
</div>
<div class="col">
<label class="label supra" for="extend-expiry-days">{{ .strings.inviteDays }}</label>
<div class="select ~neutral @low mb-2 mt-4">
<select id="extend-expiry-days">
<option>0</option>
</select>
</div>
</div> </div>
</div> </div>
<div class="col"> <div class="row">
<label class="label supra" for="extend-expiry-minutes">{{ .strings.inviteMinutes }}</label> <div class="col">
<div class="select ~neutral @low mb-2 mt-4"> <label class="label supra" for="extend-expiry-hours">{{ .strings.inviteHours }}</label>
<select id="extend-expiry-minutes"> <div class="select ~neutral @low mb-2 mt-4">
<option>0</option> <select id="extend-expiry-hours">
</select> <option>0</option>
</select>
</div>
</div>
<div class="col">
<label class="label supra" for="extend-expiry-minutes">{{ .strings.inviteMinutes }}</label>
<div class="select ~neutral @low mb-2 mt-4">
<select id="extend-expiry-minutes">
<option>0</option>
</select>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -64,6 +64,7 @@
"extendExpiry": "Extend expiry", "extendExpiry": "Extend expiry",
"setExpiry": "Set expiry", "setExpiry": "Set expiry",
"removeExpiry": "Remove 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.", "sendPWRManual": "User {n} has no method of contact, press copy to get a link to send to them.",
"sendPWRSuccess": "Password reset link sent.", "sendPWRSuccess": "Password reset link sent.",
"sendPWRSuccessManual": "If the user hasn't received it, press copy to get a link to manually send to them.", "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}", "accountDisabled": "Account disabled: {user}",
"accountReEnabled": "Account re-enabled: {user}", "accountReEnabled": "Account re-enabled: {user}",
"accountExpired": "Account expired: {user}", "accountExpired": "Account expired: {user}",
"accountWillExpire": "Account will expire on {date}",
"userDeleted": "User was deleted.", "userDeleted": "User was deleted.",
"userDisabled": "User was disabled", "userDisabled": "User was disabled",
"inviteCreated": "Invite created: {invite}", "inviteCreated": "Invite created: {invite}",
@ -219,6 +221,7 @@
"errorCheckUpdate": "Failed to check for update.", "errorCheckUpdate": "Failed to check for update.",
"errorNoReferralTemplate": "Profile doesn't contain referral template, add one in settings.", "errorNoReferralTemplate": "Profile doesn't contain referral template, add one in settings.",
"errorLoadActivities": "Failed to load activities.", "errorLoadActivities": "Failed to load activities.",
"errorInvalidDate": "Date is invalid.",
"updateAvailable": "A new update is available, check settings.", "updateAvailable": "A new update is available, check settings.",
"noUpdatesAvailable": "No new updates available." "noUpdatesAvailable": "No new updates available."
}, },

View File

@ -261,11 +261,12 @@ type customEmailDTO struct {
} }
type extendExpiryDTO struct { type extendExpiryDTO struct {
Users []string `json:"users"` // List of user IDs to apply to. Users []string `json:"users"` // List of user IDs to apply to.
Months int `json:"months" example:"1"` // Number of months to add. Months int `json:"months" example:"1"` // Number of months to add.
Days int `json:"days" example:"1"` // Number of days to add. Days int `json:"days" example:"1"` // Number of days to add.
Hours int `json:"hours" example:"2"` // Number of hours to add. Hours int `json:"hours" example:"2"` // Number of hours to add.
Minutes int `json:"minutes" example:"3"` // Number of minutes 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 { type checkUpdateDTO struct {

View File

@ -771,6 +771,12 @@ export class accountsList {
private _deleteReason = document.getElementById("textarea-delete-user") as HTMLTextAreaElement; private _deleteReason = document.getElementById("textarea-delete-user") as HTMLTextAreaElement;
private _expiryDropdown = document.getElementById("accounts-expiry-dropdown") as HTMLElement; private _expiryDropdown = document.getElementById("accounts-expiry-dropdown") as HTMLElement;
private _extendExpiry = document.getElementById("accounts-extend-expiry") as HTMLSpanElement; 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 _removeExpiry = document.getElementById("accounts-remove-expiry") as HTMLSpanElement;
private _enableExpiryNotify = document.getElementById("expiry-extend-enable") as HTMLInputElement; private _enableExpiryNotify = document.getElementById("expiry-extend-enable") as HTMLInputElement;
private _enableExpiryReason = document.getElementById("textarea-extend-enable") as HTMLTextAreaElement; private _enableExpiryReason = document.getElementById("textarea-extend-enable") as HTMLTextAreaElement;
@ -1625,6 +1631,45 @@ export class accountsList {
this.reload(); 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<HTMLSelectElement> = [
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) => { extendExpiry = (enableUser?: boolean) => {
const list = this._collectUsers(); const list = this._collectUsers();
let applyList: string[] = []; let applyList: string[] = [];
@ -1647,10 +1692,20 @@ export class accountsList {
} }
document.getElementById("header-extend-expiry").textContent = header; document.getElementById("header-extend-expiry").textContent = header;
const extend = () => { const extend = () => {
let send = { "users": applyList } let send = { "users": applyList, "timestamp": 0 }
for (let field of ["months", "days", "hours", "minutes"]) { if (this._usingExtendExpiryTextInput) {
send[field] = +(document.getElementById("extend-expiry-"+field) as HTMLSelectElement).value; 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) => { _post("/users/extend", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
if (req.status != 200 && req.status != 204) { if (req.status != 200 && req.status != 204) {
@ -1663,8 +1718,7 @@ export class accountsList {
} }
}); });
}; };
const form = document.getElementById("form-extend-expiry") as HTMLFormElement; this._extendExpiryForm.onsubmit = (event: Event) => {
form.onsubmit = (event: Event) => {
event.preventDefault(); event.preventDefault();
if (enableUser) { if (enableUser) {
this._enableDisableUsers(applyList, true, this._enableExpiryNotify.checked, this._enableExpiryNotify ? this._enableExpiryReason.value : null, (req: XMLHttpRequest) => { this._enableDisableUsers(applyList, true, this._enableExpiryNotify.checked, this._enableExpiryNotify ? this._enableExpiryReason.value : null, (req: XMLHttpRequest) => {
@ -1685,6 +1739,7 @@ export class accountsList {
extend(); extend();
} }
} }
this._extendExpiryTextInput.value = "";
window.modals.extendExpiry.show(); window.modals.extendExpiry.show();
} }
@ -1821,6 +1876,37 @@ export class accountsList {
this._extendExpiry.onclick = () => { this.extendExpiry(); }; this._extendExpiry.onclick = () => { this.extendExpiry(); };
this._removeExpiry.onclick = () => { this.removeExpiry(); }; this._removeExpiry.onclick = () => { this.removeExpiry(); };
this._expiryDropdown.classList.add("unfocused"); 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.onclick = this.enableDisableUsers;
this._disableEnable.parentElement.classList.add("unfocused"); this._disableEnable.parentElement.classList.add("unfocused");