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);
|
||||
|
||||
@@ -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