Add keypass schedule
This commit is contained in:
@@ -59,6 +59,7 @@ class Keypass(Base):
|
||||
revoked: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
revoked_at: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
||||
allowed_gates: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # JSON list of gate IDs; NULL = all gates
|
||||
schedule: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # JSON schedule rule; NULL = always allowed
|
||||
|
||||
|
||||
class GateAccessLog(Base):
|
||||
@@ -107,3 +108,9 @@ def get_db():
|
||||
def init_db() -> None:
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
# Lightweight migrations: add columns that may not exist in older databases
|
||||
with engine.connect() as conn:
|
||||
existing = {row[1] for row in conn.execute(text("PRAGMA table_info(keypasses)"))}
|
||||
if "schedule" not in existing:
|
||||
conn.execute(text("ALTER TABLE keypasses ADD COLUMN schedule TEXT"))
|
||||
conn.commit()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
@@ -48,4 +49,19 @@ def require_keypass(
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Keypass has been revoked")
|
||||
if kp.expires_at is not None and kp.expires_at < utcnow():
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Keypass has expired")
|
||||
if kp.schedule:
|
||||
try:
|
||||
sched = json.loads(kp.schedule)
|
||||
except (ValueError, TypeError):
|
||||
sched = {}
|
||||
now_local = datetime.now()
|
||||
allowed_days = sched.get("days")
|
||||
if allowed_days is not None and now_local.weekday() not in allowed_days:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, "Keypass not currently allowed")
|
||||
time_start = sched.get("time_start")
|
||||
time_end = sched.get("time_end")
|
||||
if time_start and time_end:
|
||||
now_hhmm = now_local.strftime("%H:%M")
|
||||
if now_hhmm < time_start or now_hhmm > time_end:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, "Keypass not currently allowed")
|
||||
return kp
|
||||
|
||||
@@ -32,6 +32,13 @@ class AdminLoginResponse(BaseModel):
|
||||
|
||||
# ── Keypasses ─────────────────────────────────────────────────────────────────
|
||||
|
||||
class ScheduleRule(BaseModel):
|
||||
"""Optional time/day-of-week restriction for a keypass."""
|
||||
days: Optional[list[int]] = None # 0=Mon..6=Sun; None/absent = any day
|
||||
time_start: Optional[str] = None # "HH:MM" 24-hour server local time
|
||||
time_end: Optional[str] = None # "HH:MM" 24-hour server local time
|
||||
|
||||
|
||||
class KeypassCreate(BaseModel):
|
||||
description: str
|
||||
expires_at: Optional[datetime] = None # None = never expires
|
||||
@@ -40,12 +47,14 @@ class KeypassCreate(BaseModel):
|
||||
# Auto-generation options (ignored when `code` is supplied manually)
|
||||
length: int = 12 # 6–32
|
||||
charset: str = "alphanumeric" # "alphanumeric" | "alpha" | "numeric" | "passphrase"
|
||||
schedule: Optional[ScheduleRule] = None # None = always allowed
|
||||
|
||||
|
||||
class KeypassPatch(BaseModel):
|
||||
description: Optional[str] = None
|
||||
expires_at: Optional[datetime] = None # None = never expires
|
||||
gate_ids: Optional[list[int]] = None # None = keep unchanged; [] = all gates
|
||||
schedule: Optional[ScheduleRule] = None # absent = keep unchanged; null = clear
|
||||
|
||||
|
||||
class KeypassResponse(BaseModel):
|
||||
@@ -58,9 +67,16 @@ class KeypassResponse(BaseModel):
|
||||
revoked: bool
|
||||
revoked_at: Optional[datetime] = None
|
||||
allowed_gate_ids: list[int] # empty = all gates
|
||||
schedule: Optional[ScheduleRule] = None
|
||||
|
||||
|
||||
def keypass_to_response(kp: Keypass) -> KeypassResponse:
|
||||
sched: Optional[ScheduleRule] = None
|
||||
if kp.schedule:
|
||||
try:
|
||||
sched = ScheduleRule(**json.loads(kp.schedule))
|
||||
except Exception:
|
||||
pass
|
||||
return KeypassResponse(
|
||||
id=kp.id,
|
||||
code=kp.code,
|
||||
@@ -70,6 +86,7 @@ def keypass_to_response(kp: Keypass) -> KeypassResponse:
|
||||
revoked=kp.revoked,
|
||||
revoked_at=kp.revoked_at,
|
||||
allowed_gate_ids=json.loads(kp.allowed_gates) if kp.allowed_gates else [],
|
||||
schedule=sched,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,16 @@ from core.schemas import KeypassCreate, KeypassPatch, KeypassResponse, keypass_t
|
||||
|
||||
router = APIRouter(prefix="/api/admin/keypasses", tags=["admin-keypasses"])
|
||||
|
||||
|
||||
def _serialize_schedule(s) -> Optional[str]:
|
||||
"""Serialize a ScheduleRule to a JSON string, or None if effectively empty."""
|
||||
if s is None:
|
||||
return None
|
||||
d = s.model_dump(exclude_none=True)
|
||||
if "days" in d and not d["days"]:
|
||||
del d["days"]
|
||||
return json.dumps(d) if d else None
|
||||
|
||||
# ── Word list for passphrase mode ─────────────────────────────────────────────
|
||||
_WORDS = [
|
||||
"apple", "beach", "brick", "brush", "cabin", "calm", "cedar", "chain",
|
||||
@@ -102,6 +112,7 @@ async def create_keypass(
|
||||
expires_at=req.expires_at,
|
||||
revoked=False,
|
||||
allowed_gates=json.dumps(req.gate_ids) if req.gate_ids else None,
|
||||
schedule=_serialize_schedule(req.schedule),
|
||||
)
|
||||
db.add(kp)
|
||||
db.commit()
|
||||
@@ -126,6 +137,8 @@ async def update_keypass(
|
||||
kp.expires_at = req.expires_at
|
||||
if req.gate_ids is not None:
|
||||
kp.allowed_gates = json.dumps(req.gate_ids) if req.gate_ids else None
|
||||
if "schedule" in req.model_fields_set:
|
||||
kp.schedule = _serialize_schedule(req.schedule)
|
||||
db.commit()
|
||||
db.refresh(kp)
|
||||
return keypass_to_response(kp)
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
<th>Description</th>
|
||||
<th>Gates</th>
|
||||
<th>Expires</th>
|
||||
<th>Schedule</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -468,6 +469,38 @@
|
||||
<div id="kp-gate-checks" style="display:flex;flex-direction:column;gap:.3rem"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label style="margin-bottom:.5rem">Access schedule</label>
|
||||
<label style="display:flex;align-items:center;gap:.75rem;cursor:pointer;color:var(--text);margin-bottom:.4rem">
|
||||
<input type="checkbox" id="kp-no-schedule" checked style="width:1rem;height:1rem;flex-shrink:0" />
|
||||
<span style="font-size:.9rem">Always accessible (no restriction)</span>
|
||||
</label>
|
||||
<div id="kp-schedule-wrap" style="display:none;flex-direction:column;gap:.75rem;padding:.75rem;background:var(--surface2);border-radius:8px;border:1px solid var(--border)">
|
||||
<div>
|
||||
<div style="font-size:.8rem;font-weight:600;color:var(--text-muted);margin-bottom:.4rem">Allowed days <span style="font-weight:400">(leave all unchecked for any day)</span></div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:.4rem">
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.88rem;padding:.25rem .5rem;background:var(--surface);border-radius:4px;border:1px solid var(--border)"><input type="checkbox" name="kp-day" value="0" style="width:.9rem;height:.9rem" /> Mon</label>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.88rem;padding:.25rem .5rem;background:var(--surface);border-radius:4px;border:1px solid var(--border)"><input type="checkbox" name="kp-day" value="1" style="width:.9rem;height:.9rem" /> Tue</label>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.88rem;padding:.25rem .5rem;background:var(--surface);border-radius:4px;border:1px solid var(--border)"><input type="checkbox" name="kp-day" value="2" style="width:.9rem;height:.9rem" /> Wed</label>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.88rem;padding:.25rem .5rem;background:var(--surface);border-radius:4px;border:1px solid var(--border)"><input type="checkbox" name="kp-day" value="3" style="width:.9rem;height:.9rem" /> Thu</label>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.88rem;padding:.25rem .5rem;background:var(--surface);border-radius:4px;border:1px solid var(--border)"><input type="checkbox" name="kp-day" value="4" style="width:.9rem;height:.9rem" /> Fri</label>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.88rem;padding:.25rem .5rem;background:var(--surface);border-radius:4px;border:1px solid var(--border)"><input type="checkbox" name="kp-day" value="5" style="width:.9rem;height:.9rem" /> Sat</label>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.88rem;padding:.25rem .5rem;background:var(--surface);border-radius:4px;border:1px solid var(--border)"><input type="checkbox" name="kp-day" value="6" style="width:.9rem;height:.9rem" /> Sun</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:.75rem;align-items:flex-end">
|
||||
<div style="display:flex;flex-direction:column;gap:.3rem">
|
||||
<label for="kp-time-start" style="font-size:.8rem;font-weight:600;color:var(--text-muted)">From</label>
|
||||
<input id="kp-time-start" type="time" style="width:auto;font-size:.9rem" />
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:.3rem">
|
||||
<label for="kp-time-end" style="font-size:.8rem;font-weight:600;color:var(--text-muted)">To</label>
|
||||
<input id="kp-time-end" type="time" style="width:auto;font-size:.9rem" />
|
||||
</div>
|
||||
<span style="font-size:.8rem;color:var(--text-muted);padding-bottom:.35rem">(server local time)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p id="kp-error" class="error-msg hidden"></p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="kp-cancel" class="btn btn-ghost">Cancel</button>
|
||||
@@ -505,6 +538,38 @@
|
||||
<div id="kp-edit-gate-checks" style="display:flex;flex-direction:column;gap:.3rem"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label style="margin-bottom:.5rem">Access schedule</label>
|
||||
<label style="display:flex;align-items:center;gap:.75rem;cursor:pointer;color:var(--text);margin-bottom:.4rem">
|
||||
<input type="checkbox" id="kp-edit-no-schedule" checked style="width:1rem;height:1rem;flex-shrink:0" />
|
||||
<span style="font-size:.9rem">Always accessible (no restriction)</span>
|
||||
</label>
|
||||
<div id="kp-edit-schedule-wrap" style="display:none;flex-direction:column;gap:.75rem;padding:.75rem;background:var(--surface2);border-radius:8px;border:1px solid var(--border)">
|
||||
<div>
|
||||
<div style="font-size:.8rem;font-weight:600;color:var(--text-muted);margin-bottom:.4rem">Allowed days <span style="font-weight:400">(leave all unchecked for any day)</span></div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:.4rem">
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.88rem;padding:.25rem .5rem;background:var(--surface);border-radius:4px;border:1px solid var(--border)"><input type="checkbox" name="kp-edit-day" value="0" style="width:.9rem;height:.9rem" /> Mon</label>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.88rem;padding:.25rem .5rem;background:var(--surface);border-radius:4px;border:1px solid var(--border)"><input type="checkbox" name="kp-edit-day" value="1" style="width:.9rem;height:.9rem" /> Tue</label>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.88rem;padding:.25rem .5rem;background:var(--surface);border-radius:4px;border:1px solid var(--border)"><input type="checkbox" name="kp-edit-day" value="2" style="width:.9rem;height:.9rem" /> Wed</label>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.88rem;padding:.25rem .5rem;background:var(--surface);border-radius:4px;border:1px solid var(--border)"><input type="checkbox" name="kp-edit-day" value="3" style="width:.9rem;height:.9rem" /> Thu</label>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.88rem;padding:.25rem .5rem;background:var(--surface);border-radius:4px;border:1px solid var(--border)"><input type="checkbox" name="kp-edit-day" value="4" style="width:.9rem;height:.9rem" /> Fri</label>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.88rem;padding:.25rem .5rem;background:var(--surface);border-radius:4px;border:1px solid var(--border)"><input type="checkbox" name="kp-edit-day" value="5" style="width:.9rem;height:.9rem" /> Sat</label>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.88rem;padding:.25rem .5rem;background:var(--surface);border-radius:4px;border:1px solid var(--border)"><input type="checkbox" name="kp-edit-day" value="6" style="width:.9rem;height:.9rem" /> Sun</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:.75rem;align-items:flex-end">
|
||||
<div style="display:flex;flex-direction:column;gap:.3rem">
|
||||
<label for="kp-edit-time-start" style="font-size:.8rem;font-weight:600;color:var(--text-muted)">From</label>
|
||||
<input id="kp-edit-time-start" type="time" style="width:auto;font-size:.9rem" />
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:.3rem">
|
||||
<label for="kp-edit-time-end" style="font-size:.8rem;font-weight:600;color:var(--text-muted)">To</label>
|
||||
<input id="kp-edit-time-end" type="time" style="width:auto;font-size:.9rem" />
|
||||
</div>
|
||||
<span style="font-size:.8rem;color:var(--text-muted);padding-bottom:.35rem">(server local time)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p id="kp-edit-error" class="error-msg hidden"></p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="kp-edit-cancel" class="btn btn-ghost">Cancel</button>
|
||||
|
||||
@@ -132,12 +132,20 @@ function showToast(msg, isError = false) {
|
||||
}
|
||||
|
||||
// ── Keypasses ─────────────────────────────────────────────────────────────────
|
||||
const _DAY_SHORT = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"];
|
||||
function formatSchedule(sched) {
|
||||
if (!sched) return null;
|
||||
const parts = [];
|
||||
if (sched.days && sched.days.length > 0) parts.push(sched.days.map(d => _DAY_SHORT[d]).join(" "));
|
||||
if (sched.time_start && sched.time_end) parts.push(`${sched.time_start}–${sched.time_end}`);
|
||||
return parts.length ? parts.join(" · ") : null;
|
||||
}
|
||||
async function loadKeypasses() {
|
||||
const rows = await api("GET", "/api/admin/keypasses");
|
||||
const tbody = document.getElementById("keypasses-body");
|
||||
tbody.innerHTML = "";
|
||||
if (!rows.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="color:var(--text-muted);text-align:center;padding:2rem">No keypasses yet</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="7" style="color:var(--text-muted);text-align:center;padding:2rem">No keypasses yet</td></tr>';
|
||||
return;
|
||||
}
|
||||
for (const kp of rows) {
|
||||
@@ -156,18 +164,24 @@ async function loadKeypasses() {
|
||||
? `<span style="white-space:nowrap">${fmtDate(kp.expires_at)}</span>`
|
||||
: '<span style="color:var(--text-muted)">Never</span>';
|
||||
|
||||
const schedText = formatSchedule(kp.schedule);
|
||||
const schedCell = schedText
|
||||
? `<span style="font-size:.85em;white-space:nowrap">${esc(schedText)}</span>`
|
||||
: '<span style="color:var(--text-muted)">Always</span>';
|
||||
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td><code style="font-size:.95em;letter-spacing:.06em">${esc(kp.code)}</code></td>
|
||||
<td>${esc(kp.description)}</td>
|
||||
<td>${gatesCell}</td>
|
||||
<td>${expiresCell}</td>
|
||||
<td>${schedCell}</td>
|
||||
<td>${badge}</td>
|
||||
<td><div style="display:flex;gap:.5rem;justify-content:flex-end;white-space:nowrap">
|
||||
${!kp.revoked && expMs >= now ? `<button class="btn btn-ghost" style="font-size:.8rem;padding:.35rem .9rem"
|
||||
data-qr-kp-id="${kp.id}" data-qr-kp-desc="${esc(kp.description)}">QR</button>` : ""}
|
||||
${!kp.revoked ? `<button class="btn btn-ghost" style="font-size:.8rem;padding:.35rem .9rem"
|
||||
data-edit-kp='${JSON.stringify({id:kp.id, description:kp.description, expires_at:kp.expires_at, allowed_gate_ids:kp.allowed_gate_ids})}'>Edit</button>` : ""}
|
||||
data-edit-kp='${JSON.stringify({id:kp.id, description:kp.description, expires_at:kp.expires_at, allowed_gate_ids:kp.allowed_gate_ids, schedule:kp.schedule})}'>Edit</button>` : ""}
|
||||
${!kp.revoked && expMs >= now ? `<button class="btn btn-danger" style="font-size:.8rem;padding:.35rem .9rem"
|
||||
data-kp-id="${kp.id}">Revoke</button>` : ""}
|
||||
</div></td>`;
|
||||
@@ -210,6 +224,17 @@ async function loadKeypasses() {
|
||||
allGatesCb.checked = !allowedIds;
|
||||
checksContainer.style.display = allowedIds ? "flex" : "none";
|
||||
document.getElementById("kp-edit-error").classList.add("hidden");
|
||||
// Schedule
|
||||
const sched = kp.schedule;
|
||||
const noSchedCb = document.getElementById("kp-edit-no-schedule");
|
||||
const schedWrap = document.getElementById("kp-edit-schedule-wrap");
|
||||
noSchedCb.checked = !sched;
|
||||
schedWrap.style.display = sched ? "flex" : "none";
|
||||
document.querySelectorAll('input[name="kp-edit-day"]').forEach(cb => {
|
||||
cb.checked = sched && sched.days ? sched.days.includes(parseInt(cb.value)) : false;
|
||||
});
|
||||
document.getElementById("kp-edit-time-start").value = (sched && sched.time_start) || "";
|
||||
document.getElementById("kp-edit-time-end").value = (sched && sched.time_end) || "";
|
||||
document.getElementById("kp-edit-modal").classList.remove("hidden");
|
||||
});
|
||||
});
|
||||
@@ -287,6 +312,12 @@ document.getElementById("btn-new-keypass").addEventListener("click", () => {
|
||||
// Reset All gates checkbox
|
||||
document.getElementById("kp-all-gates").checked = true;
|
||||
checksContainer.style.display = "none";
|
||||
// Reset schedule
|
||||
document.getElementById("kp-no-schedule").checked = true;
|
||||
document.getElementById("kp-schedule-wrap").style.display = "none";
|
||||
document.querySelectorAll('input[name="kp-day"]').forEach(cb => cb.checked = false);
|
||||
document.getElementById("kp-time-start").value = "";
|
||||
document.getElementById("kp-time-end").value = "";
|
||||
document.getElementById("kp-error").classList.add("hidden");
|
||||
document.getElementById("keypass-modal").classList.remove("hidden");
|
||||
});
|
||||
@@ -310,6 +341,11 @@ document.getElementById("kp-never-expires").addEventListener("change", e => {
|
||||
kpExpInput.style.opacity = e.target.checked ? ".4" : "";
|
||||
});
|
||||
|
||||
// Schedule toggle
|
||||
document.getElementById("kp-no-schedule").addEventListener("change", e => {
|
||||
document.getElementById("kp-schedule-wrap").style.display = e.target.checked ? "none" : "flex";
|
||||
});
|
||||
|
||||
// All gates toggle
|
||||
document.getElementById("kp-all-gates").addEventListener("change", e => {
|
||||
const checksContainer = document.getElementById("kp-gate-checks");
|
||||
@@ -337,6 +373,9 @@ document.getElementById("kp-edit-never").addEventListener("change", e => {
|
||||
expInput.disabled = e.target.checked;
|
||||
expInput.style.opacity = e.target.checked ? ".4" : "";
|
||||
});
|
||||
document.getElementById("kp-edit-no-schedule").addEventListener("change", e => {
|
||||
document.getElementById("kp-edit-schedule-wrap").style.display = e.target.checked ? "none" : "flex";
|
||||
});
|
||||
document.getElementById("kp-edit-all-gates").addEventListener("change", e => {
|
||||
const checksContainer = document.getElementById("kp-edit-gate-checks");
|
||||
if (e.target.checked) {
|
||||
@@ -368,10 +407,22 @@ document.getElementById("kp-edit-form").addEventListener("submit", async e => {
|
||||
const expires_at = never ? null : new Date(document.getElementById("kp-edit-expires").value).toISOString();
|
||||
const allGates = document.getElementById("kp-edit-all-gates").checked;
|
||||
const gate_ids = allGates ? [] : Array.from(document.querySelectorAll('input[name="kp-edit-gate"]:checked')).map(cb => parseInt(cb.value));
|
||||
const noEditSched = document.getElementById("kp-edit-no-schedule").checked;
|
||||
let schedule = null;
|
||||
if (!noEditSched) {
|
||||
const days = Array.from(document.querySelectorAll('input[name="kp-edit-day"]:checked')).map(cb => parseInt(cb.value));
|
||||
const time_start = document.getElementById("kp-edit-time-start").value || undefined;
|
||||
const time_end = document.getElementById("kp-edit-time-end").value || undefined;
|
||||
schedule = {};
|
||||
if (days.length) schedule.days = days;
|
||||
if (time_start) schedule.time_start = time_start;
|
||||
if (time_end) schedule.time_end = time_end;
|
||||
if (!Object.keys(schedule).length) schedule = null;
|
||||
}
|
||||
const errEl = document.getElementById("kp-edit-error");
|
||||
errEl.classList.add("hidden");
|
||||
try {
|
||||
await api("PATCH", `/api/admin/keypasses/${id}`, { description, expires_at, gate_ids });
|
||||
await api("PATCH", `/api/admin/keypasses/${id}`, { description, expires_at, gate_ids, schedule });
|
||||
document.getElementById("kp-edit-modal").classList.add("hidden");
|
||||
showToast("Keypass updated");
|
||||
loadKeypasses();
|
||||
@@ -390,10 +441,22 @@ document.getElementById("keypass-form").addEventListener("submit", async e => {
|
||||
const expires_at = neverExpires ? null : new Date(document.getElementById("kp-expires").value).toISOString();
|
||||
const allGates = document.getElementById("kp-all-gates").checked;
|
||||
const gate_ids = allGates ? [] : Array.from(document.querySelectorAll('input[name="kp-gate"]:checked')).map(cb => parseInt(cb.value));
|
||||
const noSched = document.getElementById("kp-no-schedule").checked;
|
||||
let schedule = null;
|
||||
if (!noSched) {
|
||||
const days = Array.from(document.querySelectorAll('input[name="kp-day"]:checked')).map(cb => parseInt(cb.value));
|
||||
const time_start = document.getElementById("kp-time-start").value || undefined;
|
||||
const time_end = document.getElementById("kp-time-end").value || undefined;
|
||||
schedule = {};
|
||||
if (days.length) schedule.days = days;
|
||||
if (time_start) schedule.time_start = time_start;
|
||||
if (time_end) schedule.time_end = time_end;
|
||||
if (!Object.keys(schedule).length) schedule = null;
|
||||
}
|
||||
const errEl = document.getElementById("kp-error");
|
||||
errEl.classList.add("hidden");
|
||||
try {
|
||||
await api("POST", "/api/admin/keypasses", { description: desc, expires_at, gate_ids, code, charset, length });
|
||||
await api("POST", "/api/admin/keypasses", { description: desc, expires_at, gate_ids, code, charset, length, schedule });
|
||||
document.getElementById("keypass-modal").classList.add("hidden");
|
||||
showToast("Keypass created");
|
||||
loadKeypasses();
|
||||
|
||||
Reference in New Issue
Block a user