gallery now supports multiple users

This commit is contained in:
2025-12-06 02:07:32 +01:00
parent 2df7625657
commit e7388bbb25
6 changed files with 97 additions and 51 deletions

View File

@@ -29,6 +29,7 @@ fastify.addHook("preHandler", async (request) => {
const token = auth.replace("Bearer ", ""); const token = auth.replace("Bearer ", "");
request.user = jwt.verify(token, process.env.JWT_SECRET); request.user = jwt.verify(token, process.env.JWT_SECRET);
} catch (e) { } catch (e) {
return reply.code(401).send({ error: "Invalid token" });
} }
}); });

View File

@@ -59,22 +59,25 @@ export async function getInfo(req: any, reply: FastifyReply) {
} }
} }
export async function getFavorites(req: FastifyRequest, reply: FastifyReply) { export async function getFavorites(req: any, reply: FastifyReply) {
try { try {
const favorites = await galleryService.getFavorites(); if (!req.user) return reply.code(401).send({ error: "Unauthorized" });
const favorites = await galleryService.getFavorites(req.user.id);
return { favorites }; return { favorites };
} catch (err) { } catch (err) {
const error = err as Error; console.error("Get Favorites Error:", (err as Error).message);
console.error("Get Favorites Error:", error.message);
return reply.code(500).send({ error: "Failed to retrieve favorites" }); return reply.code(500).send({ error: "Failed to retrieve favorites" });
} }
} }
export async function getFavoriteById(req: FastifyRequest, reply: FastifyReply) { export async function getFavoriteById(req: any, reply: FastifyReply) {
try { try {
if (!req.user) return reply.code(401).send({ error: "Unauthorized" });
const { id } = req.params as { id: string }; const { id } = req.params as { id: string };
const favorite = await galleryService.getFavoriteById(id); const favorite = await galleryService.getFavoriteById(id, req.user.id);
if (!favorite) { if (!favorite) {
return reply.code(404).send({ error: "Favorite not found" }); return reply.code(404).send({ error: "Favorite not found" });
@@ -83,25 +86,26 @@ export async function getFavoriteById(req: FastifyRequest, reply: FastifyReply)
return { favorite }; return { favorite };
} catch (err) { } catch (err) {
const error = err as Error; console.error("Get Favorite By ID Error:", (err as Error).message);
console.error("Get Favorite By ID Error:", error.message);
return reply.code(500).send({ error: "Failed to retrieve favorite" }); return reply.code(500).send({ error: "Failed to retrieve favorite" });
} }
} }
export async function addFavorite(req: any, reply: FastifyReply) {
export async function addFavorite(req: FastifyRequest<{ Body: AddFavoriteBody }>, reply: FastifyReply) {
try { try {
if (!req.user) return reply.code(401).send({ error: "Unauthorized" });
const { id, title, image_url, thumbnail_url, tags, provider, headers } = req.body; const { id, title, image_url, thumbnail_url, tags, provider, headers } = req.body;
if (!id || !title || !image_url || !thumbnail_url) { if (!id || !title || !image_url || !thumbnail_url) {
return reply.code(400).send({ return reply.code(400).send({
error: "Missing required fields: id, title, image_url, thumbnail_url" error: "Missing required fields"
}); });
} }
const result = await galleryService.addFavorite({ const result = await galleryService.addFavorite({
id, id,
user_id: req.user.id,
title, title,
image_url, image_url,
thumbnail_url, thumbnail_url,
@@ -122,15 +126,13 @@ export async function addFavorite(req: FastifyRequest<{ Body: AddFavoriteBody }>
} }
} }
export async function removeFavorite(req: FastifyRequest<{ Params: RemoveFavoriteParams }>, reply: FastifyReply) { export async function removeFavorite(req: any, reply: FastifyReply) {
try { try {
if (!req.user) return reply.code(401).send({ error: "Unauthorized" });
const { id } = req.params; const { id } = req.params;
if (!id) { const result = await galleryService.removeFavorite(id, req.user.id);
return reply.code(400).send({ error: "Missing favorite ID" });
}
const result = await galleryService.removeFavorite(id);
if (result.success) { if (result.success) {
return { success: true, message: "Favorite removed successfully" }; return { success: true, message: "Favorite removed successfully" };
@@ -138,8 +140,7 @@ export async function removeFavorite(req: FastifyRequest<{ Params: RemoveFavorit
return reply.code(404).send({ error: "Favorite not found" }); return reply.code(404).send({ error: "Favorite not found" });
} }
} catch (err) { } catch (err) {
const error = err as Error; console.error("Remove Favorite Error:", (err as Error).message);
console.error("Remove Favorite Error:", error.message);
return reply.code(500).send({ error: "Failed to remove favorite" }); return reply.code(500).send({ error: "Failed to remove favorite" });
} }
} }

View File

@@ -102,28 +102,32 @@ export async function searchInExtension(providerName: string, query: string, pag
} }
} }
export async function getFavorites(): Promise<Favorite[]> { export async function getFavorites(userId: number): Promise<Favorite[]> {
const db = getDatabase("favorites"); const db = getDatabase("favorites");
return new Promise((resolve) => { return new Promise((resolve) => {
db.all('SELECT * FROM favorites', [], (err: Error | null, rows: Favorite[]) => { db.all(
'SELECT * FROM favorites WHERE user_id = ?',
[userId],
(err: Error | null, rows: Favorite[]) => {
if (err) { if (err) {
console.error('Error getting favorites:', err.message); console.error('Error getting favorites:', err.message);
resolve([]); resolve([]);
} else { } else {
resolve(rows); resolve(rows);
} }
}); }
);
}); });
} }
export async function getFavoriteById(id: string): Promise<Favorite | null> { export async function getFavoriteById(id: string, userId: number): Promise<Favorite | null> {
const db = getDatabase("favorites"); const db = getDatabase("favorites");
return new Promise((resolve) => { return new Promise((resolve) => {
db.get( db.get(
'SELECT * FROM favorites WHERE id = ?', 'SELECT * FROM favorites WHERE id = ? AND user_id = ?',
[id], [id, userId],
(err: Error | null, row: Favorite | undefined) => { (err: Error | null, row: Favorite | undefined) => {
if (err) { if (err) {
console.error('Error getting favorite by id:', err.message); console.error('Error getting favorite by id:', err.message);
@@ -136,19 +140,20 @@ export async function getFavoriteById(id: string): Promise<Favorite | null> {
}); });
} }
export async function addFavorite(fav: Favorite): Promise<FavoriteResult> { export async function addFavorite(fav: Favorite & { user_id: number }): Promise<FavoriteResult> {
const db = getDatabase("favorites"); const db = getDatabase("favorites");
return new Promise((resolve) => { return new Promise((resolve) => {
const stmt = ` const stmt = `
INSERT INTO favorites (id, title, image_url, thumbnail_url, tags, headers, provider) INSERT INTO favorites (id, user_id, title, image_url, thumbnail_url, tags, headers, provider)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`; `;
db.run( db.run(
stmt, stmt,
[ [
fav.id, fav.id,
fav.user_id,
fav.title, fav.title,
fav.image_url, fav.image_url,
fav.thumbnail_url, fav.thumbnail_url,
@@ -172,18 +177,18 @@ export async function addFavorite(fav: Favorite): Promise<FavoriteResult> {
}); });
} }
export async function removeFavorite(id: string): Promise<FavoriteResult> { export async function removeFavorite(id: string, userId: number): Promise<FavoriteResult> {
const db = getDatabase("favorites"); const db = getDatabase("favorites");
return new Promise((resolve) => { return new Promise((resolve) => {
const stmt = 'DELETE FROM favorites WHERE id = ?'; const stmt = 'DELETE FROM favorites WHERE id = ? AND user_id = ?';
db.run(stmt, [id], function (err: Error | null) { db.run(stmt, [id, userId], function (err: Error | null) {
if (err) { if (err) {
console.error('Error removing favorite:', err.message); console.error('Error removing favorite:', err.message);
resolve({ success: false, error: err.message }); resolve({ success: false, error: err.message });
} else { } else {
// @ts-ignore - this.changes existe en el contexto de db.run // @ts-ignore
resolve({ success: this.changes > 0 }); resolve({ success: this.changes > 0 });
} }
}); });

View File

@@ -19,6 +19,13 @@ sentinel.style.marginBottom = '300px';
sentinel.style.display = 'none'; sentinel.style.display = 'none';
resultsContainer.parentNode.appendChild(sentinel); resultsContainer.parentNode.appendChild(sentinel);
function getAuthHeaders(extra = {}) {
const token = localStorage.getItem("token");
return token
? { ...extra, Authorization: `Bearer ${token}` }
: extra;
}
function initializeMasonry() { function initializeMasonry() {
if (typeof Masonry === 'undefined') { if (typeof Masonry === 'undefined') {
setTimeout(initializeMasonry, 100); setTimeout(initializeMasonry, 100);
@@ -84,7 +91,9 @@ function getProxiedImageUrl(item) {
async function loadFavorites() { async function loadFavorites() {
try { try {
const res = await fetch('/api/gallery/favorites'); const res = await fetch('/api/gallery/favorites', {
headers: getAuthHeaders()
});
if (!res.ok) return; if (!res.ok) return;
const data = await res.json(); const data = await res.json();
favorites.clear(); favorites.clear();
@@ -100,7 +109,10 @@ async function toggleFavorite(item) {
try { try {
if (isFav) { if (isFav) {
await fetch(`/api/gallery/favorites/${encodeURIComponent(id)}`, { method: 'DELETE' }); await fetch(`/api/gallery/favorites/${encodeURIComponent(id)}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
favorites.delete(id); favorites.delete(id);
} else { } else {
const tagsArray = getTagsArray(item); const tagsArray = getTagsArray(item);
@@ -109,7 +121,9 @@ async function toggleFavorite(item) {
await fetch('/api/gallery/favorites', { await fetch('/api/gallery/favorites', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: getAuthHeaders({
'Content-Type': 'application/json'
}),
body: JSON.stringify({ body: JSON.stringify({
id: item.id, id: item.id,
title: item.title || tagsArray.join(', ') || 'Untitled', title: item.title || tagsArray.join(', ') || 'Untitled',
@@ -121,6 +135,7 @@ async function toggleFavorite(item) {
}) })
}); });
favorites.add(id); favorites.add(id);
} }
@@ -238,7 +253,9 @@ async function searchGallery(isLoadMore = false) {
let data; let data;
if (favoritesMode) { if (favoritesMode) {
const res = await fetch('/api/gallery/favorites'); const res = await fetch('/api/gallery/favorites', {
headers: getAuthHeaders()
});
data = await res.json(); data = await res.json();
let favoritesResults = data.favorites || []; let favoritesResults = data.favorites || [];

View File

@@ -1,6 +1,13 @@
const itemMainContentContainer = document.getElementById('item-main-content'); const itemMainContentContainer = document.getElementById('item-main-content');
let currentItem = null; let currentItem = null;
function getAuthHeaders(extra = {}) {
const token = localStorage.getItem("token");
return token
? { ...extra, Authorization: `Bearer ${token}` }
: extra;
}
function getProxiedItemUrl(url, headers = null) { function getProxiedItemUrl(url, headers = null) {
if (!url || !headers) { if (!url || !headers) {
return url; return url;
@@ -52,14 +59,19 @@ async function toggleFavorite() {
try { try {
if (wasFavorited) { if (wasFavorited) {
await fetch(`/api/gallery/favorites/${encodeURIComponent(currentItem.id)}`, { method: 'DELETE' }); await fetch(`/api/gallery/favorites/${encodeURIComponent(currentItem.id)}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
} else { } else {
const serializedHeaders = currentItem.headers ? JSON.stringify(currentItem.headers) : ""; const serializedHeaders = currentItem.headers ? JSON.stringify(currentItem.headers) : "";
const tagsString = Array.isArray(currentItem.tags) ? currentItem.tags.join(',') : (currentItem.tags || ''); const tagsString = Array.isArray(currentItem.tags) ? currentItem.tags.join(',') : (currentItem.tags || '');
await fetch('/api/gallery/favorites', { await fetch('/api/gallery/favorites', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: getAuthHeaders({
'Content-Type': 'application/json'
}),
body: JSON.stringify({ body: JSON.stringify({
id: currentItem.id, id: currentItem.id,
title: currentItem.title || 'Waifu', title: currentItem.title || 'Waifu',
@@ -70,6 +82,7 @@ async function toggleFavorite() {
headers: serializedHeaders headers: serializedHeaders
}) })
}); });
} }
btn.classList.toggle('favorited', !wasFavorited); btn.classList.toggle('favorited', !wasFavorited);
@@ -216,7 +229,9 @@ function renderError(msg) {
async function loadFromFavorites(id) { async function loadFromFavorites(id) {
try { try {
const res = await fetch(`/api/gallery/favorites/${encodeURIComponent(id)}`); const res = await fetch(`/api/gallery/favorites/${encodeURIComponent(id)}`, {
headers: getAuthHeaders()
});
if (!res.ok) throw new Error('Not found'); if (!res.ok) throw new Error('Not found');
const { favorite: fav } = await res.json(); const { favorite: fav } = await res.json();
@@ -269,7 +284,9 @@ async function loadFromProvider(provider, id) {
renderItem(item); renderItem(item);
document.getElementById('page-title').textContent = `WaifuBoard - ${item.title}`; document.getElementById('page-title').textContent = `WaifuBoard - ${item.title}`;
const favRes = await fetch(`/api/gallery/favorites/${encodeURIComponent(id)}`); const favRes = await fetch(`/api/gallery/favorites/${encodeURIComponent(id)}`, {
headers: getAuthHeaders()
});
if (favRes.ok) { if (favRes.ok) {
document.getElementById('fav-btn')?.classList.add('favorited'); document.getElementById('fav-btn')?.classList.add('favorited');
const btn = document.getElementById('fav-btn'); const btn = document.getElementById('fav-btn');

View File

@@ -36,7 +36,6 @@ async function ensureUserDataDB(dbPath) {
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
-- Tabla 2: UserIntegration (✅ ACTUALIZADA)
CREATE TABLE IF NOT EXISTS UserIntegration ( CREATE TABLE IF NOT EXISTS UserIntegration (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL UNIQUE, user_id INTEGER NOT NULL UNIQUE,
@@ -49,7 +48,6 @@ async function ensureUserDataDB(dbPath) {
FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE
); );
-- Tabla 3: ListEntry
CREATE TABLE IF NOT EXISTS ListEntry ( CREATE TABLE IF NOT EXISTS ListEntry (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
@@ -124,13 +122,15 @@ function ensureFavoritesDB(dbPath) {
if (!exists) { if (!exists) {
const schema = ` const schema = `
CREATE TABLE IF NOT EXISTS favorites ( CREATE TABLE IF NOT EXISTS favorites (
id TEXT PRIMARY KEY, id TEXT NOT NULL,
user_id INTEGER NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
image_url TEXT NOT NULL, image_url TEXT NOT NULL,
thumbnail_url TEXT NOT NULL DEFAULT "", thumbnail_url TEXT NOT NULL DEFAULT "",
tags TEXT NOT NULL DEFAULT "", tags TEXT NOT NULL DEFAULT "",
headers TEXT NOT NULL DEFAULT "", headers TEXT NOT NULL DEFAULT "",
provider TEXT NOT NULL DEFAULT "" provider TEXT NOT NULL DEFAULT "",
PRIMARY KEY (id, user_id)
); );
`; `;
@@ -147,6 +147,7 @@ function ensureFavoritesDB(dbPath) {
const hasHeaders = cols.some(c => c.name === "headers"); const hasHeaders = cols.some(c => c.name === "headers");
const hasProvider = cols.some(c => c.name === "provider"); const hasProvider = cols.some(c => c.name === "provider");
const hasUserId = cols.some(c => c.name === "user_id");
const queries = []; const queries = [];
@@ -158,6 +159,10 @@ function ensureFavoritesDB(dbPath) {
queries.push(`ALTER TABLE favorites ADD COLUMN provider TEXT NOT NULL DEFAULT ""`); queries.push(`ALTER TABLE favorites ADD COLUMN provider TEXT NOT NULL DEFAULT ""`);
} }
if (!hasUserId) {
queries.push(`ALTER TABLE favorites ADD COLUMN user_id INTEGER NOT NULL DEFAULT 1`);
}
if (queries.length === 0) { if (queries.length === 0) {
return resolve(false); return resolve(false);
} }