Добавление временной авторизации

This commit is contained in:
2025-12-22 22:25:51 +03:00
parent 49d1681bcb
commit 82d298effe
13 changed files with 154 additions and 127 deletions
+27 -13
View File
@@ -1,6 +1,6 @@
$(() => { $(() => {
$("#login-tab").on("click", function () { $("#login-tab").on("click", function () {
$("#login-tab") $(this)
.removeClass("text-gray-400 hover:text-gray-600") .removeClass("text-gray-400 hover:text-gray-600")
.addClass("text-gray-700 bg-gray-50 border-b-2 border-gray-500"); .addClass("text-gray-700 bg-gray-50 border-b-2 border-gray-500");
$("#register-tab") $("#register-tab")
@@ -12,7 +12,7 @@ $(() => {
}); });
$("#register-tab").on("click", function () { $("#register-tab").on("click", function () {
$("#register-tab") $(this)
.removeClass("text-gray-400 hover:text-gray-600") .removeClass("text-gray-400 hover:text-gray-600")
.addClass("text-gray-700 bg-gray-50 border-b-2 border-gray-500"); .addClass("text-gray-700 bg-gray-50 border-b-2 border-gray-500");
$("#login-tab") $("#login-tab")
@@ -23,6 +23,15 @@ $(() => {
$("#login-form").addClass("hidden"); $("#login-form").addClass("hidden");
}); });
$("body").on("click", ".toggle-password", function () {
const $btn = $(this);
const $input = $btn.siblings("input");
const isPassword = $input.attr("type") === "password";
$input.attr("type", isPassword ? "text" : "password");
$btn.find("svg").toggleClass("hidden");
});
$("#register-password").on("input", function () { $("#register-password").on("input", function () {
const password = $(this).val(); const password = $(this).val();
let strength = 0; let strength = 0;
@@ -74,6 +83,7 @@ $(() => {
const username = $("#login-username").val(); const username = $("#login-username").val();
const password = $("#login-password").val(); const password = $("#login-password").val();
const rememberMe = $("#remember-me").prop("checked");
$submitBtn.prop("disabled", true).text("Вход..."); $submitBtn.prop("disabled", true).text("Вход...");
try { try {
@@ -82,10 +92,17 @@ $(() => {
formData.append("password", password); formData.append("password", password);
const data = await Api.postForm("/api/auth/token", formData); const data = await Api.postForm("/api/auth/token", formData);
const storage = rememberMe ? localStorage : sessionStorage;
storage.setItem("access_token", data.access_token);
if (rememberMe && data.refresh_token) {
storage.setItem("refresh_token", data.refresh_token);
}
const otherStorage = rememberMe ? sessionStorage : localStorage;
otherStorage.removeItem("access_token");
otherStorage.removeItem("refresh_token");
localStorage.setItem("access_token", data.access_token);
if (data.refresh_token)
localStorage.setItem("refresh_token", data.refresh_token);
window.location.href = "/"; window.location.href = "/";
} catch (error) { } catch (error) {
Utils.showToast(error.message || "Ошибка входа", "error"); Utils.showToast(error.message || "Ошибка входа", "error");
@@ -117,7 +134,11 @@ $(() => {
try { try {
await Api.post("/api/auth/register", userData); await Api.post("/api/auth/register", userData);
Utils.showToast("Регистрация успешна! Войдите в систему.", "success"); Utils.showToast("Регистрация успешна! Войдите в систему.", "success");
setTimeout(() => window.location.reload(), 1500);
setTimeout(() => {
$("#login-tab").trigger("click");
$("#login-username").val(userData.username);
}, 1500);
} catch (error) { } catch (error) {
let msg = error.message; let msg = error.message;
if (Array.isArray(error.detail)) { if (Array.isArray(error.detail)) {
@@ -128,11 +149,4 @@ $(() => {
$submitBtn.prop("disabled", false).text("Зарегистрироваться"); $submitBtn.prop("disabled", false).text("Зарегистрироваться");
} }
}); });
$("body").on("click", ".toggle-password", function () {
const $input = $(this).siblings("input");
const type = $input.attr("type") === "password" ? "text" : "password";
$input.attr("type", type);
$(this).find("svg").toggleClass("hidden");
});
}); });
+1 -1
View File
@@ -12,7 +12,7 @@ $(document).ready(() => {
document.title = `LiB - ${author.name}`; document.title = `LiB - ${author.name}`;
renderAuthor(author); renderAuthor(author);
renderBooks(author.books); renderBooks(author.books);
if (window.canManage) { if (window.canManage()) {
$("#edit-author-btn") $("#edit-author-btn")
.attr("href", `/author/${author.id}/edit`) .attr("href", `/author/${author.id}/edit`)
.removeClass("hidden"); .removeClass("hidden");
+2 -2
View File
@@ -57,7 +57,7 @@ $(document).ready(() => {
currentBook = book; currentBook = book;
document.title = `LiB - ${book.title}`; document.title = `LiB - ${book.title}`;
renderBook(book); renderBook(book);
if (window.canManage) { if (window.canManage()) {
$("#edit-book-btn") $("#edit-book-btn")
.attr("href", `/book/${book.id}/edit`) .attr("href", `/book/${book.id}/edit`)
.removeClass("hidden"); .removeClass("hidden");
@@ -123,7 +123,7 @@ $(document).ready(() => {
$container.empty(); $container.empty();
const config = getStatusConfig(book.status); const config = getStatusConfig(book.status);
if (window.canManage) { if (window.canManage()) {
const $dropdownHTML = $(` const $dropdownHTML = $(`
<div class="relative inline-block text-left w-full md:w-auto"> <div class="relative inline-block text-left w-full md:w-auto">
<button id="status-toggle-btn" type="button" class="w-full justify-center md:w-auto inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold transition-all shadow-sm ${config.bgClass} ${config.textClass} hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-400"> <button id="status-toggle-btn" type="button" class="w-full justify-center md:w-auto inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold transition-all shadow-sm ${config.bgClass} ${config.textClass} hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-400">
+1 -1
View File
@@ -403,7 +403,7 @@ $(document).ready(() => {
}); });
function showAdminControls() { function showAdminControls() {
if (window.canManage) { if (window.canManage()) {
$("#admin-actions").removeClass("hidden"); $("#admin-actions").removeClass("hidden");
} }
} }
+1 -1
View File
@@ -1,5 +1,5 @@
$(document).ready(() => { $(document).ready(() => {
if (!window.canManage) return; if (!window.canManage()) return;
setTimeout(() => window.canManage, 100); setTimeout(() => window.canManage, 100);
const $form = $("#create-author-form"); const $form = $("#create-author-form");
+1 -1
View File
@@ -1,5 +1,5 @@
$(document).ready(() => { $(document).ready(() => {
if (!window.canManage) return; if (!window.canManage()) return;
setTimeout(() => window.canManage, 100); setTimeout(() => window.canManage, 100);
let allAuthors = []; let allAuthors = [];
+1 -1
View File
@@ -1,5 +1,5 @@
$(document).ready(() => { $(document).ready(() => {
if (!window.canManage) return; if (!window.canManage()) return;
setTimeout(() => window.canManage, 100); setTimeout(() => window.canManage, 100);
const $form = $("#create-genre-form"); const $form = $("#create-genre-form");
+1 -1
View File
@@ -1,5 +1,5 @@
$(document).ready(() => { $(document).ready(() => {
if (!window.canManage) return; if (!window.canManage()) return;
setTimeout(() => window.canManage, 100); setTimeout(() => window.canManage, 100);
const pathParts = window.location.pathname.split("/"); const pathParts = window.location.pathname.split("/");
+1 -1
View File
@@ -1,5 +1,5 @@
$(document).ready(() => { $(document).ready(() => {
if (!window.canManage) { if (!window.canManage()) {
Utils.showToast("У вас недостаточно прав", "error"); Utils.showToast("У вас недостаточно прав", "error");
setTimeout(() => (window.location.href = "/"), 1500); setTimeout(() => (window.location.href = "/"), 1500);
return; return;
+1 -1
View File
@@ -1,5 +1,5 @@
$(document).ready(() => { $(document).ready(() => {
const token = localStorage.getItem("access_token"); const token = StorageHelper.get("access_token");
if (!token) { if (!token) {
window.location.href = "/auth"; window.location.href = "/auth";
return; return;
+72 -83
View File
@@ -1,11 +1,74 @@
@keyframes shake {
0%,
to {
transform: translateX(0);
}
10%,
30%,
50%,
70%,
90% {
transform: translateX(-5px);
}
20%,
40%,
60%,
80% {
transform: translateX(5px);
}
}
@keyframes fadeIn {
0% {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes dropdownFade {
0% {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse-soft {
0%,
to {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
@keyframes fadeInUp {
0% {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@font-face { @font-face {
font-family: "Novem"; font-family: "Novem";
src: url("novem.regular.ttf") format("truetype"); src: url(novem.regular.ttf) format("truetype");
} }
@font-face { @font-face {
font-family: "Dited"; font-family: "Dited";
src: url("dited.regular.ttf") format("truetype"); src: url(dited.regular.ttf) format("truetype");
} }
h1 { h1 {
@@ -13,9 +76,9 @@ h1 {
letter-spacing: 10px; letter-spacing: 10px;
} }
h2,
.book-id, .book-id,
.book-status, .book-status,
h2,
nav ul li a { nav ul li a {
font-family: "Dited", sans-serif; font-family: "Dited", sans-serif;
letter-spacing: 2.5px; letter-spacing: 2.5px;
@@ -73,7 +136,7 @@ nav ul li a {
top: 6px; top: 6px;
width: 4px; width: 4px;
height: 8px; height: 8px;
border: solid white; border: solid #fff;
border-width: 0 2px 2px 0; border-width: 0 2px 2px 0;
transform: rotate(45deg); transform: rotate(45deg);
} }
@@ -96,17 +159,6 @@ button:disabled {
animation: fadeIn 0.3s ease-in-out; animation: fadeIn 0.3s ease-in-out;
} }
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.flex.justify-center.gap-4 button:hover { .flex.justify-center.gap-4 button:hover {
transform: translateY(-2px); transform: translateY(-2px);
} }
@@ -115,30 +167,10 @@ button:disabled {
animation: shake 0.5s ease-in-out; animation: shake 0.5s ease-in-out;
} }
@keyframes shake { #req-digit,
0%,
100% {
transform: translateX(0);
}
10%,
30%,
50%,
70%,
90% {
transform: translateX(-5px);
}
20%,
40%,
60%,
80% {
transform: translateX(5px);
}
}
#req-length, #req-length,
#req-upper,
#req-lower, #req-lower,
#req-digit { #req-upper {
transition: color 0.2s ease; transition: color 0.2s ease;
} }
@@ -153,17 +185,6 @@ button:disabled {
animation: fadeIn 0.3s ease-in-out; animation: fadeIn 0.3s ease-in-out;
} }
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
#login-tab, #login-tab,
#register-tab { #register-tab {
font-family: "Dited", sans-serif; font-family: "Dited", sans-serif;
@@ -175,17 +196,6 @@ button:disabled {
animation: dropdownFade 0.1s ease-out; animation: dropdownFade 0.1s ease-out;
} }
@keyframes dropdownFade {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
#user-arrow.rotate-180 { #user-arrow.rotate-180 {
transform: rotate(180deg); transform: rotate(180deg);
} }
@@ -198,16 +208,6 @@ button:disabled {
min-width: 140px; min-width: 140px;
} }
@keyframes pulse-soft {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
.animate-pulse-soft { .animate-pulse-soft {
animation: pulse-soft 2s ease-in-out infinite; animation: pulse-soft 2s ease-in-out infinite;
} }
@@ -216,32 +216,21 @@ button:disabled {
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1)); filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
} }
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-up { .animate-fade-in-up {
animation: fadeInUp 0.5s ease-out forwards; animation: fadeInUp 0.5s ease-out forwards;
} }
.stat-card:hover svg { .stat-card:hover svg {
transform: scale(1.1); transform: scale(1.1);
transition: transform 0.3s ease;
} }
.stat-card svg { .stat-card svg,
.stat-card:hover svg {
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
.gradient-text { .gradient-text {
background: linear-gradient(135deg, #374151 0%, #6b7280 100%); background: linear-gradient(135deg, #374151 0, #6b7280 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
+45 -18
View File
@@ -1,3 +1,22 @@
const StorageHelper = {
get: (key) => {
return localStorage.getItem(key) || sessionStorage.getItem(key);
},
getCurrentStorage: () => {
return localStorage.getItem("refresh_token")
? localStorage
: sessionStorage;
},
clearAll: () => {
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
localStorage.removeItem("user");
sessionStorage.removeItem("access_token");
sessionStorage.removeItem("refresh_token");
sessionStorage.removeItem("user");
},
};
const Utils = { const Utils = {
escapeHtml: (text) => { escapeHtml: (text) => {
if (!text) return ""; if (!text) return "";
@@ -59,7 +78,8 @@ const Api = {
async request(endpoint, options = {}) { async request(endpoint, options = {}) {
const fullUrl = this.getBaseUrl() + endpoint; const fullUrl = this.getBaseUrl() + endpoint;
const token = localStorage.getItem("access_token"); const token = StorageHelper.get("access_token");
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
...options.headers, ...options.headers,
@@ -74,11 +94,13 @@ const Api = {
try { try {
const response = await fetch(fullUrl, config); const response = await fetch(fullUrl, config);
if (response.status === 401) { const isLoginRequest = endpoint.includes("/auth/token");
if (response.status === 401 && !isLoginRequest) {
const refreshed = await Auth.tryRefresh(); const refreshed = await Auth.tryRefresh();
if (refreshed) { if (refreshed) {
headers["Authorization"] = headers["Authorization"] =
`Bearer ${localStorage.getItem("access_token")}`; `Bearer ${StorageHelper.get("access_token")}`;
const retryResponse = await fetch(fullUrl, { ...options, headers }); const retryResponse = await fetch(fullUrl, { ...options, headers });
if (retryResponse.ok) { if (retryResponse.ok) {
return retryResponse.json(); return retryResponse.json();
@@ -90,7 +112,11 @@ const Api = {
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `Error ${response.status}`); throw new Error(
errorData.detail ||
errorData.error_description ||
`Ошибка ${response.status}`,
);
} }
return response.json(); return response.json();
} catch (error) { } catch (error) {
@@ -131,16 +157,16 @@ const Api = {
const Auth = { const Auth = {
logout: () => { logout: () => {
localStorage.removeItem("access_token"); StorageHelper.clearAll();
localStorage.removeItem("refresh_token");
localStorage.removeItem("user");
window.location.href = "/"; window.location.href = "/";
}, },
tryRefresh: async () => { tryRefresh: async () => {
const refreshToken = localStorage.getItem("refresh_token"); const refreshToken = StorageHelper.get("refresh_token");
if (!refreshToken) return false; if (!refreshToken) return false;
const activeStorage = StorageHelper.getCurrentStorage();
try { try {
const response = await fetch("/api/auth/refresh", { const response = await fetch("/api/auth/refresh", {
method: "POST", method: "POST",
@@ -150,8 +176,8 @@ const Auth = {
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
localStorage.setItem("access_token", data.access_token); activeStorage.setItem("access_token", data.access_token);
localStorage.setItem("refresh_token", data.refresh_token); activeStorage.setItem("refresh_token", data.refresh_token);
return true; return true;
} }
} catch (e) { } catch (e) {
@@ -161,14 +187,17 @@ const Auth = {
}, },
init: async () => { init: async () => {
const token = localStorage.getItem("access_token"); const token = StorageHelper.get("access_token");
const refreshToken = localStorage.getItem("refresh_token"); const refreshToken = StorageHelper.get("refresh_token");
if (!token && !refreshToken) { if (!token && !refreshToken) {
localStorage.removeItem("user"); localStorage.removeItem("user");
sessionStorage.removeItem("user");
return null; return null;
} }
const activeStorage = StorageHelper.getCurrentStorage();
try { try {
let response = await fetch("/api/auth/me", { let response = await fetch("/api/auth/me", {
headers: { Authorization: "Bearer " + token }, headers: { Authorization: "Bearer " + token },
@@ -179,7 +208,7 @@ const Auth = {
if (refreshed) { if (refreshed) {
response = await fetch("/api/auth/me", { response = await fetch("/api/auth/me", {
headers: { headers: {
Authorization: "Bearer " + localStorage.getItem("access_token"), Authorization: "Bearer " + StorageHelper.get("access_token"),
}, },
}); });
} }
@@ -187,7 +216,7 @@ const Auth = {
if (response.ok) { if (response.ok) {
const user = await response.json(); const user = await response.json();
localStorage.setItem("user", JSON.stringify(user)); activeStorage.setItem("user", JSON.stringify(user));
document.dispatchEvent(new CustomEvent("auth:login", { detail: user })); document.dispatchEvent(new CustomEvent("auth:login", { detail: user }));
return user; return user;
} }
@@ -195,15 +224,13 @@ const Auth = {
console.error("Auth check failed", e); console.error("Auth check failed", e);
} }
localStorage.removeItem("access_token"); StorageHelper.clearAll();
localStorage.removeItem("refresh_token");
localStorage.removeItem("user");
return null; return null;
}, },
}; };
window.getUser = function () { window.getUser = function () {
const userJson = localStorage.getItem("user"); const userJson = StorageHelper.get("user");
if (!userJson) return null; if (!userJson) return null;
try { try {
return JSON.parse(userJson); return JSON.parse(userJson);
-3
View File
@@ -53,7 +53,6 @@ block content %}
<button <button
type="button" type="button"
class="toggle-password absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600" class="toggle-password absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
onclick="togglePassword(this)"
> >
<svg <svg
class="eye-open w-5 h-5" class="eye-open w-5 h-5"
@@ -190,7 +189,6 @@ block content %}
<button <button
type="button" type="button"
class="toggle-password absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600" class="toggle-password absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
onclick="togglePassword(this)"
> >
<svg <svg
class="eye-open w-5 h-5" class="eye-open w-5 h-5"
@@ -259,7 +257,6 @@ block content %}
<button <button
type="button" type="button"
class="toggle-password absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600" class="toggle-password absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
onclick="togglePassword(this)"
> >
<svg <svg
class="eye-open w-5 h-5" class="eye-open w-5 h-5"