feat: add first_name/last_name to users, avatar in header, neutral dashboard bg

This commit is contained in:
2026-05-12 12:51:47 +00:00
parent dedf4aea77
commit 6aa40eb5c2
5 changed files with 44 additions and 5 deletions
+3 -1
View File
@@ -1337,6 +1337,8 @@ def create_user(payload: dict, request: Request, _: User = Depends(require_admin
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),
first_name=payload.get("first_name", ""),
last_name=payload.get("last_name", ""),
) )
db.add(user) db.add(user)
db.commit() db.commit()
@@ -1349,7 +1351,7 @@ def edit_user(user_id: int, payload: dict, request: Request, _: User = Depends(r
user = db.get(User, user_id) user = db.get(User, user_id)
if not user: if not user:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
for key in ["username", "active", "is_admin"]: 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"]: if "password" in payload and payload["password"]:
+10
View File
@@ -32,6 +32,8 @@ class User(Base):
expires_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), index=True) expires_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), index=True)
active: Mapped[bool] = mapped_column(Boolean, default=True, index=True) active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
is_admin: Mapped[bool] = mapped_column(Boolean, default=False) is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
first_name: Mapped[str] = mapped_column(String(64), default="")
last_name: Mapped[str] = mapped_column(String(64), default="")
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc)) created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc))
@@ -50,6 +52,8 @@ class Service(Base):
icon_path: Mapped[str] = mapped_column(Text, default="") icon_path: Mapped[str] = mapped_column(Text, default="")
active: Mapped[bool] = mapped_column(Boolean, default=True) active: Mapped[bool] = mapped_column(Boolean, default=True)
warm_pool_size: Mapped[int] = mapped_column(Integer, default=0) warm_pool_size: Mapped[int] = mapped_column(Integer, default=0)
first_name: Mapped[str] = mapped_column(String(64), default="")
last_name: Mapped[str] = mapped_column(String(64), default="")
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc)) created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc))
@@ -59,6 +63,8 @@ class Category(Base):
id: Mapped[int] = mapped_column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(128), unique=True, index=True) name: Mapped[str] = mapped_column(String(128), unique=True, index=True)
slug: Mapped[str] = mapped_column(String(64), unique=True, index=True) slug: Mapped[str] = mapped_column(String(64), unique=True, index=True)
first_name: Mapped[str] = mapped_column(String(64), default="")
last_name: Mapped[str] = mapped_column(String(64), default="")
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc)) created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc))
@@ -69,6 +75,8 @@ class ServiceCategory(Base):
id: Mapped[int] = mapped_column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
service_id: Mapped[int] = mapped_column(ForeignKey("services.id", ondelete="CASCADE"), index=True) service_id: Mapped[int] = mapped_column(ForeignKey("services.id", ondelete="CASCADE"), index=True)
category_id: Mapped[int] = mapped_column(ForeignKey("categories.id", ondelete="CASCADE"), index=True) category_id: Mapped[int] = mapped_column(ForeignKey("categories.id", ondelete="CASCADE"), index=True)
first_name: Mapped[str] = mapped_column(String(64), default="")
last_name: Mapped[str] = mapped_column(String(64), default="")
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc)) created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc))
@@ -90,6 +98,8 @@ class RdpSlot(Base):
rdp_username: Mapped[str] = mapped_column(String(128)) rdp_username: Mapped[str] = mapped_column(String(128))
rdp_password: Mapped[str] = mapped_column(String(256), default="") rdp_password: Mapped[str] = mapped_column(String(256), default="")
container_name: Mapped[Optional[str]] = mapped_column(String(128), nullable=True) container_name: Mapped[Optional[str]] = mapped_column(String(128), nullable=True)
first_name: Mapped[str] = mapped_column(String(64), default="")
last_name: Mapped[str] = mapped_column(String(64), default="")
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc)) created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc))
+16
View File
@@ -107,6 +107,22 @@ button {
} }
.header-left { display: flex; align-items: center; } .header-left { display: flex; align-items: center; }
.header-right { display: flex; align-items: center; gap: 0.75rem; } .header-right { display: flex; align-items: center; gap: 0.75rem; }
.user-avatar {
width: 34px;
height: 34px;
border-radius: 50%;
background: linear-gradient(135deg, #1e7dc8 0%, #1360a0 100%);
color: #fff;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.02em;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(14,80,160,0.4);
}
.header-username { .header-username {
color: #ffffff; color: #ffffff;
font-size: 1rem; font-size: 1rem;
+13 -3
View File
@@ -53,8 +53,8 @@
<input class="list-search" id="users_search" placeholder="Поиск пользователя..." oninput="filterList('users_search', '#users_list .user-item')" /> <input class="list-search" id="users_search" placeholder="Поиск пользователя..." oninput="filterList('users_search', '#users_list .user-item')" />
<div class="list-box" id="users_list"> <div class="list-box" id="users_list">
{% for u in users %} {% for u in users %}
<button class="list-item user-item" data-user-id="{{u.id}}" data-filter="{{u.username|lower}}" onclick='selectUser({{u.id}}, {{u.username|tojson}}, {{u.active|tojson}}, {{u.is_admin|tojson}}, {{u.expires_at.isoformat()|tojson}})'> <button class="list-item user-item" data-user-id="{{u.id}}" data-filter="{{u.username|lower}}" onclick='selectUser({{u.id}}, {{u.username|tojson}}, {{u.active|tojson}}, {{u.is_admin|tojson}}, {{u.expires_at.isoformat()|tojson}}, {{u.first_name|tojson}}, {{u.last_name|tojson}})'>
<div>{{u.username}}</div> <div>{{u.username}}{% if u.first_name or u.last_name %} <small style="opacity:.6">— {{ (u.first_name + ' ' + u.last_name)|trim }}</small>{% endif %}</div>
<small class="user-days" data-exp="{{u.expires_at.isoformat()}}"></small> <small class="user-days" data-exp="{{u.expires_at.isoformat()}}"></small>
</button> </button>
{% endfor %} {% endfor %}
@@ -66,6 +66,8 @@
<div class="form-grid"> <div class="form-grid">
<input id="u_id" type="hidden" /> <input id="u_id" type="hidden" />
<input id="u_name" placeholder="username" /> <input id="u_name" placeholder="username" />
<input id="u_first_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" /> <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>
@@ -91,6 +93,8 @@
<div class="list-title">Добавить пользователя</div> <div class="list-title">Добавить пользователя</div>
<div class="form-grid"> <div class="form-grid">
<input id="new_u_name" placeholder="username" /> <input id="new_u_name" placeholder="username" />
<input id="new_u_first_name" placeholder="Имя" />
<input id="new_u_last_name" placeholder="Фамилия" />
<input id="new_u_pwd" placeholder="password" type="password" /> <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>
@@ -668,9 +672,11 @@
return r.json(); return r.json();
} }
function selectUser(id, username, active, isAdmin, expiresIso) { function selectUser(id, username, active, isAdmin, expiresIso, firstName, lastName) {
document.getElementById('u_id').value = id; document.getElementById('u_id').value = id;
document.getElementById('u_name').value = username; document.getElementById('u_name').value = username;
document.getElementById('u_first_name').value = firstName || '';
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_pwd').value = '';
document.getElementById('u_active').value = String(active); document.getElementById('u_active').value = String(active);
@@ -684,6 +690,8 @@
if (!expDate) return alert('Выберите дату деактивации'); if (!expDate) return alert('Выберите дату деактивации');
await api('/api/admin/users', 'POST', { 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,
last_name: document.getElementById('new_u_last_name').value,
password: document.getElementById('new_u_pwd').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',
@@ -699,6 +707,8 @@
if (!expDate) return alert('Выберите дату деактивации'); if (!expDate) return alert('Выберите дату деактивации');
const payload = { const payload = {
username: document.getElementById('u_name').value, username: document.getElementById('u_name').value,
first_name: document.getElementById('u_first_name').value,
last_name: document.getElementById('u_last_name').value,
expires_at: expiryToApi(expDate), expires_at: expiryToApi(expDate),
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',
+2 -1
View File
@@ -44,7 +44,8 @@
<header class="header"> <header class="header">
<div class="header-left"> <div class="header-left">
<span class="header-username">{{ user.username }}</span> <div class="user-avatar">{{ ((user.first_name[0] if user.first_name else user.username[0]) + (user.last_name[0] if user.last_name else ''))|upper }}</div>
<span class="header-username">{{ (user.first_name + ' ' + user.last_name)|trim or user.username }}</span>
</div> </div>
<div class="header-right"> <div class="header-right">
{% if user.is_admin %} {% if user.is_admin %}