mirror of
https://github.com/wowlikon/LiB.git
synced 2026-02-04 04:31:09 +00:00
Добавление временной авторизации
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
$(() => {
|
||||
$("#login-tab").on("click", function () {
|
||||
$("#login-tab")
|
||||
$(this)
|
||||
.removeClass("text-gray-400 hover:text-gray-600")
|
||||
.addClass("text-gray-700 bg-gray-50 border-b-2 border-gray-500");
|
||||
$("#register-tab")
|
||||
@@ -12,7 +12,7 @@ $(() => {
|
||||
});
|
||||
|
||||
$("#register-tab").on("click", function () {
|
||||
$("#register-tab")
|
||||
$(this)
|
||||
.removeClass("text-gray-400 hover:text-gray-600")
|
||||
.addClass("text-gray-700 bg-gray-50 border-b-2 border-gray-500");
|
||||
$("#login-tab")
|
||||
@@ -23,6 +23,15 @@ $(() => {
|
||||
$("#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 () {
|
||||
const password = $(this).val();
|
||||
let strength = 0;
|
||||
@@ -74,6 +83,7 @@ $(() => {
|
||||
const username = $("#login-username").val();
|
||||
const password = $("#login-password").val();
|
||||
|
||||
const rememberMe = $("#remember-me").prop("checked");
|
||||
$submitBtn.prop("disabled", true).text("Вход...");
|
||||
|
||||
try {
|
||||
@@ -82,10 +92,17 @@ $(() => {
|
||||
formData.append("password", password);
|
||||
|
||||
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 = "/";
|
||||
} catch (error) {
|
||||
Utils.showToast(error.message || "Ошибка входа", "error");
|
||||
@@ -117,7 +134,11 @@ $(() => {
|
||||
try {
|
||||
await Api.post("/api/auth/register", userData);
|
||||
Utils.showToast("Регистрация успешна! Войдите в систему.", "success");
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
|
||||
setTimeout(() => {
|
||||
$("#login-tab").trigger("click");
|
||||
$("#login-username").val(userData.username);
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
let msg = error.message;
|
||||
if (Array.isArray(error.detail)) {
|
||||
@@ -128,11 +149,4 @@ $(() => {
|
||||
$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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ $(document).ready(() => {
|
||||
document.title = `LiB - ${author.name}`;
|
||||
renderAuthor(author);
|
||||
renderBooks(author.books);
|
||||
if (window.canManage) {
|
||||
if (window.canManage()) {
|
||||
$("#edit-author-btn")
|
||||
.attr("href", `/author/${author.id}/edit`)
|
||||
.removeClass("hidden");
|
||||
|
||||
@@ -57,7 +57,7 @@ $(document).ready(() => {
|
||||
currentBook = book;
|
||||
document.title = `LiB - ${book.title}`;
|
||||
renderBook(book);
|
||||
if (window.canManage) {
|
||||
if (window.canManage()) {
|
||||
$("#edit-book-btn")
|
||||
.attr("href", `/book/${book.id}/edit`)
|
||||
.removeClass("hidden");
|
||||
@@ -123,7 +123,7 @@ $(document).ready(() => {
|
||||
$container.empty();
|
||||
const config = getStatusConfig(book.status);
|
||||
|
||||
if (window.canManage) {
|
||||
if (window.canManage()) {
|
||||
const $dropdownHTML = $(`
|
||||
<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">
|
||||
|
||||
@@ -403,7 +403,7 @@ $(document).ready(() => {
|
||||
});
|
||||
|
||||
function showAdminControls() {
|
||||
if (window.canManage) {
|
||||
if (window.canManage()) {
|
||||
$("#admin-actions").removeClass("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
$(document).ready(() => {
|
||||
if (!window.canManage) return;
|
||||
if (!window.canManage()) return;
|
||||
setTimeout(() => window.canManage, 100);
|
||||
|
||||
const $form = $("#create-author-form");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
$(document).ready(() => {
|
||||
if (!window.canManage) return;
|
||||
if (!window.canManage()) return;
|
||||
setTimeout(() => window.canManage, 100);
|
||||
|
||||
let allAuthors = [];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
$(document).ready(() => {
|
||||
if (!window.canManage) return;
|
||||
if (!window.canManage()) return;
|
||||
setTimeout(() => window.canManage, 100);
|
||||
|
||||
const $form = $("#create-genre-form");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
$(document).ready(() => {
|
||||
if (!window.canManage) return;
|
||||
if (!window.canManage()) return;
|
||||
setTimeout(() => window.canManage, 100);
|
||||
|
||||
const pathParts = window.location.pathname.split("/");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
$(document).ready(() => {
|
||||
if (!window.canManage) {
|
||||
if (!window.canManage()) {
|
||||
Utils.showToast("У вас недостаточно прав", "error");
|
||||
setTimeout(() => (window.location.href = "/"), 1500);
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
$(document).ready(() => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
const token = StorageHelper.get("access_token");
|
||||
if (!token) {
|
||||
window.location.href = "/auth";
|
||||
return;
|
||||
|
||||
@@ -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-family: "Novem";
|
||||
src: url("novem.regular.ttf") format("truetype");
|
||||
src: url(novem.regular.ttf) format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Dited";
|
||||
src: url("dited.regular.ttf") format("truetype");
|
||||
src: url(dited.regular.ttf) format("truetype");
|
||||
}
|
||||
|
||||
h1 {
|
||||
@@ -13,9 +76,9 @@ h1 {
|
||||
letter-spacing: 10px;
|
||||
}
|
||||
|
||||
h2,
|
||||
.book-id,
|
||||
.book-status,
|
||||
h2,
|
||||
nav ul li a {
|
||||
font-family: "Dited", sans-serif;
|
||||
letter-spacing: 2.5px;
|
||||
@@ -73,7 +136,7 @@ nav ul li a {
|
||||
top: 6px;
|
||||
width: 4px;
|
||||
height: 8px;
|
||||
border: solid white;
|
||||
border: solid #fff;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
@@ -96,17 +159,6 @@ button:disabled {
|
||||
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 {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
@@ -115,30 +167,10 @@ button:disabled {
|
||||
animation: shake 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
10%,
|
||||
30%,
|
||||
50%,
|
||||
70%,
|
||||
90% {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
20%,
|
||||
40%,
|
||||
60%,
|
||||
80% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
#req-digit,
|
||||
#req-length,
|
||||
#req-upper,
|
||||
#req-lower,
|
||||
#req-digit {
|
||||
#req-upper {
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -153,17 +185,6 @@ button:disabled {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
#login-tab,
|
||||
#register-tab {
|
||||
font-family: "Dited", sans-serif;
|
||||
@@ -175,17 +196,6 @@ button:disabled {
|
||||
animation: dropdownFade 0.1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes dropdownFade {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
#user-arrow.rotate-180 {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
@@ -198,16 +208,6 @@ button:disabled {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
@keyframes pulse-soft {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse-soft {
|
||||
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));
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
.stat-card:hover svg {
|
||||
transform: scale(1.1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card svg {
|
||||
.stat-card svg,
|
||||
.stat-card:hover svg {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, #374151 0%, #6b7280 100%);
|
||||
background: linear-gradient(135deg, #374151 0, #6b7280 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
|
||||
@@ -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 = {
|
||||
escapeHtml: (text) => {
|
||||
if (!text) return "";
|
||||
@@ -59,7 +78,8 @@ const Api = {
|
||||
|
||||
async request(endpoint, options = {}) {
|
||||
const fullUrl = this.getBaseUrl() + endpoint;
|
||||
const token = localStorage.getItem("access_token");
|
||||
const token = StorageHelper.get("access_token");
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
@@ -74,11 +94,13 @@ const Api = {
|
||||
try {
|
||||
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();
|
||||
if (refreshed) {
|
||||
headers["Authorization"] =
|
||||
`Bearer ${localStorage.getItem("access_token")}`;
|
||||
`Bearer ${StorageHelper.get("access_token")}`;
|
||||
const retryResponse = await fetch(fullUrl, { ...options, headers });
|
||||
if (retryResponse.ok) {
|
||||
return retryResponse.json();
|
||||
@@ -90,7 +112,11 @@ const Api = {
|
||||
|
||||
if (!response.ok) {
|
||||
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();
|
||||
} catch (error) {
|
||||
@@ -131,16 +157,16 @@ const Api = {
|
||||
|
||||
const Auth = {
|
||||
logout: () => {
|
||||
localStorage.removeItem("access_token");
|
||||
localStorage.removeItem("refresh_token");
|
||||
localStorage.removeItem("user");
|
||||
StorageHelper.clearAll();
|
||||
window.location.href = "/";
|
||||
},
|
||||
|
||||
tryRefresh: async () => {
|
||||
const refreshToken = localStorage.getItem("refresh_token");
|
||||
const refreshToken = StorageHelper.get("refresh_token");
|
||||
if (!refreshToken) return false;
|
||||
|
||||
const activeStorage = StorageHelper.getCurrentStorage();
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/auth/refresh", {
|
||||
method: "POST",
|
||||
@@ -150,8 +176,8 @@ const Auth = {
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
localStorage.setItem("access_token", data.access_token);
|
||||
localStorage.setItem("refresh_token", data.refresh_token);
|
||||
activeStorage.setItem("access_token", data.access_token);
|
||||
activeStorage.setItem("refresh_token", data.refresh_token);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -161,14 +187,17 @@ const Auth = {
|
||||
},
|
||||
|
||||
init: async () => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
const refreshToken = localStorage.getItem("refresh_token");
|
||||
const token = StorageHelper.get("access_token");
|
||||
const refreshToken = StorageHelper.get("refresh_token");
|
||||
|
||||
if (!token && !refreshToken) {
|
||||
localStorage.removeItem("user");
|
||||
sessionStorage.removeItem("user");
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeStorage = StorageHelper.getCurrentStorage();
|
||||
|
||||
try {
|
||||
let response = await fetch("/api/auth/me", {
|
||||
headers: { Authorization: "Bearer " + token },
|
||||
@@ -179,7 +208,7 @@ const Auth = {
|
||||
if (refreshed) {
|
||||
response = await fetch("/api/auth/me", {
|
||||
headers: {
|
||||
Authorization: "Bearer " + localStorage.getItem("access_token"),
|
||||
Authorization: "Bearer " + StorageHelper.get("access_token"),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -187,7 +216,7 @@ const Auth = {
|
||||
|
||||
if (response.ok) {
|
||||
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 }));
|
||||
return user;
|
||||
}
|
||||
@@ -195,15 +224,13 @@ const Auth = {
|
||||
console.error("Auth check failed", e);
|
||||
}
|
||||
|
||||
localStorage.removeItem("access_token");
|
||||
localStorage.removeItem("refresh_token");
|
||||
localStorage.removeItem("user");
|
||||
StorageHelper.clearAll();
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
window.getUser = function () {
|
||||
const userJson = localStorage.getItem("user");
|
||||
const userJson = StorageHelper.get("user");
|
||||
if (!userJson) return null;
|
||||
try {
|
||||
return JSON.parse(userJson);
|
||||
|
||||
@@ -53,7 +53,6 @@ block content %}
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
class="eye-open w-5 h-5"
|
||||
@@ -190,7 +189,6 @@ block content %}
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
class="eye-open w-5 h-5"
|
||||
@@ -259,7 +257,6 @@ block content %}
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
class="eye-open w-5 h-5"
|
||||
|
||||
Reference in New Issue
Block a user