Auto-generate and email passwords - admin never types/sees one
The admin password field is gone from both the create-user and
edit-user forms. Three flows now generate a password and mail it
instead of letting an admin type one:
- create_user(): password generated, welcome email sent
(login/password/portal link) right after the row commits
- new POST /api/admin/users/{id}/reset-password: generates a new
password, saves it, emails it - the only way a password changes now
- assign_rdp_slot(): sending a user their first grant on a pilot
(not every later tweak of an existing grant's expiry - guarded by
an is_new_grant check) sends a short "you have pilot access" email,
no credentials since the account already exists
If an email fails to send, the plaintext password comes back in the
API response as a one-time fallback so the admin isn't locked out of
handing it over by hand - the frontend only shows it then, never on
a successful send.
Added two small helpers (_build_light_email, _credentials_table) so
these three new templates share the light-theme card shell instead
of re-typing it; the existing access-approval emails keep their own
inline copies untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+166
-5
@@ -165,6 +165,133 @@ def _send_email(to: str, subject: str, html_body: str) -> None:
|
|||||||
srv.login(SMTP_USERNAME, SMTP_PASSWORD)
|
srv.login(SMTP_USERNAME, SMTP_PASSWORD)
|
||||||
srv.sendmail(SMTP_FROM_EMAIL, to, msg.as_string())
|
srv.sendmail(SMTP_FROM_EMAIL, to, msg.as_string())
|
||||||
|
|
||||||
|
|
||||||
|
def _build_light_email(title: str, subhead: str, body_html: str, cta_url: str | None = None, cta_label: str | None = None) -> str:
|
||||||
|
"""Shared light-theme card shell (logo, heading, body, optional button,
|
||||||
|
footer) for one-off transactional emails that don't need the full
|
||||||
|
credentials-table layout the access-approval templates have inline.
|
||||||
|
Those keep their own copies (changing one must not risk the others);
|
||||||
|
this is for newer, simpler notifications - welcome, password reset,
|
||||||
|
pilot-granted - so they don't each re-type the whole shell."""
|
||||||
|
cta_html = ""
|
||||||
|
if cta_url:
|
||||||
|
cta_html = (
|
||||||
|
f'<div style="margin:28px 0">'
|
||||||
|
f'<a href="{cta_url}" style="display:inline-block;padding:12px 28px;background:#1a5db5;color:#fff;'
|
||||||
|
f'text-decoration:none;border-radius:8px;font-size:15px;font-weight:600">{cta_label or "Войти в полигон"}</a>'
|
||||||
|
f'</div>'
|
||||||
|
)
|
||||||
|
return f"""<!DOCTYPE html>
|
||||||
|
<html lang="ru"><head><meta charset="utf-8"/></head>
|
||||||
|
<body style="margin:0;padding:0;background:#eef1f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0"><tr><td align="center" style="padding:40px 20px">
|
||||||
|
<table width="560" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:16px;overflow:hidden;border:1px solid #e3e8ee">
|
||||||
|
<tr><td style="padding:32px 36px 0">
|
||||||
|
<img src="cid:mont_logo" alt="MONT" width="160" height="46" style="display:block;margin-bottom:24px;border:0"/>
|
||||||
|
<h1 style="margin:0 0 8px;font-size:22px;color:#14233a">{title}</h1>
|
||||||
|
<p style="margin:0 0 24px;color:#7c8ba0;font-size:14px">{subhead}</p>
|
||||||
|
<hr style="border:none;border-top:1px solid #e7ebf0;margin:0 0 24px"/>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:0 36px">
|
||||||
|
{body_html}
|
||||||
|
{cta_html}
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:20px 36px 28px;color:#8b98ab;font-size:12px;border-top:1px solid #e7ebf0;line-height:1.6">
|
||||||
|
Если у вас возникли вопросы, свяжитесь с вашим менеджером MONT или напишите на <a href="mailto:RGalyaviev@mont.ru" style="color:#1a5db5">RGalyaviev@mont.ru</a>
|
||||||
|
</td></tr>
|
||||||
|
</table></td></tr></table>
|
||||||
|
</body></html>"""
|
||||||
|
|
||||||
|
|
||||||
|
def _credentials_table(rows: list[tuple[str, str, bool]]) -> str:
|
||||||
|
"""rows: (label, value, monospace) - renders the same light-theme
|
||||||
|
label/value table used across access emails."""
|
||||||
|
trs = []
|
||||||
|
for i, (label, value, mono) in enumerate(rows):
|
||||||
|
border = "border-top:1px solid #e7ebf0;" if i > 0 else ""
|
||||||
|
font = "font-family:monospace;" if mono else ""
|
||||||
|
trs.append(
|
||||||
|
f'<tr><td style="color:#7c8ba0;font-size:13px;width:40%;{border}">{label}</td>'
|
||||||
|
f'<td style="{border}color:#14233a;font-size:14px;{font}">{value}</td></tr>'
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
'<table width="100%" cellpadding="12" cellspacing="0" '
|
||||||
|
'style="background:#f7f9fb;border-radius:10px;border:1px solid #e7ebf0">'
|
||||||
|
+ "".join(trs) + "</table>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _send_welcome_email(user, password: str) -> str:
|
||||||
|
"""Sent once, when an admin manually creates a user on the Users tab -
|
||||||
|
the only way that person learns their login/password, since the admin
|
||||||
|
never sees the plaintext (create_user() generates it)."""
|
||||||
|
body = (
|
||||||
|
f'<p style="color:#45566e;font-size:15px;line-height:1.6;margin:0 0 20px">'
|
||||||
|
f'Здравствуйте{", <b>" + escape(user.first_name) + "</b>" if user.first_name else ""}!<br>'
|
||||||
|
f'Для вас создан аккаунт на Инфраструктурном полигоне MONT.</p>'
|
||||||
|
+ _credentials_table([
|
||||||
|
("Адрес портала", f'<a href="{PORTAL_URL}" style="color:#1a5db5">{PORTAL_URL}</a>', False),
|
||||||
|
("Логин", escape(user.username), True),
|
||||||
|
("Пароль", escape(password), True),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
html = _build_light_email("Доступ к Инфраструктурному полигону MONT", "Для вас создан аккаунт", body, PORTAL_URL)
|
||||||
|
try:
|
||||||
|
_send_email(user.username, "Доступ к Инфраструктурному полигону MONT", html)
|
||||||
|
return "Email отправлен"
|
||||||
|
except Exception as ex:
|
||||||
|
log_event("email_send_error", error=str(ex), channel="welcome")
|
||||||
|
return f"Ошибка email: {ex}"
|
||||||
|
|
||||||
|
|
||||||
|
def _send_password_reset_email(user, password: str) -> str:
|
||||||
|
"""Sent when an admin resets a user's password from the Users tab -
|
||||||
|
same reasoning as the welcome email: the plaintext only ever exists
|
||||||
|
in this message, never in the admin UI."""
|
||||||
|
body = (
|
||||||
|
f'<p style="color:#45566e;font-size:15px;line-height:1.6;margin:0 0 20px">'
|
||||||
|
f'Здравствуйте{", <b>" + escape(user.first_name) + "</b>" if user.first_name else ""}!<br>'
|
||||||
|
f'Пароль от вашего аккаунта на полигоне MONT был сброшен администратором.</p>'
|
||||||
|
+ _credentials_table([
|
||||||
|
("Адрес портала", f'<a href="{PORTAL_URL}" style="color:#1a5db5">{PORTAL_URL}</a>', False),
|
||||||
|
("Логин", escape(user.username), True),
|
||||||
|
("Новый пароль", escape(password), True),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
html = _build_light_email("Пароль обновлён", "Инфраструктурный полигон MONT", body, PORTAL_URL)
|
||||||
|
try:
|
||||||
|
_send_email(user.username, "Пароль обновлён — полигон MONT", html)
|
||||||
|
return "Email отправлен"
|
||||||
|
except Exception as ex:
|
||||||
|
log_event("email_send_error", error=str(ex), channel="password_reset")
|
||||||
|
return f"Ошибка email: {ex}"
|
||||||
|
|
||||||
|
|
||||||
|
def _send_pilot_granted_email(user, service, expires_at) -> str:
|
||||||
|
"""Sent once, the moment a pilot's RDP slot is first assigned to this
|
||||||
|
user (not on every later tweak of that same assignment - see the
|
||||||
|
"new grant" check in assign_rdp_slot()). No credentials here: this
|
||||||
|
user already has an account and password, this just tells them a new
|
||||||
|
product showed up."""
|
||||||
|
expiry_row = (
|
||||||
|
f'<p style="color:#7c8ba0;font-size:13px;margin:0 0 20px">Доступ действует до <b style="color:#14233a">{expires_at.strftime("%d.%m.%Y")}</b></p>'
|
||||||
|
if expires_at else ""
|
||||||
|
)
|
||||||
|
body = (
|
||||||
|
f'<p style="color:#45566e;font-size:15px;line-height:1.6;margin:0 0 16px">'
|
||||||
|
f'Здравствуйте{", <b>" + escape(user.first_name) + "</b>" if user.first_name else ""}!<br>'
|
||||||
|
f'Вам открыт доступ к пилотному проекту <b>{escape(service.name)}</b> на Инфраструктурном полигоне MONT.</p>'
|
||||||
|
+ expiry_row
|
||||||
|
)
|
||||||
|
html = _build_light_email(f"Доступ к пилоту: {escape(service.name)}", "Новый продукт на полигоне", body, PORTAL_URL, "Открыть полигон")
|
||||||
|
try:
|
||||||
|
_send_email(user.username, f"Доступ к пилоту «{service.name}» — полигон MONT", html)
|
||||||
|
return "Email отправлен"
|
||||||
|
except Exception as ex:
|
||||||
|
log_event("email_send_error", error=str(ex), channel="pilot_granted")
|
||||||
|
return f"Ошибка email: {ex}"
|
||||||
|
|
||||||
|
|
||||||
def _tg_api(method: str, payload: dict) -> dict:
|
def _tg_api(method: str, payload: dict) -> dict:
|
||||||
import urllib.request as _ur
|
import urllib.request as _ur
|
||||||
import json as _j
|
import json as _j
|
||||||
@@ -2920,6 +3047,7 @@ def assign_rdp_slot(slot_id: int, payload: dict, request: Request, _: User = Dep
|
|||||||
UserServiceAccess.service_id == service.id,
|
UserServiceAccess.service_id == service.id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
is_new_grant = access is None
|
||||||
if access:
|
if access:
|
||||||
access.expires_at = expires_at
|
access.expires_at = expires_at
|
||||||
else:
|
else:
|
||||||
@@ -2928,7 +3056,13 @@ def assign_rdp_slot(slot_id: int, payload: dict, request: Request, _: User = Dep
|
|||||||
slot.assigned_user_id = target_user.id
|
slot.assigned_user_id = target_user.id
|
||||||
db.commit()
|
db.commit()
|
||||||
audit(db, "RDP_SLOT_ASSIGN", f"service={service.slug} slot={slot.id} user={target_user.username}", user_id=None)
|
audit(db, "RDP_SLOT_ASSIGN", f"service={service.slug} slot={slot.id} user={target_user.username}", user_id=None)
|
||||||
return {"ok": True, "assigned_user_id": target_user.id}
|
email_status = None
|
||||||
|
if is_new_grant:
|
||||||
|
# Only on a genuinely new grant - not every time the admin tweaks
|
||||||
|
# the expiry date for someone who already has this pilot, which
|
||||||
|
# also calls this endpoint.
|
||||||
|
email_status = _send_pilot_granted_email(target_user, service, expires_at)
|
||||||
|
return {"ok": True, "assigned_user_id": target_user.id, "email_status": email_status}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/admin/categories")
|
@app.post("/api/admin/categories")
|
||||||
@@ -2972,11 +3106,17 @@ def update_web_pool_size(payload: dict, request: Request, _: User = Depends(requ
|
|||||||
|
|
||||||
@app.post("/api/admin/users")
|
@app.post("/api/admin/users")
|
||||||
def create_user(payload: dict, request: Request, _: User = Depends(require_admin), db: Session = Depends(get_db)):
|
def create_user(payload: dict, request: Request, _: User = Depends(require_admin), db: Session = Depends(get_db)):
|
||||||
|
"""The password is never typed by the admin - it's generated here and
|
||||||
|
mailed to the user (username is their email, same convention as the
|
||||||
|
self-service approval flow). If the email fails to send, the plaintext
|
||||||
|
comes back in the response as a one-time fallback so the admin isn't
|
||||||
|
locked out of handing it over; the frontend only surfaces it then."""
|
||||||
validate_csrf(request)
|
validate_csrf(request)
|
||||||
expires_at = dt.datetime.fromisoformat(payload["expires_at"])
|
expires_at = dt.datetime.fromisoformat(payload["expires_at"])
|
||||||
|
password = _generate_password()
|
||||||
user = User(
|
user = User(
|
||||||
username=payload["username"],
|
username=payload["username"],
|
||||||
password_hash=hash_password(payload["password"]),
|
password_hash=hash_password(password),
|
||||||
expires_at=expires_at,
|
expires_at=expires_at,
|
||||||
active=payload.get("active", True),
|
active=payload.get("active", True),
|
||||||
is_admin=payload.get("is_admin", False),
|
is_admin=payload.get("is_admin", False),
|
||||||
@@ -2985,7 +3125,11 @@ def create_user(payload: dict, request: Request, _: User = Depends(require_admin
|
|||||||
)
|
)
|
||||||
db.add(user)
|
db.add(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"id": user.id}
|
email_status = _send_welcome_email(user, password)
|
||||||
|
result = {"id": user.id, "email_status": email_status}
|
||||||
|
if not email_status.startswith("Email отправлен"):
|
||||||
|
result["password"] = password
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@app.put("/api/admin/users/{user_id}")
|
@app.put("/api/admin/users/{user_id}")
|
||||||
@@ -2997,14 +3141,31 @@ def edit_user(user_id: int, payload: dict, request: Request, _: User = Depends(r
|
|||||||
for key in ["username", "active", "is_admin", "first_name", "last_name"]:
|
for key in ["username", "active", "is_admin", "first_name", "last_name"]:
|
||||||
if key in payload:
|
if key in payload:
|
||||||
setattr(user, key, payload[key])
|
setattr(user, key, payload[key])
|
||||||
if "password" in payload and payload["password"]:
|
|
||||||
user.password_hash = hash_password(payload["password"])
|
|
||||||
if "expires_at" in payload:
|
if "expires_at" in payload:
|
||||||
user.expires_at = dt.datetime.fromisoformat(payload["expires_at"])
|
user.expires_at = dt.datetime.fromisoformat(payload["expires_at"])
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/admin/users/{user_id}/reset-password")
|
||||||
|
def reset_user_password(user_id: int, request: Request, _: User = Depends(require_admin), db: Session = Depends(get_db)):
|
||||||
|
"""Generates a new password and emails it - the only way a user's
|
||||||
|
password changes now, mirroring create_user(). Same fallback: if the
|
||||||
|
email bounces, the plaintext comes back so the admin can pass it on."""
|
||||||
|
validate_csrf(request)
|
||||||
|
user = db.get(User, user_id)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
password = _generate_password()
|
||||||
|
user.password_hash = hash_password(password)
|
||||||
|
db.commit()
|
||||||
|
email_status = _send_password_reset_email(user, password)
|
||||||
|
result = {"ok": True, "email_status": email_status}
|
||||||
|
if not email_status.startswith("Email отправлен"):
|
||||||
|
result["password"] = password
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/api/admin/users/{user_id}")
|
@app.delete("/api/admin/users/{user_id}")
|
||||||
def delete_user(user_id: int, request: Request, admin: User = Depends(require_admin), db: Session = Depends(get_db)):
|
def delete_user(user_id: int, request: Request, admin: User = Depends(require_admin), db: Session = Depends(get_db)):
|
||||||
validate_csrf(request)
|
validate_csrf(request)
|
||||||
|
|||||||
+26
-10
@@ -258,12 +258,12 @@
|
|||||||
<input id="u_first_name" placeholder="Имя" />
|
<input id="u_first_name" placeholder="Имя" />
|
||||||
<input id="u_last_name" placeholder="Фамилия" />
|
<input id="u_last_name" placeholder="Фамилия" />
|
||||||
<input id="u_exp" type="date" required />
|
<input id="u_exp" type="date" required />
|
||||||
<input id="u_pwd" placeholder="new password (optional)" type="password" />
|
|
||||||
<select id="u_active"><option value="true">active</option><option value="false">inactive</option></select>
|
<select id="u_active"><option value="true">active</option><option value="false">inactive</option></select>
|
||||||
<select id="u_admin"><option value="false">user</option><option value="true">admin</option></select>
|
<select id="u_admin"><option value="false">user</option><option value="true">admin</option></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button onclick="saveUser()">Save</button>
|
<button onclick="saveUser()">Save</button>
|
||||||
|
<button id="user_reset_pwd_btn" onclick="resetUserPassword()" style="display:none;">🔑 Сбросить пароль</button>
|
||||||
<button onclick="deleteUser()">Delete</button>
|
<button onclick="deleteUser()">Delete</button>
|
||||||
<button onclick="clearUserForm()">Clear</button>
|
<button onclick="clearUserForm()">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -285,11 +285,11 @@
|
|||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<div class="list-title">Добавить пользователя</div>
|
<div class="list-title">Добавить пользователя</div>
|
||||||
|
<div class="field-help">Пароль не задаётся вручную — он сгенерируется и уйдёт пользователю на почту (username) сразу после создания.</div>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<input id="new_u_name" placeholder="username" />
|
<input id="new_u_name" placeholder="username (email)" />
|
||||||
<input id="new_u_first_name" placeholder="Имя" />
|
<input id="new_u_first_name" placeholder="Имя" />
|
||||||
<input id="new_u_last_name" placeholder="Фамилия" />
|
<input id="new_u_last_name" placeholder="Фамилия" />
|
||||||
<input id="new_u_pwd" placeholder="password" type="password" />
|
|
||||||
<input id="new_u_exp" type="date" required />
|
<input id="new_u_exp" type="date" required />
|
||||||
<select id="new_u_active"><option value="true">active</option><option value="false">inactive</option></select>
|
<select id="new_u_active"><option value="true">active</option><option value="false">inactive</option></select>
|
||||||
<select id="new_u_admin"><option value="false">user</option><option value="true">admin</option></select>
|
<select id="new_u_admin"><option value="false">user</option><option value="true">admin</option></select>
|
||||||
@@ -960,9 +960,9 @@
|
|||||||
document.getElementById('u_first_name').value = firstName || '';
|
document.getElementById('u_first_name').value = firstName || '';
|
||||||
document.getElementById('u_last_name').value = lastName || '';
|
document.getElementById('u_last_name').value = lastName || '';
|
||||||
document.getElementById('u_exp').value = dateFromIso(expiresIso);
|
document.getElementById('u_exp').value = dateFromIso(expiresIso);
|
||||||
document.getElementById('u_pwd').value = '';
|
|
||||||
document.getElementById('u_active').value = String(active);
|
document.getElementById('u_active').value = String(active);
|
||||||
document.getElementById('u_admin').value = String(isAdmin);
|
document.getElementById('u_admin').value = String(isAdmin);
|
||||||
|
document.getElementById('user_reset_pwd_btn').style.display = '';
|
||||||
markSelected('.user-item', 'data-user-id', id);
|
markSelected('.user-item', 'data-user-id', id);
|
||||||
syncAclForSelectedUser();
|
syncAclForSelectedUser();
|
||||||
}
|
}
|
||||||
@@ -970,15 +970,17 @@
|
|||||||
async function createUser() {
|
async function createUser() {
|
||||||
const expDate = document.getElementById('new_u_exp').value;
|
const expDate = document.getElementById('new_u_exp').value;
|
||||||
if (!expDate) return alert('Выберите дату деактивации');
|
if (!expDate) return alert('Выберите дату деактивации');
|
||||||
await api('/api/admin/users', 'POST', {
|
const result = await api('/api/admin/users', 'POST', {
|
||||||
username: document.getElementById('new_u_name').value,
|
username: document.getElementById('new_u_name').value,
|
||||||
first_name: document.getElementById('new_u_first_name').value,
|
first_name: document.getElementById('new_u_first_name').value,
|
||||||
last_name: document.getElementById('new_u_last_name').value,
|
last_name: document.getElementById('new_u_last_name').value,
|
||||||
password: document.getElementById('new_u_pwd').value,
|
|
||||||
expires_at: expiryToApi(expDate),
|
expires_at: expiryToApi(expDate),
|
||||||
active: document.getElementById('new_u_active').value === 'true',
|
active: document.getElementById('new_u_active').value === 'true',
|
||||||
is_admin: document.getElementById('new_u_admin').value === 'true',
|
is_admin: document.getElementById('new_u_admin').value === 'true',
|
||||||
});
|
});
|
||||||
|
if (result.password) {
|
||||||
|
alert(`Письмо не отправлено (${result.email_status}). Пароль: ${result.password}\nПередайте его пользователю вручную.`);
|
||||||
|
}
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -995,12 +997,22 @@
|
|||||||
active: document.getElementById('u_active').value === 'true',
|
active: document.getElementById('u_active').value === 'true',
|
||||||
is_admin: document.getElementById('u_admin').value === 'true',
|
is_admin: document.getElementById('u_admin').value === 'true',
|
||||||
};
|
};
|
||||||
const pwd = document.getElementById('u_pwd').value.trim();
|
|
||||||
if (pwd) payload.password = pwd;
|
|
||||||
await api(`/api/admin/users/${id}`, 'PUT', payload);
|
await api(`/api/admin/users/${id}`, 'PUT', payload);
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resetUserPassword() {
|
||||||
|
const id = document.getElementById('u_id').value;
|
||||||
|
if (!id) return alert('Выберите пользователя');
|
||||||
|
if (!confirm('Сгенерировать новый пароль и отправить его пользователю на почту?')) return;
|
||||||
|
const result = await api(`/api/admin/users/${id}/reset-password`, 'POST', {});
|
||||||
|
if (result.password) {
|
||||||
|
alert(`Письмо не отправлено (${result.email_status}). Новый пароль: ${result.password}\nПередайте его пользователю вручную.`);
|
||||||
|
} else {
|
||||||
|
alert('Новый пароль отправлен пользователю на почту.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteUser() {
|
async function deleteUser() {
|
||||||
const id = document.getElementById('u_id').value;
|
const id = document.getElementById('u_id').value;
|
||||||
if (!id) return alert('Выберите пользователя');
|
if (!id) return alert('Выберите пользователя');
|
||||||
@@ -1010,9 +1022,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearUserForm() {
|
function clearUserForm() {
|
||||||
['u_id','u_name','u_exp','u_pwd'].forEach(id => document.getElementById(id).value = '');
|
['u_id','u_name','u_exp'].forEach(id => document.getElementById(id).value = '');
|
||||||
document.getElementById('u_active').value = 'true';
|
document.getElementById('u_active').value = 'true';
|
||||||
document.getElementById('u_admin').value = 'false';
|
document.getElementById('u_admin').value = 'false';
|
||||||
|
document.getElementById('user_reset_pwd_btn').style.display = 'none';
|
||||||
document.querySelectorAll('.acl_service').forEach(x => x.checked = false);
|
document.querySelectorAll('.acl_service').forEach(x => x.checked = false);
|
||||||
document.querySelectorAll('.user-item').forEach((el) => el.classList.remove('selected-item'));
|
document.querySelectorAll('.user-item').forEach((el) => el.classList.remove('selected-item'));
|
||||||
}
|
}
|
||||||
@@ -1283,7 +1296,10 @@
|
|||||||
const userId = document.getElementById(`pilot_slot_user_${slotId}`).value;
|
const userId = document.getElementById(`pilot_slot_user_${slotId}`).value;
|
||||||
const expInput = document.getElementById(`pilot_slot_exp_${slotId}`);
|
const expInput = document.getElementById(`pilot_slot_exp_${slotId}`);
|
||||||
const expires_at = (userId && expInput.value) ? `${expInput.value}T23:59:59+00:00` : null;
|
const expires_at = (userId && expInput.value) ? `${expInput.value}T23:59:59+00:00` : null;
|
||||||
await api(`/api/admin/rdp-slots/${slotId}/assign`, 'PUT', {user_id: userId || null, expires_at});
|
const result = await api(`/api/admin/rdp-slots/${slotId}/assign`, 'PUT', {user_id: userId || null, expires_at});
|
||||||
|
if (result.email_status && !result.email_status.startsWith('Email отправлен')) {
|
||||||
|
alert(`Слот назначен, но письмо пользователю не ушло: ${result.email_status}`);
|
||||||
|
}
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user