diff --git a/docker-compose.yml b/docker-compose.yml index 793c17d..b098e2d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -141,11 +141,6 @@ services: networks: f0ckm-net: driver: bridge - enable_ipv6: true - ipam: - config: - - subnet: 172.28.0.0/16 - - subnet: fd00:f0c0:1337::/64 volumes: certs: diff --git a/migrations/add_favorites_private.sql b/migrations/add_favorites_private.sql new file mode 100644 index 0000000..787adf5 --- /dev/null +++ b/migrations/add_favorites_private.sql @@ -0,0 +1,5 @@ +-- Add favorites_private column to user_options table +ALTER TABLE user_options + ADD COLUMN IF NOT EXISTS favorites_private boolean DEFAULT false; + +COMMENT ON COLUMN public.user_options.favorites_private IS 'When true, only the owner and administrators can view the user''s favorites list'; diff --git a/migrations/f0ckm_schema.sql b/migrations/f0ckm_schema.sql index 0a36dd1..3d95514 100644 --- a/migrations/f0ckm_schema.sql +++ b/migrations/f0ckm_schema.sql @@ -1498,7 +1498,8 @@ CREATE TABLE public.user_options ( receive_user_notifications boolean DEFAULT true, do_not_disturb boolean DEFAULT false, comment_display_mode integer DEFAULT 1, - force_comment_display_mode integer DEFAULT 0 + force_comment_display_mode integer DEFAULT 0, + favorites_private boolean DEFAULT false ); diff --git a/public/s/js/settings.js b/public/s/js/settings.js index ca4e384..4ada4b7 100644 --- a/public/s/js/settings.js +++ b/public/s/js/settings.js @@ -1119,6 +1119,37 @@ }); } + const favsPrivateToggle = document.getElementById('favorites_private_toggle'); + if (favsPrivateToggle) { + favsPrivateToggle.addEventListener('change', async () => { + const favorites_private = favsPrivateToggle.checked; + try { + const res = await fetch('/api/v2/settings/favorites_private', { + method: 'PUT', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-CSRF-Token': window.f0ckSession ? window.f0ckSession.csrf_token : '' + }, + body: new URLSearchParams({ favorites_private }) + }); + const data = await res.json(); + if (data.success) { + showStatus('Favorites privacy preference updated!', 'success'); + if (window.f0ckSession) { + window.f0ckSession.favorites_private = favorites_private; + } + } else { + alert(data.msg || 'Error saving preference'); + favsPrivateToggle.checked = !favorites_private; // Revert + } + } catch (err) { + console.error('Update Favorites Privacy error:', err); + alert('Connection error'); + favsPrivateToggle.checked = !favorites_private; // Revert + } + }); + } + // New Dual Column Layout Toggle const layoutToggle = document.getElementById('use_new_layout_toggle'); if (layoutToggle) { diff --git a/src/inc/locales/de.json b/src/inc/locales/de.json index ec8ed36..2355b0d 100644 --- a/src/inc/locales/de.json +++ b/src/inc/locales/de.json @@ -165,6 +165,8 @@ "hide_item_ratings_hint": "Zeige keine SFW/NSFW-Bewertungsindikatoren bei Elementen auf der Hauptseite", "disable_swiping": "Wischen deaktivieren", "disable_swiping_hint": "Navigation per Wischen auf Mobilgeräten deaktivieren", + "favorites_private": "Private Favoriten", + "favorites_private_hint": "Nur du und Administratoren können deine Favoritenliste sehen.", "image_expand_on_click": "Bilder beim Klicken inline erweitern", "image_expand_on_click_hint": "Anstatt das Scroll-Zoom-Modal zu öffnen, wird ein Bild beim Klicken innerhalb der Seite auf volle Größe erweitert.", "enable_bg_blur": "Hintergrundunschärfe aktivieren", @@ -535,6 +537,7 @@ "subscribe_uploads_btn": "Benutzer für Uploads abonnieren", "no_uploads": "Keine Uploads gefunden", "no_favs": "Keine Favoriten", + "private_favorites": "private Favoriten", "back_to_profile": "Zurück zum Profil", "ban_modal_title": "Benutzer sperren", "ban_modal_reason": "Grund:", diff --git a/src/inc/locales/en.json b/src/inc/locales/en.json index fddd032..3bc8ef9 100644 --- a/src/inc/locales/en.json +++ b/src/inc/locales/en.json @@ -165,6 +165,8 @@ "hide_item_ratings_hint": "Don't show SFW/NSFW rating indicators on main page items", "disable_swiping": "Disable Swiping", "disable_swiping_hint": "Disable swipe-to-navigate on mobile devices", + "favorites_private": "Private Favorites", + "favorites_private_hint": "Only you and administrators can view your favorites list.", "image_expand_on_click": "Expand images inline on click", "image_expand_on_click_hint": "Instead of opening the scroll zoom modal, clicking an image will expand it to full size within the page.", "enable_bg_blur": "Enable Background blur", @@ -539,6 +541,7 @@ "subscribe_uploads_btn": "Subscribe user to uploads", "no_uploads": "no uploads found", "no_favs": "no favorites", + "private_favorites": "private favorites", "back_to_profile": "Back to Profile", "ban_modal_title": "Ban User", "ban_modal_reason": "Reason:", diff --git a/src/inc/locales/nl.json b/src/inc/locales/nl.json index d0ead57..539ed77 100644 --- a/src/inc/locales/nl.json +++ b/src/inc/locales/nl.json @@ -165,6 +165,8 @@ "hide_item_ratings_hint": "Toon geen SFW/NSFW beoordelingsindicatoren op hoofdpagina items", "disable_swiping": "Swipen uitschakelen", "disable_swiping_hint": "Swipe-to-navigate uitschakelen op mobiele apparaten", + "favorites_private": "Privé favorieten", + "favorites_private_hint": "Alleen jij en beheerders kunnen je favorietenlijst bekijken.", "image_expand_on_click": "Afbeeldingen inline vergroten bij klikken", "image_expand_on_click_hint": "In plaats van de scroll-zoom-modal te openen, wordt een afbeelding bij klikken vergroot tot volledige grootte binnen de pagina.", "enable_bg_blur": "Achtergrondvervaging inschakelen", @@ -535,6 +537,7 @@ "subscribe_uploads_btn": "Gebruiker abonneren op uploads", "no_uploads": "geen uploads gevonden", "no_favs": "geen favorieten", + "private_favorites": "privé favorieten", "back_to_profile": "Terug naar Profiel", "ban_modal_title": "Gebruiker Bannen", "ban_modal_reason": "Reden:", diff --git a/src/inc/locales/zange.json b/src/inc/locales/zange.json index 168ffbc..1cacf08 100644 --- a/src/inc/locales/zange.json +++ b/src/inc/locales/zange.json @@ -165,6 +165,8 @@ "hide_item_ratings_hint": "Zeige keine SFW/NSFW-Bewertungsindikatoren bei Elementen auf der Hauptseite", "disable_swiping": "Wischen deaktivieren", "disable_swiping_hint": "Deaktivieren der Wisch-Navigation auf Mobilgeräten", + "favorites_private": "Private Favoriten", + "favorites_private_hint": "Nur du und Administratoren können deine Favoritenliste sehen.", "image_expand_on_click": "Bildli beim Klickle inline uffblähe", "image_expand_on_click_hint": "Anstatt dat Scroll-Zoom-Moped aufzumache, wird n Bild beim Klickle uff volle Größ im Bereich uffgepumpt.", "enable_bg_blur": "Hintergrundunschärfe aktivieren", @@ -536,6 +538,7 @@ "subscribe_uploads_btn": "Benutzer für Aufladierungen abonnieren", "no_uploads": "keine Aufladierungen gefunden", "no_favs": "keine Favoriten", + "private_favorites": "private Favoriten", "back_to_profile": "Zurück zum Profil", "ban_modal_title": "Benutzer sperren", "ban_modal_reason": "Grund:", diff --git a/src/inc/routeinc/f0cklib.mjs b/src/inc/routeinc/f0cklib.mjs index b3fd2a4..7996679 100644 --- a/src/inc/routeinc/f0cklib.mjs +++ b/src/inc/routeinc/f0cklib.mjs @@ -136,8 +136,60 @@ const xdScoreMeta = (score) => { return { tier: 5, label: 'xDDDDD+' }; }; +async function checkFavoritesAccess(rawUser, { session, user_id, is_admin } = {}) { + if (!rawUser) return { isPrivate: false, isAllowed: true }; + const decodedUser = decodeURI(rawUser); + const targetUserRows = await db` + select u.id, u."user", u.admin, uo.favorites_private + from "user" u + left join user_options uo on uo.user_id = u.id + where u."user" ilike ${decodedUser} or u.login ilike ${decodedUser} + limit 1 + `; + if (!targetUserRows.length || !targetUserRows[0].favorites_private) { + return { isPrivate: false, isAllowed: true }; + } + + const targetUser = targetUserRows[0]; + + let reqUserId = user_id; + let reqIsAdmin = is_admin; + + if (typeof session === 'object' && session !== null) { + if (reqUserId === undefined) reqUserId = session.id; + if (reqIsAdmin === undefined) reqIsAdmin = !!session.admin; + } + + let isAllowed = false; + if (reqUserId) { + if (+reqUserId === +targetUser.id) { + isAllowed = true; + } else if (reqIsAdmin === true) { + isAllowed = true; + } else if (reqIsAdmin === undefined) { + const reqUserRows = await db`select admin from "user" where id = ${+reqUserId} limit 1`; + if (reqUserRows.length > 0 && reqUserRows[0].admin) { + isAllowed = true; + } + } + } + + return { isPrivate: true, isAllowed }; +} + export default { - getf0cks: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, page, mode, ratings, fav, session, limit, strict, newer, exclude, user_id, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, minXdScore, tagger: rawTagger } = {}) => { + getf0cks: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, page, mode, ratings, fav, session, limit, strict, newer, exclude, user_id, is_admin, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, minXdScore, tagger: rawTagger } = {}) => { + if (fav && rawUser) { + const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin }); + if (isPrivate && !isAllowed) { + return { + success: false, + is_private: true, + message: "private favorites" + }; + } + } + const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null; // --- title: prefix — search items.title instead of the tags table --- @@ -428,7 +480,18 @@ export default { view_mode: fav ? 'favs' : 'uploads' }; }, - getf0ck: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, itemid: rawItemid, mode, ratings, session, strict, exclude, user_id, fav, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, lang } = {}) => { + getf0ck: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, itemid: rawItemid, mode, ratings, session, strict, exclude, user_id, is_admin, fav, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, lang } = {}) => { + if (fav && rawUser) { + const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin }); + if (isPrivate && !isAllowed) { + return { + success: false, + is_private: true, + message: "private favorites" + }; + } + } + const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null; // --- title: prefix — search items.title instead of the tags table --- @@ -884,7 +947,14 @@ export default { tmp }; return data; - }, getRandom: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, mode, ratings, fav, session, strict, exclude, userHall: rawUserHall, userHallOwner: rawUserHallOwner } = {}) => { + }, + getRandom: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, mode, ratings, fav, session, strict, exclude, user_id, is_admin, userHall: rawUserHall, userHallOwner: rawUserHallOwner } = {}) => { + if (fav && rawUser) { + const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin }); + if (isPrivate && !isAllowed) { + return null; + } + } const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null; const hall = rawHall || null; diff --git a/src/inc/routes/ajax.mjs b/src/inc/routes/ajax.mjs index 15c6e56..5b5df55 100644 --- a/src/inc/routes/ajax.mjs +++ b/src/inc/routes/ajax.mjs @@ -218,9 +218,10 @@ export default (router, tpl) => { mime: query.mime || (req.cookies.mime || null), mode: query.mode !== undefined ? +query.mode : req.mode, ratings: ratingsArr, - session: !!req.session, - exclude: req.session ? (req.session.excluded_tags || []) : [], + session: req.session, user_id: req.session?.id, + is_admin: req.session?.admin, + exclude: req.session ? (req.session.excluded_tags || []) : [], fav: query.fav === 'true', random: isRandom, strict: query.strict === '1' || query.strict === 'true' || req.session?.strict_mode, @@ -231,6 +232,19 @@ export default (router, tpl) => { }); if (!data.success) { + if (data.is_private) { + const { t: tErr } = createI18n(req.session?.language || req.lang || 'en'); + const privateHtml = `