changed anilist login flow
This commit is contained in:
@@ -2,58 +2,49 @@ import { FastifyInstance } from "fastify";
|
|||||||
import { run } from "../../shared/database";
|
import { run } from "../../shared/database";
|
||||||
|
|
||||||
async function anilist(fastify: FastifyInstance) {
|
async function anilist(fastify: FastifyInstance) {
|
||||||
fastify.get("/anilist", async (request, reply) => {
|
fastify.post("/anilist/store", async (request, reply) => {
|
||||||
try {
|
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 (!userId || !accessToken) {
|
||||||
if (!state) return reply.status(400).send("No user state");
|
return reply.status(400).send({ error: "Faltan datos (User ID o Token)" });
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. Verificar que el token es válido consultando a AniList
|
||||||
const userRes = await fetch("https://graphql.anilist.co", {
|
const userRes = await fetch("https://graphql.anilist.co", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `${tokenData.token_type} ${tokenData.access_token}`
|
Authorization: `${tokenType} ${accessToken}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
query: `query { Viewer { id } }`
|
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 userData = await userRes.json();
|
||||||
const anilistUserId = userData?.data?.Viewer?.id;
|
const anilistUserId = userData?.data?.Viewer?.id;
|
||||||
|
|
||||||
if (!anilistUserId) {
|
if (!anilistUserId) {
|
||||||
console.error("AniList Viewer error:", userData);
|
return reply.status(500).send({ error: "No se pudo obtener el ID de usuario de AniList" });
|
||||||
return reply.status(500).send("Failed to fetch AniList user");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const expiresAt = new Date(
|
const expiresAt = new Date(
|
||||||
Date.now() + tokenData.expires_in * 1000
|
Date.now() + 365 * 24 * 60 * 60 * 1000
|
||||||
).toISOString();
|
).toISOString();
|
||||||
|
|
||||||
await run(
|
await run(
|
||||||
@@ -71,19 +62,19 @@ async function anilist(fastify: FastifyInstance) {
|
|||||||
[
|
[
|
||||||
userId,
|
userId,
|
||||||
"AniList",
|
"AniList",
|
||||||
tokenData.access_token,
|
accessToken,
|
||||||
tokenData.refresh_token,
|
"", // <- aquí
|
||||||
tokenData.token_type,
|
tokenType,
|
||||||
anilistUserId,
|
anilistUserId,
|
||||||
expiresAt
|
expiresAt
|
||||||
],
|
],
|
||||||
"userdata"
|
"userdata"
|
||||||
);
|
);
|
||||||
|
|
||||||
return reply.redirect("http://localhost:54322/?anilist=success");
|
return reply.send({ ok: true, anilistUserId });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("AniList error:", 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}`;
|
headerBadge.title = `Connected as ${data.anilistUserId}`;
|
||||||
}
|
}
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
statusEl.textContent = `Connected as ID: ${data.anilistUserId}`;
|
// CAMBIO: Mostrar fecha de expiración si existe
|
||||||
statusEl.style.color = 'var(--color-success)';
|
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) {
|
if (btn) {
|
||||||
btn.textContent = 'Disconnect';
|
btn.textContent = 'Disconnect';
|
||||||
btn.className = 'btn-stream-outline link-danger';
|
btn.className = 'btn-stream-outline link-danger';
|
||||||
|
|
||||||
btn.onclick = () => this.disconnectAniList(userId);
|
btn.onclick = () => this.disconnectAniList(userId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -140,7 +143,7 @@ const DashboardApp = {
|
|||||||
if (btn) {
|
if (btn) {
|
||||||
btn.textContent = 'Connect';
|
btn.textContent = 'Connect';
|
||||||
btn.className = 'btn-stream-outline';
|
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}`;
|
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'); }
|
} 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) {
|
disconnectAniList: async function(userId) {
|
||||||
if(!confirm("Disconnect AniList?")) return;
|
if(!confirm("Disconnect AniList?")) return;
|
||||||
|
|||||||
@@ -838,8 +838,7 @@ function openAniListModal(userId) {
|
|||||||
modalUserActions.classList.remove('active');
|
modalUserActions.classList.remove('active');
|
||||||
modalEditUser.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 = `
|
modalAniList.innerHTML = `
|
||||||
<div class="modal-overlay"></div>
|
<div class="modal-overlay"></div>
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -860,12 +859,15 @@ function openAniListModal(userId) {
|
|||||||
|
|
||||||
modalAniList.classList.add('active');
|
modalAniList.classList.add('active');
|
||||||
|
|
||||||
|
// Verificar si ya está conectado
|
||||||
getIntegrationStatus(userId).then(integration => {
|
getIntegrationStatus(userId).then(integration => {
|
||||||
const content = document.getElementById('aniListContent');
|
const content = document.getElementById('aniListContent');
|
||||||
|
const clientId = 32898; // Tu Client ID de AniList
|
||||||
|
|
||||||
content.innerHTML = `
|
if (integration.connected) {
|
||||||
<div class="anilist-status">
|
// VISTA: YA CONECTADO
|
||||||
${integration.connected ? `
|
content.innerHTML = `
|
||||||
|
<div class="anilist-status">
|
||||||
<div class="anilist-connected">
|
<div class="anilist-connected">
|
||||||
<div class="anilist-icon">
|
<div class="anilist-icon">
|
||||||
<img src="https://anilist.co/img/icons/icon.svg" alt="AniList" style="width:40px; height:40px;">
|
<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()">
|
<button class="btn-disconnect" onclick="handleDisconnectAniList()">
|
||||||
Disconnect AniList
|
Disconnect AniList
|
||||||
</button>
|
</button>
|
||||||
` : `
|
</div>
|
||||||
<div style="text-align: center; padding: 1rem;">
|
`;
|
||||||
<h3 style="margin-bottom: 0.5rem;">Connect with AniList</h3>
|
} else {
|
||||||
<p style="color: var(--color-text-secondary); margin-bottom: 1.5rem;">
|
// VISTA: NO CONECTADO (Formulario Manual)
|
||||||
Sync your anime list by logging in with AniList.
|
content.innerHTML = `
|
||||||
</p>
|
<div class="anilist-status">
|
||||||
<div style="display:flex; justify-content:center;">
|
<div style="text-align: left; padding: 0.5rem;">
|
||||||
<button class="btn-connect" onclick="redirectToAniListLogin()">
|
<h3 style="margin-bottom: 1rem; font-size: 1.1rem;">How to connect:</h3>
|
||||||
Login with AniList
|
|
||||||
</button>
|
<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>
|
</div>
|
||||||
<p style="font-size:0.85rem; margin-top:1rem; color:var(--color-text-secondary)">
|
|
||||||
You will be redirected and then returned here.
|
<div style="margin-bottom: 1.5rem;">
|
||||||
</p>
|
<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>
|
||||||
</div>
|
`;
|
||||||
`;
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
const content = document.getElementById('aniListContent');
|
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() {
|
async function redirectToAniListLogin() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/login`, {
|
const res = await fetch(`/api/login`, {
|
||||||
@@ -918,15 +982,10 @@ async function redirectToAniListLogin() {
|
|||||||
localStorage.setItem('token', data.token);
|
localStorage.setItem('token', data.token);
|
||||||
|
|
||||||
const clientId = 32898;
|
const clientId = 32898;
|
||||||
const redirectUri = encodeURIComponent(window.location.origin + '/api/anilist');
|
window.open(
|
||||||
const state = encodeURIComponent(currentUserId);
|
`https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=token`,
|
||||||
|
'_blank'
|
||||||
window.location.href =
|
);
|
||||||
`https://anilist.co/api/v2/oauth/authorize` +
|
|
||||||
`?client_id=${clientId}` +
|
|
||||||
`&response_type=code` +
|
|
||||||
`&redirect_uri=${redirectUri}` +
|
|
||||||
`&state=${state}`;
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@@ -10,15 +10,29 @@
|
|||||||
.background-gradient {
|
.background-gradient {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: radial-gradient(ellipse at top, rgba(139, 92, 246, 0.15) 0%, transparent 60%),
|
background:
|
||||||
radial-gradient(ellipse at bottom right, rgba(59, 130, 246, 0.1) 0%, transparent 50%);
|
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;
|
z-index: 0;
|
||||||
animation: gradientShift 10s ease infinite;
|
animation: gradientShift 10s ease infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes gradientShift {
|
@keyframes gradientShift {
|
||||||
0%, 100% { opacity: 1; }
|
0%,
|
||||||
50% { opacity: 0.7; }
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-container {
|
.content-container {
|
||||||
@@ -80,9 +94,8 @@
|
|||||||
box-shadow: 0 20px 40px rgba(139, 92, 246, 0.3);
|
box-shadow: 0 20px 40px rgba(139, 92, 246, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Badge de contraseña protegida */
|
|
||||||
.user-card.has-password::after {
|
.user-card.has-password::after {
|
||||||
content: '🔒';
|
content: "🔒";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
@@ -116,7 +129,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: 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 {
|
.user-avatar-placeholder svg {
|
||||||
@@ -131,7 +148,12 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding: 1rem 1.5rem;
|
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%);
|
transform: translateY(100%);
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
}
|
}
|
||||||
@@ -168,7 +190,9 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s, background 0.2s;
|
transition:
|
||||||
|
opacity 0.3s,
|
||||||
|
background 0.2s;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +229,6 @@
|
|||||||
box-shadow: 0 10px 30px var(--color-primary-glow);
|
box-shadow: 0 10px 30px var(--color-primary-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal Styles */
|
|
||||||
.modal {
|
.modal {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -438,7 +461,6 @@ input[type="file"] {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Estilos para modal de password */
|
|
||||||
.password-modal-content {
|
.password-modal-content {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
@@ -461,7 +483,6 @@ input[type="file"] {
|
|||||||
margin-top: 0.1rem;
|
margin-top: 0.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ESTILOS PARA MODAL DE ACCIONES INDIVIDUALES */
|
|
||||||
.manage-actions-modal {
|
.manage-actions-modal {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -673,10 +694,13 @@ input[type="file"] {
|
|||||||
border: 1px solid rgba(59, 130, 246, 0.4);
|
border: 1px solid rgba(59, 130, 246, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animations */
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; }
|
from {
|
||||||
to { opacity: 1; }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeInDown {
|
@keyframes fadeInDown {
|
||||||
@@ -720,22 +744,140 @@ input[type="file"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% { background-position: 200% 0; }
|
0% {
|
||||||
100% { background-position: -200% 0; }
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.page-wrapper {
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
font-size: 2.5rem;
|
font-size: 2.2rem;
|
||||||
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.users-grid {
|
.users-grid {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 1.5rem;
|
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 {
|
.modal-content {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 20px 20px 0 0;
|
||||||
|
max-height: 85vh;
|
||||||
padding: 1.5rem;
|
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>
|
<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>
|
<a id="downloadButton" href="https://git.waifuboard.app/ItsSkaiya/WaifuBoard/releases" target="_blank">Download</a>
|
||||||
</div>
|
</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/updateNotifier.js"></script>
|
||||||
<script src="/src/scripts/room-modal.js"></script>
|
<script src="/src/scripts/room-modal.js"></script>
|
||||||
<script src="/src/scripts/rpc-inapp.js"></script>
|
<script src="/src/scripts/rpc-inapp.js"></script>
|
||||||
|
|||||||
@@ -2,58 +2,49 @@ import { FastifyInstance } from "fastify";
|
|||||||
import { run } from "../../shared/database";
|
import { run } from "../../shared/database";
|
||||||
|
|
||||||
async function anilist(fastify: FastifyInstance) {
|
async function anilist(fastify: FastifyInstance) {
|
||||||
fastify.get("/anilist", async (request, reply) => {
|
fastify.post("/anilist/store", async (request, reply) => {
|
||||||
try {
|
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 (!userId || !accessToken) {
|
||||||
if (!state) return reply.status(400).send("No user state");
|
return reply.status(400).send({ error: "Faltan datos (User ID o Token)" });
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. Verificar que el token es válido consultando a AniList
|
||||||
const userRes = await fetch("https://graphql.anilist.co", {
|
const userRes = await fetch("https://graphql.anilist.co", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `${tokenData.token_type} ${tokenData.access_token}`
|
Authorization: `${tokenType} ${accessToken}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
query: `query { Viewer { id } }`
|
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 userData = await userRes.json();
|
||||||
const anilistUserId = userData?.data?.Viewer?.id;
|
const anilistUserId = userData?.data?.Viewer?.id;
|
||||||
|
|
||||||
if (!anilistUserId) {
|
if (!anilistUserId) {
|
||||||
console.error("AniList Viewer error:", userData);
|
return reply.status(500).send({ error: "No se pudo obtener el ID de usuario de AniList" });
|
||||||
return reply.status(500).send("Failed to fetch AniList user");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const expiresAt = new Date(
|
const expiresAt = new Date(
|
||||||
Date.now() + tokenData.expires_in * 1000
|
Date.now() + 365 * 24 * 60 * 60 * 1000
|
||||||
).toISOString();
|
).toISOString();
|
||||||
|
|
||||||
await run(
|
await run(
|
||||||
@@ -71,19 +62,19 @@ async function anilist(fastify: FastifyInstance) {
|
|||||||
[
|
[
|
||||||
userId,
|
userId,
|
||||||
"AniList",
|
"AniList",
|
||||||
tokenData.access_token,
|
accessToken,
|
||||||
tokenData.refresh_token,
|
"", // <- aquí
|
||||||
tokenData.token_type,
|
tokenType,
|
||||||
anilistUserId,
|
anilistUserId,
|
||||||
expiresAt
|
expiresAt
|
||||||
],
|
],
|
||||||
"userdata"
|
"userdata"
|
||||||
);
|
);
|
||||||
|
|
||||||
return reply.redirect("http://localhost:54322/?anilist=success");
|
return reply.send({ ok: true, anilistUserId });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("AniList error:", 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}`;
|
headerBadge.title = `Connected as ${data.anilistUserId}`;
|
||||||
}
|
}
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
statusEl.textContent = `Connected as ID: ${data.anilistUserId}`;
|
// CAMBIO: Mostrar fecha de expiración si existe
|
||||||
statusEl.style.color = 'var(--color-success)';
|
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) {
|
if (btn) {
|
||||||
btn.textContent = 'Disconnect';
|
btn.textContent = 'Disconnect';
|
||||||
btn.className = 'btn-stream-outline link-danger';
|
btn.className = 'btn-stream-outline link-danger';
|
||||||
|
|
||||||
btn.onclick = () => this.disconnectAniList(userId);
|
btn.onclick = () => this.disconnectAniList(userId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -140,7 +143,7 @@ const DashboardApp = {
|
|||||||
if (btn) {
|
if (btn) {
|
||||||
btn.textContent = 'Connect';
|
btn.textContent = 'Connect';
|
||||||
btn.className = 'btn-stream-outline';
|
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}`;
|
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'); }
|
} 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) {
|
disconnectAniList: async function(userId) {
|
||||||
if(!confirm("Disconnect AniList?")) return;
|
if(!confirm("Disconnect AniList?")) return;
|
||||||
|
|||||||
@@ -838,8 +838,7 @@ function openAniListModal(userId) {
|
|||||||
modalUserActions.classList.remove('active');
|
modalUserActions.classList.remove('active');
|
||||||
modalEditUser.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 = `
|
modalAniList.innerHTML = `
|
||||||
<div class="modal-overlay"></div>
|
<div class="modal-overlay"></div>
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -860,12 +859,15 @@ function openAniListModal(userId) {
|
|||||||
|
|
||||||
modalAniList.classList.add('active');
|
modalAniList.classList.add('active');
|
||||||
|
|
||||||
|
// Verificar si ya está conectado
|
||||||
getIntegrationStatus(userId).then(integration => {
|
getIntegrationStatus(userId).then(integration => {
|
||||||
const content = document.getElementById('aniListContent');
|
const content = document.getElementById('aniListContent');
|
||||||
|
const clientId = 32898; // Tu Client ID de AniList
|
||||||
|
|
||||||
content.innerHTML = `
|
if (integration.connected) {
|
||||||
<div class="anilist-status">
|
// VISTA: YA CONECTADO
|
||||||
${integration.connected ? `
|
content.innerHTML = `
|
||||||
|
<div class="anilist-status">
|
||||||
<div class="anilist-connected">
|
<div class="anilist-connected">
|
||||||
<div class="anilist-icon">
|
<div class="anilist-icon">
|
||||||
<img src="https://anilist.co/img/icons/icon.svg" alt="AniList" style="width:40px; height:40px;">
|
<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()">
|
<button class="btn-disconnect" onclick="handleDisconnectAniList()">
|
||||||
Disconnect AniList
|
Disconnect AniList
|
||||||
</button>
|
</button>
|
||||||
` : `
|
</div>
|
||||||
<div style="text-align: center; padding: 1rem;">
|
`;
|
||||||
<h3 style="margin-bottom: 0.5rem;">Connect with AniList</h3>
|
} else {
|
||||||
<p style="color: var(--color-text-secondary); margin-bottom: 1.5rem;">
|
// VISTA: NO CONECTADO (Formulario Manual)
|
||||||
Sync your anime list by logging in with AniList.
|
content.innerHTML = `
|
||||||
</p>
|
<div class="anilist-status">
|
||||||
<div style="display:flex; justify-content:center;">
|
<div style="text-align: left; padding: 0.5rem;">
|
||||||
<button class="btn-connect" onclick="redirectToAniListLogin()">
|
<h3 style="margin-bottom: 1rem; font-size: 1.1rem;">How to connect:</h3>
|
||||||
Login with AniList
|
|
||||||
</button>
|
<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>
|
</div>
|
||||||
<p style="font-size:0.85rem; margin-top:1rem; color:var(--color-text-secondary)">
|
|
||||||
You will be redirected and then returned here.
|
<div style="margin-bottom: 1.5rem;">
|
||||||
</p>
|
<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>
|
||||||
</div>
|
`;
|
||||||
`;
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
const content = document.getElementById('aniListContent');
|
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() {
|
async function redirectToAniListLogin() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/login`, {
|
const res = await fetch(`/api/login`, {
|
||||||
@@ -918,15 +982,10 @@ async function redirectToAniListLogin() {
|
|||||||
localStorage.setItem('token', data.token);
|
localStorage.setItem('token', data.token);
|
||||||
|
|
||||||
const clientId = 32898;
|
const clientId = 32898;
|
||||||
const redirectUri = encodeURIComponent(window.location.origin + '/api/anilist');
|
window.open(
|
||||||
const state = encodeURIComponent(currentUserId);
|
`https://anilist.co/api/v2/oauth/authorize?client_id=${clientId}&response_type=token`,
|
||||||
|
'_blank'
|
||||||
window.location.href =
|
);
|
||||||
`https://anilist.co/api/v2/oauth/authorize` +
|
|
||||||
`?client_id=${clientId}` +
|
|
||||||
`&response_type=code` +
|
|
||||||
`&redirect_uri=${redirectUri}` +
|
|
||||||
`&state=${state}`;
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@@ -862,3 +862,22 @@ input[type="file"] {
|
|||||||
padding: 1.2rem;
|
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>
|
<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>
|
<a id="downloadButton" href="https://git.waifuboard.app/ItsSkaiya/WaifuBoard/releases" target="_blank">Download</a>
|
||||||
</div>
|
</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/updateNotifier.js"></script>
|
||||||
<script src="/src/scripts/room-modal.js"></script>
|
<script src="/src/scripts/room-modal.js"></script>
|
||||||
<script src="/src/scripts/rpc-inapp.js"></script>
|
<script src="/src/scripts/rpc-inapp.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user