changed anilist login flow
This commit is contained in:
@@ -2,58 +2,49 @@ import { FastifyInstance } from "fastify";
|
||||
import { run } from "../../shared/database";
|
||||
|
||||
async function anilist(fastify: FastifyInstance) {
|
||||
fastify.get("/anilist", async (request, reply) => {
|
||||
fastify.post("/anilist/store", async (request, reply) => {
|
||||
try {
|
||||
const { code, state } = request.query as { code?: string; state?: string };
|
||||
const {
|
||||
userId,
|
||||
accessToken,
|
||||
tokenType = "Bearer",
|
||||
expiresIn
|
||||
} = request.body as {
|
||||
userId: number;
|
||||
accessToken: string;
|
||||
tokenType?: string;
|
||||
expiresIn?: number;
|
||||
};
|
||||
|
||||
if (!code) return reply.status(400).send("No code");
|
||||
if (!state) return reply.status(400).send("No user state");
|
||||
|
||||
const userId = Number(state);
|
||||
if (!userId || isNaN(userId)) {
|
||||
return reply.status(400).send("Invalid user id");
|
||||
}
|
||||
|
||||
const tokenRes = await fetch("https://anilist.co/api/v2/oauth/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
grant_type: "authorization_code",
|
||||
client_id: process.env.ANILIST_CLIENT_ID,
|
||||
client_secret: process.env.ANILIST_CLIENT_SECRET,
|
||||
redirect_uri: "http://localhost:54322/api/anilist",
|
||||
code
|
||||
})
|
||||
});
|
||||
|
||||
const tokenData = await tokenRes.json();
|
||||
|
||||
if (!tokenData.access_token) {
|
||||
console.error("AniList token error:", tokenData);
|
||||
return reply.status(500).send("Failed to get AniList token");
|
||||
if (!userId || !accessToken) {
|
||||
return reply.status(400).send({ error: "Faltan datos (User ID o Token)" });
|
||||
}
|
||||
|
||||
// 1. Verificar que el token es válido consultando a AniList
|
||||
const userRes = await fetch("https://graphql.anilist.co", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `${tokenData.token_type} ${tokenData.access_token}`
|
||||
Authorization: `${tokenType} ${accessToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `query { Viewer { id } }`
|
||||
})
|
||||
});
|
||||
|
||||
if (!userRes.ok) {
|
||||
return reply.status(401).send({ error: "Token de AniList inválido o expirado" });
|
||||
}
|
||||
|
||||
const userData = await userRes.json();
|
||||
const anilistUserId = userData?.data?.Viewer?.id;
|
||||
|
||||
if (!anilistUserId) {
|
||||
console.error("AniList Viewer error:", userData);
|
||||
return reply.status(500).send("Failed to fetch AniList user");
|
||||
return reply.status(500).send({ error: "No se pudo obtener el ID de usuario de AniList" });
|
||||
}
|
||||
|
||||
const expiresAt = new Date(
|
||||
Date.now() + tokenData.expires_in * 1000
|
||||
Date.now() + 365 * 24 * 60 * 60 * 1000
|
||||
).toISOString();
|
||||
|
||||
await run(
|
||||
@@ -71,19 +62,19 @@ async function anilist(fastify: FastifyInstance) {
|
||||
[
|
||||
userId,
|
||||
"AniList",
|
||||
tokenData.access_token,
|
||||
tokenData.refresh_token,
|
||||
tokenData.token_type,
|
||||
accessToken,
|
||||
"", // <- aquí
|
||||
tokenType,
|
||||
anilistUserId,
|
||||
expiresAt
|
||||
],
|
||||
"userdata"
|
||||
);
|
||||
|
||||
return reply.redirect("http://localhost:54322/?anilist=success");
|
||||
return reply.send({ ok: true, anilistUserId });
|
||||
} catch (e) {
|
||||
console.error("AniList error:", e);
|
||||
return reply.redirect("http://localhost:54322/?anilist=error");
|
||||
return reply.status(500).send({ error: "Error interno del servidor al guardar AniList" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -122,13 +122,16 @@ const DashboardApp = {
|
||||
headerBadge.title = `Connected as ${data.anilistUserId}`;
|
||||
}
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `Connected as ID: ${data.anilistUserId}`;
|
||||
statusEl.style.color = 'var(--color-success)';
|
||||
// CAMBIO: Mostrar fecha de expiración si existe
|
||||
const expiresDate = data.expiresAt ? new Date(data.expiresAt).toLocaleDateString() : 'Unknown';
|
||||
statusEl.innerHTML = `
|
||||
<span style="color:var(--color-success)">Connected as: <b>${data.anilistUserId}</b></span>
|
||||
<span style="display:block; font-size:0.75rem; color:#71717a">Expires: ${expiresDate}</span>
|
||||
`;
|
||||
}
|
||||
if (btn) {
|
||||
btn.textContent = 'Disconnect';
|
||||
btn.className = 'btn-stream-outline link-danger';
|
||||
|
||||
btn.onclick = () => this.disconnectAniList(userId);
|
||||
}
|
||||
} else {
|
||||
@@ -140,7 +143,7 @@ const DashboardApp = {
|
||||
if (btn) {
|
||||
btn.textContent = 'Connect';
|
||||
btn.className = 'btn-stream-outline';
|
||||
btn.onclick = () => this.redirectToAniListLogin();
|
||||
btn.onclick = () => this.openAniListModal();
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -154,6 +157,83 @@ const DashboardApp = {
|
||||
window.location.href = `https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirectUri}&state=${state}`;
|
||||
} catch (err) { console.error(err); alert('Error starting AniList login'); }
|
||||
},
|
||||
openAniListModal: function() {
|
||||
const modal = document.getElementById('anilist-connect-modal');
|
||||
const body = document.getElementById('anilist-modal-body');
|
||||
const clientId = 32898; // Tu Client ID
|
||||
|
||||
// Generamos el HTML del modal dinámicamente
|
||||
body.innerHTML = `
|
||||
<p class="modal-description">Connect your AniList account to sync your progress automatically.</p>
|
||||
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<label style="display:block; font-size:0.85rem; font-weight:600; color:#a1a1aa; margin-bottom:0.5rem">Step 1: Get Token</label>
|
||||
<a href="https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=token"
|
||||
target="_blank"
|
||||
class="btn-blur"
|
||||
style="width:100%; text-align:center; box-sizing:border-box; display:block;">
|
||||
Open AniList Login ↗
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Step 2: Paste Token</label>
|
||||
<input type="text" id="manual-anilist-token" class="stream-input" placeholder="Paste the long access token here..." autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="modal-footer" style="padding:0; background:transparent;">
|
||||
<button class="btn-primary" style="width:100%" onclick="DashboardApp.User.submitAniListToken()">Verify & Connect</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
},
|
||||
|
||||
closeAniListModal: function() {
|
||||
document.getElementById('anilist-connect-modal').classList.add('hidden');
|
||||
},
|
||||
|
||||
submitAniListToken: async function() {
|
||||
const tokenInput = document.getElementById('manual-anilist-token');
|
||||
const token = tokenInput.value.trim();
|
||||
const userId = DashboardApp.State.currentUserId;
|
||||
|
||||
if (!token) {
|
||||
alert('Please paste the AniList token first');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmBtn = document.querySelector('#anilist-connect-modal .btn-primary');
|
||||
const originalText = confirmBtn.textContent;
|
||||
confirmBtn.textContent = "Verifying...";
|
||||
confirmBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/anilist/store`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
userId: userId,
|
||||
accessToken: token
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) throw new Error(data.error || 'Failed to verify token');
|
||||
|
||||
this.closeAniListModal();
|
||||
await this.checkIntegrations(userId);
|
||||
alert('AniList connected successfully!');
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert(err.message || 'Invalid Token');
|
||||
} finally {
|
||||
confirmBtn.textContent = originalText;
|
||||
confirmBtn.disabled = false;
|
||||
}
|
||||
},
|
||||
|
||||
disconnectAniList: async function(userId) {
|
||||
if(!confirm("Disconnect AniList?")) return;
|
||||
|
||||
@@ -838,8 +838,7 @@ function openAniListModal(userId) {
|
||||
modalUserActions.classList.remove('active');
|
||||
modalEditUser.classList.remove('active');
|
||||
|
||||
aniListContent.innerHTML = `<div style="text-align: center; padding: 2rem;">Loading integration status...</div>`;
|
||||
|
||||
// Estado de carga inicial
|
||||
modalAniList.innerHTML = `
|
||||
<div class="modal-overlay"></div>
|
||||
<div class="modal-content">
|
||||
@@ -860,12 +859,15 @@ function openAniListModal(userId) {
|
||||
|
||||
modalAniList.classList.add('active');
|
||||
|
||||
// Verificar si ya está conectado
|
||||
getIntegrationStatus(userId).then(integration => {
|
||||
const content = document.getElementById('aniListContent');
|
||||
const clientId = 32898; // Tu Client ID de AniList
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="anilist-status">
|
||||
${integration.connected ? `
|
||||
if (integration.connected) {
|
||||
// VISTA: YA CONECTADO
|
||||
content.innerHTML = `
|
||||
<div class="anilist-status">
|
||||
<div class="anilist-connected">
|
||||
<div class="anilist-icon">
|
||||
<img src="https://anilist.co/img/icons/icon.svg" alt="AniList" style="width:40px; height:40px;">
|
||||
@@ -879,24 +881,43 @@ function openAniListModal(userId) {
|
||||
<button class="btn-disconnect" onclick="handleDisconnectAniList()">
|
||||
Disconnect AniList
|
||||
</button>
|
||||
` : `
|
||||
<div style="text-align: center; padding: 1rem;">
|
||||
<h3 style="margin-bottom: 0.5rem;">Connect with AniList</h3>
|
||||
<p style="color: var(--color-text-secondary); margin-bottom: 1.5rem;">
|
||||
Sync your anime list by logging in with AniList.
|
||||
</p>
|
||||
<div style="display:flex; justify-content:center;">
|
||||
<button class="btn-connect" onclick="redirectToAniListLogin()">
|
||||
Login with AniList
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// VISTA: NO CONECTADO (Formulario Manual)
|
||||
content.innerHTML = `
|
||||
<div class="anilist-status">
|
||||
<div style="text-align: left; padding: 0.5rem;">
|
||||
<h3 style="margin-bottom: 1rem; font-size: 1.1rem;">How to connect:</h3>
|
||||
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<p style="color: var(--color-text-secondary); margin-bottom: 0.5rem; font-size: 0.9rem;">
|
||||
1. Open the authorization page in a new tab:
|
||||
</p>
|
||||
<a href="https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=token"
|
||||
target="_blank"
|
||||
class="btn-secondary"
|
||||
style="display: inline-block; text-decoration: none; text-align: center; width: 100%; padding: 0.8rem;">
|
||||
Open AniList Login ↗
|
||||
</a>
|
||||
</div>
|
||||
<p style="font-size:0.85rem; margin-top:1rem; color:var(--color-text-secondary)">
|
||||
You will be redirected and then returned here.
|
||||
</p>
|
||||
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<p style="color: var(--color-text-secondary); margin-bottom: 0.5rem; font-size: 0.9rem;">
|
||||
2. Authorize the app, then copy the <b>Access Token</b> provided and paste it here:
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<input type="text" id="manualAniListToken" placeholder="Paste your Access Token here..." autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-connect" onclick="handleManualAniListToken()">
|
||||
Verify & Save Token
|
||||
</button>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
const content = document.getElementById('aniListContent');
|
||||
@@ -904,6 +925,49 @@ function openAniListModal(userId) {
|
||||
});
|
||||
}
|
||||
|
||||
// Nueva función para manejar el token pegado manualmente
|
||||
async function handleManualAniListToken() {
|
||||
const tokenInput = document.getElementById('manualAniListToken');
|
||||
const token = tokenInput.value.trim();
|
||||
|
||||
if (!token) {
|
||||
showUserToast('Please paste the AniList token first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const submitBtn = document.querySelector('.btn-connect');
|
||||
const originalText = submitBtn.textContent;
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Verifying...';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/anilist/store`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
userId: currentUserId,
|
||||
accessToken: token,
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Failed to verify token');
|
||||
}
|
||||
|
||||
showUserToast('AniList connected successfully!', 'success');
|
||||
// Recargar el modal para mostrar el estado "Conectado"
|
||||
openAniListModal(currentUserId);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showUserToast(err.message || 'Invalid Token', 'error');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
async function redirectToAniListLogin() {
|
||||
try {
|
||||
const res = await fetch(`/api/login`, {
|
||||
@@ -918,15 +982,10 @@ async function redirectToAniListLogin() {
|
||||
localStorage.setItem('token', data.token);
|
||||
|
||||
const clientId = 32898;
|
||||
const redirectUri = encodeURIComponent(window.location.origin + '/api/anilist');
|
||||
const state = encodeURIComponent(currentUserId);
|
||||
|
||||
window.location.href =
|
||||
`https://anilist.co/api/v2/oauth/authorize` +
|
||||
`?client_id=${clientId}` +
|
||||
`&response_type=code` +
|
||||
`&redirect_uri=${redirectUri}` +
|
||||
`&state=${state}`;
|
||||
window.open(
|
||||
`https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=token`,
|
||||
'_blank'
|
||||
);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -10,15 +10,29 @@
|
||||
.background-gradient {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at top, rgba(139, 92, 246, 0.15) 0%, transparent 60%),
|
||||
radial-gradient(ellipse at bottom right, rgba(59, 130, 246, 0.1) 0%, transparent 50%);
|
||||
background:
|
||||
radial-gradient(
|
||||
ellipse at top,
|
||||
rgba(139, 92, 246, 0.15) 0%,
|
||||
transparent 60%
|
||||
),
|
||||
radial-gradient(
|
||||
ellipse at bottom right,
|
||||
rgba(59, 130, 246, 0.1) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
z-index: 0;
|
||||
animation: gradientShift 10s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.content-container {
|
||||
@@ -80,9 +94,8 @@
|
||||
box-shadow: 0 20px 40px rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Badge de contraseña protegida */
|
||||
.user-card.has-password::after {
|
||||
content: '🔒';
|
||||
content: "🔒";
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
@@ -116,7 +129,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(139, 92, 246, 0.1) 0%, rgba(59, 130, 246, 0.05) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(139, 92, 246, 0.1) 0%,
|
||||
rgba(59, 130, 246, 0.05) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.user-avatar-placeholder svg {
|
||||
@@ -131,7 +148,12 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 1rem 1.5rem;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.7) 50%, transparent 100%);
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgba(0, 0, 0, 0.95) 0%,
|
||||
rgba(0, 0, 0, 0.7) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
@@ -168,7 +190,9 @@
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s, background 0.2s;
|
||||
transition:
|
||||
opacity 0.3s,
|
||||
background 0.2s;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
@@ -205,7 +229,6 @@
|
||||
box-shadow: 0 10px 30px var(--color-primary-glow);
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
@@ -438,7 +461,6 @@ input[type="file"] {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Estilos para modal de password */
|
||||
.password-modal-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
@@ -461,7 +483,6 @@ input[type="file"] {
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
/* ESTILOS PARA MODAL DE ACCIONES INDIVIDUALES */
|
||||
.manage-actions-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -673,10 +694,13 @@ input[type="file"] {
|
||||
border: 1px solid rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
@@ -720,22 +744,140 @@ input[type="file"] {
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.page-wrapper {
|
||||
padding: 1rem;
|
||||
|
||||
align-items: flex-start;
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.header-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2.5rem;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.users-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1.5rem;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.user-config-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.user-config-btn svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.btn-add-user {
|
||||
width: 100%;
|
||||
padding: 1.25rem;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.users-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.user-info {
|
||||
transform: translateY(0);
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgba(0, 0, 0, 0.95) 0%,
|
||||
rgba(0, 0, 0, 0.6) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.user-card:hover {
|
||||
transform: none;
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.user-card:active {
|
||||
transform: scale(0.97);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modal {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border-radius: 20px 20px 0 0;
|
||||
max-height: 85vh;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="password"] {
|
||||
padding: 1.2rem;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
padding: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.anilist-status a.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.anilist-status a.btn-secondary:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: white;
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
}
|
||||
|
||||
#manualAniListToken {
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -242,7 +242,17 @@
|
||||
<p>Update available: <span id="latestVersionDisplay">v1.x</span></p>
|
||||
<a id="downloadButton" href="https://git.waifuboard.app/ItsSkaiya/WaifuBoard/releases" target="_blank">Download</a>
|
||||
</div>
|
||||
<div id="anilist-connect-modal" class="custom-modal-overlay hidden">
|
||||
<div class="custom-modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>AniList Integration</h3>
|
||||
<button class="close-modal-btn" onclick="DashboardApp.User.closeAniListModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body" id="anilist-modal-body">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/src/scripts/updateNotifier.js"></script>
|
||||
<script src="/src/scripts/room-modal.js"></script>
|
||||
<script src="/src/scripts/rpc-inapp.js"></script>
|
||||
|
||||
@@ -2,58 +2,49 @@ import { FastifyInstance } from "fastify";
|
||||
import { run } from "../../shared/database";
|
||||
|
||||
async function anilist(fastify: FastifyInstance) {
|
||||
fastify.get("/anilist", async (request, reply) => {
|
||||
fastify.post("/anilist/store", async (request, reply) => {
|
||||
try {
|
||||
const { code, state } = request.query as { code?: string; state?: string };
|
||||
const {
|
||||
userId,
|
||||
accessToken,
|
||||
tokenType = "Bearer",
|
||||
expiresIn
|
||||
} = request.body as {
|
||||
userId: number;
|
||||
accessToken: string;
|
||||
tokenType?: string;
|
||||
expiresIn?: number;
|
||||
};
|
||||
|
||||
if (!code) return reply.status(400).send("No code");
|
||||
if (!state) return reply.status(400).send("No user state");
|
||||
|
||||
const userId = Number(state);
|
||||
if (!userId || isNaN(userId)) {
|
||||
return reply.status(400).send("Invalid user id");
|
||||
}
|
||||
|
||||
const tokenRes = await fetch("https://anilist.co/api/v2/oauth/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
grant_type: "authorization_code",
|
||||
client_id: process.env.ANILIST_CLIENT_ID,
|
||||
client_secret: process.env.ANILIST_CLIENT_SECRET,
|
||||
redirect_uri: "http://localhost:54322/api/anilist",
|
||||
code
|
||||
})
|
||||
});
|
||||
|
||||
const tokenData = await tokenRes.json();
|
||||
|
||||
if (!tokenData.access_token) {
|
||||
console.error("AniList token error:", tokenData);
|
||||
return reply.status(500).send("Failed to get AniList token");
|
||||
if (!userId || !accessToken) {
|
||||
return reply.status(400).send({ error: "Faltan datos (User ID o Token)" });
|
||||
}
|
||||
|
||||
// 1. Verificar que el token es válido consultando a AniList
|
||||
const userRes = await fetch("https://graphql.anilist.co", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `${tokenData.token_type} ${tokenData.access_token}`
|
||||
Authorization: `${tokenType} ${accessToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `query { Viewer { id } }`
|
||||
})
|
||||
});
|
||||
|
||||
if (!userRes.ok) {
|
||||
return reply.status(401).send({ error: "Token de AniList inválido o expirado" });
|
||||
}
|
||||
|
||||
const userData = await userRes.json();
|
||||
const anilistUserId = userData?.data?.Viewer?.id;
|
||||
|
||||
if (!anilistUserId) {
|
||||
console.error("AniList Viewer error:", userData);
|
||||
return reply.status(500).send("Failed to fetch AniList user");
|
||||
return reply.status(500).send({ error: "No se pudo obtener el ID de usuario de AniList" });
|
||||
}
|
||||
|
||||
const expiresAt = new Date(
|
||||
Date.now() + tokenData.expires_in * 1000
|
||||
Date.now() + 365 * 24 * 60 * 60 * 1000
|
||||
).toISOString();
|
||||
|
||||
await run(
|
||||
@@ -71,19 +62,19 @@ async function anilist(fastify: FastifyInstance) {
|
||||
[
|
||||
userId,
|
||||
"AniList",
|
||||
tokenData.access_token,
|
||||
tokenData.refresh_token,
|
||||
tokenData.token_type,
|
||||
accessToken,
|
||||
"", // <- aquí
|
||||
tokenType,
|
||||
anilistUserId,
|
||||
expiresAt
|
||||
],
|
||||
"userdata"
|
||||
);
|
||||
|
||||
return reply.redirect("http://localhost:54322/?anilist=success");
|
||||
return reply.send({ ok: true, anilistUserId });
|
||||
} catch (e) {
|
||||
console.error("AniList error:", e);
|
||||
return reply.redirect("http://localhost:54322/?anilist=error");
|
||||
return reply.status(500).send({ error: "Error interno del servidor al guardar AniList" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -122,13 +122,16 @@ const DashboardApp = {
|
||||
headerBadge.title = `Connected as ${data.anilistUserId}`;
|
||||
}
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `Connected as ID: ${data.anilistUserId}`;
|
||||
statusEl.style.color = 'var(--color-success)';
|
||||
// CAMBIO: Mostrar fecha de expiración si existe
|
||||
const expiresDate = data.expiresAt ? new Date(data.expiresAt).toLocaleDateString() : 'Unknown';
|
||||
statusEl.innerHTML = `
|
||||
<span style="color:var(--color-success)">Connected as: <b>${data.anilistUserId}</b></span>
|
||||
<span style="display:block; font-size:0.75rem; color:#71717a">Expires: ${expiresDate}</span>
|
||||
`;
|
||||
}
|
||||
if (btn) {
|
||||
btn.textContent = 'Disconnect';
|
||||
btn.className = 'btn-stream-outline link-danger';
|
||||
|
||||
btn.onclick = () => this.disconnectAniList(userId);
|
||||
}
|
||||
} else {
|
||||
@@ -140,7 +143,7 @@ const DashboardApp = {
|
||||
if (btn) {
|
||||
btn.textContent = 'Connect';
|
||||
btn.className = 'btn-stream-outline';
|
||||
btn.onclick = () => this.redirectToAniListLogin();
|
||||
btn.onclick = () => this.openAniListModal();
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -154,6 +157,83 @@ const DashboardApp = {
|
||||
window.location.href = `https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirectUri}&state=${state}`;
|
||||
} catch (err) { console.error(err); alert('Error starting AniList login'); }
|
||||
},
|
||||
openAniListModal: function() {
|
||||
const modal = document.getElementById('anilist-connect-modal');
|
||||
const body = document.getElementById('anilist-modal-body');
|
||||
const clientId = 32898; // Tu Client ID
|
||||
|
||||
// Generamos el HTML del modal dinámicamente
|
||||
body.innerHTML = `
|
||||
<p class="modal-description">Connect your AniList account to sync your progress automatically.</p>
|
||||
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<label style="display:block; font-size:0.85rem; font-weight:600; color:#a1a1aa; margin-bottom:0.5rem">Step 1: Get Token</label>
|
||||
<a href="https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=token"
|
||||
target="_blank"
|
||||
class="btn-blur"
|
||||
style="width:100%; text-align:center; box-sizing:border-box; display:block;">
|
||||
Open AniList Login ↗
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Step 2: Paste Token</label>
|
||||
<input type="text" id="manual-anilist-token" class="stream-input" placeholder="Paste the long access token here..." autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="modal-footer" style="padding:0; background:transparent;">
|
||||
<button class="btn-primary" style="width:100%" onclick="DashboardApp.User.submitAniListToken()">Verify & Connect</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
},
|
||||
|
||||
closeAniListModal: function() {
|
||||
document.getElementById('anilist-connect-modal').classList.add('hidden');
|
||||
},
|
||||
|
||||
submitAniListToken: async function() {
|
||||
const tokenInput = document.getElementById('manual-anilist-token');
|
||||
const token = tokenInput.value.trim();
|
||||
const userId = DashboardApp.State.currentUserId;
|
||||
|
||||
if (!token) {
|
||||
alert('Please paste the AniList token first');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmBtn = document.querySelector('#anilist-connect-modal .btn-primary');
|
||||
const originalText = confirmBtn.textContent;
|
||||
confirmBtn.textContent = "Verifying...";
|
||||
confirmBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/anilist/store`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
userId: userId,
|
||||
accessToken: token
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) throw new Error(data.error || 'Failed to verify token');
|
||||
|
||||
this.closeAniListModal();
|
||||
await this.checkIntegrations(userId);
|
||||
alert('AniList connected successfully!');
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert(err.message || 'Invalid Token');
|
||||
} finally {
|
||||
confirmBtn.textContent = originalText;
|
||||
confirmBtn.disabled = false;
|
||||
}
|
||||
},
|
||||
|
||||
disconnectAniList: async function(userId) {
|
||||
if(!confirm("Disconnect AniList?")) return;
|
||||
|
||||
@@ -838,8 +838,7 @@ function openAniListModal(userId) {
|
||||
modalUserActions.classList.remove('active');
|
||||
modalEditUser.classList.remove('active');
|
||||
|
||||
aniListContent.innerHTML = `<div style="text-align: center; padding: 2rem;">Loading integration status...</div>`;
|
||||
|
||||
// Estado de carga inicial
|
||||
modalAniList.innerHTML = `
|
||||
<div class="modal-overlay"></div>
|
||||
<div class="modal-content">
|
||||
@@ -860,12 +859,15 @@ function openAniListModal(userId) {
|
||||
|
||||
modalAniList.classList.add('active');
|
||||
|
||||
// Verificar si ya está conectado
|
||||
getIntegrationStatus(userId).then(integration => {
|
||||
const content = document.getElementById('aniListContent');
|
||||
const clientId = 32898; // Tu Client ID de AniList
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="anilist-status">
|
||||
${integration.connected ? `
|
||||
if (integration.connected) {
|
||||
// VISTA: YA CONECTADO
|
||||
content.innerHTML = `
|
||||
<div class="anilist-status">
|
||||
<div class="anilist-connected">
|
||||
<div class="anilist-icon">
|
||||
<img src="https://anilist.co/img/icons/icon.svg" alt="AniList" style="width:40px; height:40px;">
|
||||
@@ -879,24 +881,43 @@ function openAniListModal(userId) {
|
||||
<button class="btn-disconnect" onclick="handleDisconnectAniList()">
|
||||
Disconnect AniList
|
||||
</button>
|
||||
` : `
|
||||
<div style="text-align: center; padding: 1rem;">
|
||||
<h3 style="margin-bottom: 0.5rem;">Connect with AniList</h3>
|
||||
<p style="color: var(--color-text-secondary); margin-bottom: 1.5rem;">
|
||||
Sync your anime list by logging in with AniList.
|
||||
</p>
|
||||
<div style="display:flex; justify-content:center;">
|
||||
<button class="btn-connect" onclick="redirectToAniListLogin()">
|
||||
Login with AniList
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// VISTA: NO CONECTADO (Formulario Manual)
|
||||
content.innerHTML = `
|
||||
<div class="anilist-status">
|
||||
<div style="text-align: left; padding: 0.5rem;">
|
||||
<h3 style="margin-bottom: 1rem; font-size: 1.1rem;">How to connect:</h3>
|
||||
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<p style="color: var(--color-text-secondary); margin-bottom: 0.5rem; font-size: 0.9rem;">
|
||||
1. Open the authorization page in a new tab:
|
||||
</p>
|
||||
<a href="https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=token"
|
||||
target="_blank"
|
||||
class="btn-secondary"
|
||||
style="display: inline-block; text-decoration: none; text-align: center; width: 100%; padding: 0.8rem;">
|
||||
Open AniList Login ↗
|
||||
</a>
|
||||
</div>
|
||||
<p style="font-size:0.85rem; margin-top:1rem; color:var(--color-text-secondary)">
|
||||
You will be redirected and then returned here.
|
||||
</p>
|
||||
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<p style="color: var(--color-text-secondary); margin-bottom: 0.5rem; font-size: 0.9rem;">
|
||||
2. Authorize the app, then copy the <b>Access Token</b> provided and paste it here:
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<input type="text" id="manualAniListToken" placeholder="Paste your Access Token here..." autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-connect" onclick="handleManualAniListToken()">
|
||||
Verify & Save Token
|
||||
</button>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
const content = document.getElementById('aniListContent');
|
||||
@@ -904,6 +925,49 @@ function openAniListModal(userId) {
|
||||
});
|
||||
}
|
||||
|
||||
// Nueva función para manejar el token pegado manualmente
|
||||
async function handleManualAniListToken() {
|
||||
const tokenInput = document.getElementById('manualAniListToken');
|
||||
const token = tokenInput.value.trim();
|
||||
|
||||
if (!token) {
|
||||
showUserToast('Please paste the AniList token first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const submitBtn = document.querySelector('.btn-connect');
|
||||
const originalText = submitBtn.textContent;
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Verifying...';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/anilist/store`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
userId: currentUserId,
|
||||
accessToken: token,
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Failed to verify token');
|
||||
}
|
||||
|
||||
showUserToast('AniList connected successfully!', 'success');
|
||||
// Recargar el modal para mostrar el estado "Conectado"
|
||||
openAniListModal(currentUserId);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showUserToast(err.message || 'Invalid Token', 'error');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
async function redirectToAniListLogin() {
|
||||
try {
|
||||
const res = await fetch(`/api/login`, {
|
||||
@@ -918,15 +982,10 @@ async function redirectToAniListLogin() {
|
||||
localStorage.setItem('token', data.token);
|
||||
|
||||
const clientId = 32898;
|
||||
const redirectUri = encodeURIComponent(window.location.origin + '/api/anilist');
|
||||
const state = encodeURIComponent(currentUserId);
|
||||
|
||||
window.location.href =
|
||||
`https://anilist.co/api/v2/oauth/authorize` +
|
||||
`?client_id=${clientId}` +
|
||||
`&response_type=code` +
|
||||
`&redirect_uri=${redirectUri}` +
|
||||
`&state=${state}`;
|
||||
window.open(
|
||||
`https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=token`,
|
||||
'_blank'
|
||||
);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -862,3 +862,22 @@ input[type="file"] {
|
||||
padding: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.anilist-status a.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.anilist-status a.btn-secondary:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: white;
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
}
|
||||
|
||||
#manualAniListToken {
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -229,7 +229,17 @@
|
||||
<p>Update available: <span id="latestVersionDisplay">v1.x</span></p>
|
||||
<a id="downloadButton" href="https://git.waifuboard.app/ItsSkaiya/WaifuBoard/releases" target="_blank">Download</a>
|
||||
</div>
|
||||
<div id="anilist-connect-modal" class="custom-modal-overlay hidden">
|
||||
<div class="custom-modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>AniList Integration</h3>
|
||||
<button class="close-modal-btn" onclick="DashboardApp.User.closeAniListModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body" id="anilist-modal-body">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/src/scripts/updateNotifier.js"></script>
|
||||
<script src="/src/scripts/room-modal.js"></script>
|
||||
<script src="/src/scripts/rpc-inapp.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user