Добавление страницы профиля

This commit is contained in:
2025-12-20 09:59:16 +03:00
parent a3203d713d
commit 3473c31f73
6 changed files with 615 additions and 8 deletions
+2 -1
View File
@@ -3,7 +3,7 @@ from .author import AuthorBase, AuthorCreate, AuthorList, AuthorRead, AuthorUpda
from .genre import GenreBase, GenreCreate, GenreList, GenreRead, GenreUpdate from .genre import GenreBase, GenreCreate, GenreList, GenreRead, GenreUpdate
from .book import BookBase, BookCreate, BookList, BookRead, BookUpdate from .book import BookBase, BookCreate, BookList, BookRead, BookUpdate
from .role import RoleBase, RoleCreate, RoleList, RoleRead, RoleUpdate from .role import RoleBase, RoleCreate, RoleList, RoleRead, RoleUpdate
from .user import UserBase, UserCreate, UserLogin, UserRead, UserUpdate from .user import UserBase, UserCreate, UserList, UserRead, UserUpdate, UserLogin
from .token import Token, TokenData from .token import Token, TokenData
from .combined import (AuthorWithBooks, GenreWithBooks, BookWithAuthors, BookWithGenres, from .combined import (AuthorWithBooks, GenreWithBooks, BookWithAuthors, BookWithGenres,
BookWithAuthorsAndGenres, BookFilteredList) BookWithAuthorsAndGenres, BookFilteredList)
@@ -36,5 +36,6 @@ __all__ = [
"UserCreate", "UserCreate",
"UserRead", "UserRead",
"UserUpdate", "UserUpdate",
"UserList",
"UserLogin", "UserLogin",
] ]
+6
View File
@@ -59,3 +59,9 @@ class UserUpdate(SQLModel):
email: EmailStr | None = None email: EmailStr | None = None
full_name: str | None = None full_name: str | None = None
password: str | None = None password: str | None = None
class UserList(SQLModel):
"""Список пользователей"""
users: List[UserRead]
total: int
+23 -6
View File
@@ -7,7 +7,7 @@ from fastapi.security import OAuth2PasswordRequestForm
from sqlmodel import Session, select from sqlmodel import Session, select
from library_service.models.db import Role, User from library_service.models.db import Role, User
from library_service.models.dto import Token, UserCreate, UserRead, UserUpdate from library_service.models.dto import Token, UserCreate, UserRead, UserUpdate, UserList, RoleRead, RoleList
from library_service.settings import get_session from library_service.settings import get_session
from library_service.auth import (ACCESS_TOKEN_EXPIRE_MINUTES, RequireAdmin, from library_service.auth import (ACCESS_TOKEN_EXPIRE_MINUTES, RequireAdmin,
RequireAuth, authenticate_user, get_password_hash, RequireAuth, authenticate_user, get_password_hash,
@@ -136,7 +136,7 @@ def update_user_me(
@router.get( @router.get(
"/users", "/users",
response_model=list[UserRead], response_model=UserList,
summary="Список пользователей", summary="Список пользователей",
description="Получить список всех пользователей (только для админов)", description="Получить список всех пользователей (только для админов)",
) )
@@ -148,10 +148,10 @@ def read_users(
): ):
"""Эндпоинт получения списка всех пользователей""" """Эндпоинт получения списка всех пользователей"""
users = session.exec(select(User).offset(skip).limit(limit)).all() users = session.exec(select(User).offset(skip).limit(limit)).all()
return [ return UserList(
UserRead(**user.model_dump(), roles=[role.name for role in user.roles]) users=[UserRead(**user.model_dump()) for user in users],
for user in users total=len(users),
] )
@router.post( @router.post(
@@ -234,3 +234,20 @@ def remove_role_from_user(
session.refresh(user) session.refresh(user)
return UserRead(**user.model_dump(), roles=[r.name for r in user.roles]) return UserRead(**user.model_dump(), roles=[r.name for r in user.roles])
@router.get(
"/roles",
response_model=RoleList,
summary="Получить список ролей",
description="Возвращает список ролей",
)
def get_roles(
session: Session = Depends(get_session),
):
"""Эндпоинт получения списа ролей"""
roles = session.exec(select(Role)).all()
return RoleList(
roles=[RoleRead(**role.model_dump()) for role in roles],
total=len(roles),
)
+5
View File
@@ -66,6 +66,11 @@ async def auth(request: Request):
return templates.TemplateResponse(request, "auth.html") return templates.TemplateResponse(request, "auth.html")
@router.get("/profile", include_in_schema=False)
async def profile(request: Request):
"""Эндпоинт страницы профиля"""
return templates.TemplateResponse(request, "profile.html")
@router.get("/api", include_in_schema=False) @router.get("/api", include_in_schema=False)
async def api(request: Request, app=Depends(lambda: get_app())): async def api(request: Request, app=Depends(lambda: get_app())):
+509
View File
@@ -0,0 +1,509 @@
$(document).ready(() => {
let currentUser = null;
let allRoles = [];
const token = localStorage.getItem("access_token");
if (!token) {
window.location.href = "/login";
return;
}
loadProfile();
function loadProfile() {
showLoadingState();
Promise.all([
fetch("/api/auth/me", {
headers: { Authorization: "Bearer " + token },
}).then((response) => {
if (!response.ok) {
if (response.status === 401) {
throw new Error("Unauthorized");
}
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}),
fetch("/api/auth/roles", {
headers: { Authorization: "Bearer " + token },
}).then((response) => {
if (response.ok) return response.json();
return { roles: [] };
}),
])
.then(([user, rolesData]) => {
currentUser = user;
allRoles = rolesData.roles || [];
renderProfile(user);
renderAccountInfo(user);
renderRoles(user.roles, allRoles);
renderActions();
document.title = `LiB - ${user.full_name || user.username}`;
})
.catch((error) => {
console.error("Error loading profile:", error);
if (error.message === "Unauthorized") {
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
window.location.href = "/login";
} else {
showErrorState(error.message);
}
});
}
function renderProfile(user) {
const $card = $("#profile-card");
const displayName = user.full_name || user.username;
const firstLetter = displayName.charAt(0).toUpperCase();
const emailHash = sha256(user.email.trim().toLowerCase());
const avatarUrl = `https://www.gravatar.com/avatar/${emailHash}?d=identicon&s=200`;
$card.html(`
<div class="flex flex-col sm:flex-row items-center sm:items-start">
<!-- Аватар -->
<div class="relative mb-4 sm:mb-0 sm:mr-6">
<img src="${avatarUrl}" alt="Аватар"
class="w-24 h-24 rounded-full object-cover border-4 border-gray-200"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="w-24 h-24 bg-gray-500 text-white rounded-full items-center justify-center text-4xl font-bold hidden">
${firstLetter}
</div>
<!-- Статус верификации -->
${user.is_verified ? `
<div class="absolute -bottom-1 -right-1 bg-green-500 rounded-full p-1" title="Подтверждённый аккаунт">
<svg class="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
</div>
` : ''}
</div>
<!-- Информация -->
<div class="flex-1 text-center sm:text-left">
<h1 class="text-2xl font-bold text-gray-900 mb-1">${escapeHtml(displayName)}</h1>
<p class="text-gray-500 mb-3">@${escapeHtml(user.username)}</p>
<!-- Статусы -->
<div class="flex flex-wrap justify-center sm:justify-start gap-2">
${user.is_active ? `
<span class="inline-flex items-center bg-green-100 text-green-800 text-sm px-3 py-1 rounded-full">
<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
Активен
</span>
` : `
<span class="inline-flex items-center bg-red-100 text-red-800 text-sm px-3 py-1 rounded-full">
<span class="w-2 h-2 bg-red-500 rounded-full mr-2"></span>
Заблокирован
</span>
`}
${user.is_verified ? `
<span class="inline-flex items-center bg-blue-100 text-blue-800 text-sm px-3 py-1 rounded-full">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
Подтверждён
</span>
` : `
<span class="inline-flex items-center bg-yellow-100 text-yellow-800 text-sm px-3 py-1 rounded-full">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
Не подтверждён
</span>
`}
</div>
</div>
</div>
`);
}
function renderAccountInfo(user) {
const $container = $("#account-container");
$container.html(`
<div class="space-y-4">
<!-- ID пользователя -->
<div class="flex items-center justify-between py-3 border-b border-gray-100">
<div class="flex items-center">
<svg class="w-5 h-5 text-gray-400 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"/>
</svg>
<div>
<p class="text-sm text-gray-500">ID пользователя</p>
<p class="text-gray-900">${user.id}</p>
</div>
</div>
</div>
<!-- Имя пользователя -->
<div class="flex items-center justify-between py-3 border-b border-gray-100">
<div class="flex items-center">
<svg class="w-5 h-5 text-gray-400 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
<div>
<p class="text-sm text-gray-500">Имя пользователя</p>
<p class="text-gray-900">@${escapeHtml(user.username)}</p>
</div>
</div>
</div>
<!-- Полное имя -->
<div class="flex items-center justify-between py-3 border-b border-gray-100">
<div class="flex items-center">
<svg class="w-5 h-5 text-gray-400 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2"/>
</svg>
<div>
<p class="text-sm text-gray-500">Полное имя</p>
<p class="text-gray-900">${escapeHtml(user.full_name || "Не указано")}</p>
</div>
</div>
</div>
<!-- Email -->
<div class="flex items-center justify-between py-3">
<div class="flex items-center">
<svg class="w-5 h-5 text-gray-400 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
<div>
<p class="text-sm text-gray-500">Email</p>
<p class="text-gray-900">${escapeHtml(user.email)}</p>
</div>
</div>
</div>
</div>
`);
}
function renderRoles(userRoles, allRoles) {
const $container = $("#roles-container");
if (!userRoles || userRoles.length === 0) {
$container.html(`
<p class="text-gray-500">У вас нет назначенных ролей</p>
`);
return;
}
const roleDescriptions = {};
allRoles.forEach((role) => {
roleDescriptions[role.name] = role.description;
});
const roleIcons = {
admin: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>`,
librarian: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
</svg>`,
member: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>`,
};
const roleColors = {
admin: "bg-red-100 text-red-800 border-red-200",
librarian: "bg-blue-100 text-blue-800 border-blue-200",
member: "bg-green-100 text-green-800 border-green-200",
};
let rolesHtml = '<div class="space-y-3">';
userRoles.forEach((roleName) => {
const description = roleDescriptions[roleName] || "Описание недоступно";
const icon = roleIcons[roleName] || roleIcons.member;
const colorClass = roleColors[roleName] || roleColors.member;
rolesHtml += `
<div class="flex items-center p-4 rounded-lg border ${colorClass}">
<div class="flex-shrink-0 mr-4">
${icon}
</div>
<div>
<h4 class="font-medium capitalize">${escapeHtml(roleName)}</h4>
<p class="text-sm opacity-75">${escapeHtml(description)}</p>
</div>
</div>
`;
});
rolesHtml += '</div>';
$container.html(rolesHtml);
}
function renderActions() {
const $container = $("#actions-container");
$container.html(`
<div class="space-y-3">
<!-- Смена пароля -->
<button id="change-password-btn" class="w-full flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 rounded-lg transition-colors">
<div class="flex items-center">
<svg class="w-5 h-5 text-gray-500 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
</svg>
<span class="text-gray-700">Сменить пароль</span>
</div>
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</button>
<!-- Выход -->
<button id="logout-profile-btn" class="w-full flex items-center justify-between p-4 bg-red-50 hover:bg-red-100 rounded-lg transition-colors">
<div class="flex items-center">
<svg class="w-5 h-5 text-red-500 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
</svg>
<span class="text-red-700">Выйти из аккаунта</span>
</div>
<svg class="w-5 h-5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</button>
</div>
`);
$("#change-password-btn").on("click", openPasswordModal);
$("#logout-profile-btn").on("click", logout);
}
function openPasswordModal() {
$("#password-modal").removeClass("hidden").addClass("flex");
$("#current-password").focus();
}
function closePasswordModal() {
$("#password-modal").removeClass("flex").addClass("hidden");
$("#password-form")[0].reset();
$("#password-error").addClass("hidden").text("");
}
$("#close-password-modal, #cancel-password").on("click", closePasswordModal);
$("#password-modal").on("click", function (e) {
if (e.target === this) {
closePasswordModal();
}
});
$(document).on("keydown", function (e) {
if (e.key === "Escape" && $("#password-modal").hasClass("flex")) {
closePasswordModal();
}
});
$("#password-form").on("submit", function (e) {
e.preventDefault();
const currentPassword = $("#current-password").val();
const newPassword = $("#new-password").val();
const confirmPassword = $("#confirm-password").val();
const $error = $("#password-error");
if (newPassword !== confirmPassword) {
$error.text("Пароли не совпадают").removeClass("hidden");
return;
}
if (newPassword.length < 6) {
$error.text("Пароль должен содержать минимум 6 символов").removeClass("hidden");
return;
}
// TODO: смена пароля, 2FA
// fetch("/api/auth/change-password", {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// Authorization: "Bearer " + token,
// },
// body: JSON.stringify({
// current_password: currentPassword,
// new_password: newPassword,
// }),
// })
// .then((response) => {
// if (!response.ok) throw new Error("Ошибка смены пароля");
// return response.json();
// })
// .then(() => {
// closePasswordModal();
// showNotification("Пароль успешно изменён", "success");
// })
// .catch((error) => {
// $error.text(error.message).removeClass("hidden");
// });
$error.text("Функция смены пароля временно недоступна").removeClass("hidden");
});
function logout() {
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
window.location.href = "/login";
}
function showLoadingState() {
const $profileCard = $("#profile-card");
const $accountContainer = $("#account-container");
const $rolesContainer = $("#roles-container");
const $actionsContainer = $("#actions-container");
$profileCard.html(`
<div class="flex flex-col sm:flex-row items-center sm:items-start animate-pulse">
<div class="w-24 h-24 bg-gray-200 rounded-full mb-4 sm:mb-0 sm:mr-6"></div>
<div class="flex-1 text-center sm:text-left">
<div class="h-7 bg-gray-200 rounded w-48 mx-auto sm:mx-0 mb-2"></div>
<div class="h-5 bg-gray-200 rounded w-32 mx-auto sm:mx-0 mb-3"></div>
<div class="flex justify-center sm:justify-start gap-2">
<div class="h-7 bg-gray-200 rounded-full w-20"></div>
<div class="h-7 bg-gray-200 rounded-full w-28"></div>
</div>
</div>
</div>
`);
$accountContainer.html(`
<div class="space-y-4 animate-pulse">
${Array(4)
.fill()
.map(
() => `
<div class="flex items-center py-3 border-b border-gray-100">
<div class="w-5 h-5 bg-gray-200 rounded mr-3"></div>
<div>
<div class="h-3 bg-gray-200 rounded w-16 mb-2"></div>
<div class="h-4 bg-gray-200 rounded w-40"></div>
</div>
</div>
`
)
.join("")}
</div>
`);
$rolesContainer.html(`
<div class="space-y-3 animate-pulse">
<div class="h-16 bg-gray-200 rounded-lg"></div>
</div>
`);
$actionsContainer.html(`
<div class="space-y-3 animate-pulse">
<div class="h-14 bg-gray-200 rounded-lg"></div>
<div class="h-14 bg-gray-200 rounded-lg"></div>
</div>
`);
}
function showErrorState(message) {
const $profileCard = $("#profile-card");
const $accountSection = $("#account-section");
const $rolesSection = $("#roles-section");
const $actionsSection = $("#actions-section");
$accountSection.hide();
$rolesSection.hide();
$actionsSection.hide();
$profileCard.html(`
<div class="text-center py-8">
<svg class="mx-auto h-16 w-16 text-red-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<h3 class="text-xl font-medium text-gray-900 mb-2">${escapeHtml(message)}</h3>
<p class="text-gray-500 mb-6">Не удалось загрузить профиль</p>
<div class="flex justify-center gap-4">
<button id="retry-btn" class="bg-gray-500 text-white px-6 py-2 rounded-lg hover:bg-gray-600 transition-colors">
Попробовать снова
</button>
<a href="/" class="bg-white text-gray-700 px-6 py-2 rounded-lg border border-gray-300 hover:bg-gray-50 transition-colors">
На главную
</a>
</div>
</div>
`);
$("#retry-btn").on("click", function () {
$accountSection.show();
$rolesSection.show();
$actionsSection.show();
loadProfile();
});
}
function escapeHtml(text) {
if (!text) return "";
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
const $guestLink = $("#guest-link");
const $userBtn = $("#user-btn");
const $userDropdown = $("#user-dropdown");
const $userArrow = $("#user-arrow");
const $userAvatar = $("#user-avatar");
const $dropdownName = $("#dropdown-name");
const $dropdownUsername = $("#dropdown-username");
const $dropdownEmail = $("#dropdown-email");
const $logoutBtn = $("#logout-btn");
let isDropdownOpen = false;
function openDropdown() {
isDropdownOpen = true;
$userDropdown.removeClass("hidden");
$userArrow.addClass("rotate-180");
}
function closeDropdown() {
isDropdownOpen = false;
$userDropdown.addClass("hidden");
$userArrow.removeClass("rotate-180");
}
$userBtn.on("click", function (e) {
e.stopPropagation();
isDropdownOpen ? closeDropdown() : openDropdown();
});
$(document).on("click", function (e) {
if (isDropdownOpen && !$(e.target).closest("#user-menu-container").length) {
closeDropdown();
}
});
$logoutBtn.on("click", logout);
function showGuest() {
$guestLink.removeClass("hidden");
$userBtn.addClass("hidden").removeClass("flex");
closeDropdown();
}
function showUser(user) {
$guestLink.addClass("hidden");
$userBtn.removeClass("hidden").addClass("flex");
const displayName = user.full_name || user.username;
const firstLetter = displayName.charAt(0).toUpperCase();
$userAvatar.text(firstLetter);
$dropdownName.text(displayName);
$dropdownUsername.text("@" + user.username);
$dropdownEmail.text(user.email);
}
if (currentUser) {
showUser(currentUser);
}
});
+69
View File
@@ -0,0 +1,69 @@
{% extends "base.html" %} {% block title %}LiB - Профиль{% endblock %} {% block
content %}
<div class="flex flex-1 mt-4 p-4">
<main class="flex-1 max-w-2xl mx-auto">
<div id="profile-card" class="bg-white p-6 rounded-lg shadow-md mb-6">
</div>
<div id="account-section" class="bg-white p-6 rounded-lg shadow-md mb-6">
<h2 class="text-xl font-semibold mb-4">Информация об аккаунте</h2>
<div id="account-container">
</div>
</div>
<div id="roles-section" class="bg-white p-6 rounded-lg shadow-md mb-6">
<h2 class="text-xl font-semibold mb-4">Роли и права</h2>
<div id="roles-container">
</div>
</div>
<div id="actions-section" class="bg-white p-6 rounded-lg shadow-md">
<h2 class="text-xl font-semibold mb-4">Действия</h2>
<div id="actions-container">
</div>
</div>
</main>
</div>
<div id="password-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md mx-4">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold">Смена пароля</h3>
<button id="close-password-modal" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<form id="password-form">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-medium mb-2">Текущий пароль</label>
<input type="password" id="current-password"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-gray-500"
required minlength="6">
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-medium mb-2">Новый пароль</label>
<input type="password" id="new-password"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-gray-500"
required minlength="6">
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-medium mb-2">Подтвердите новый пароль</label>
<input type="password" id="confirm-password"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-gray-500"
required minlength="6">
</div>
<div id="password-error" class="mb-4 text-red-600 text-sm hidden"></div>
<div class="flex gap-3">
<button type="submit" class="flex-1 bg-gray-500 text-white py-2 px-4 rounded-lg hover:bg-gray-600 transition-colors">
Сменить пароль
</button>
<button type="button" id="cancel-password" class="flex-1 bg-white text-gray-700 py-2 px-4 rounded-lg border border-gray-300 hover:bg-gray-50 transition-colors">
Отмена
</button>
</div>
</form>
</div>
</div>
{% endblock %} {% block scripts %}
<script type="text/javascript" src="/static/profile.js"></script>
{% endblock %}