Block banned-country locations and align GraphQL ops
Defense-in-depth banned-country gate covering every entry point that could set a location Feeld's policy disallows (~60 countries from their support article): - New src/config/bannedCountries.ts — single source of truth (ISO codes + aliases) - New src/utils/reverseGeocode.ts — Nominatim reverse lookup w/ localStorage cache - New src/api/links/bannedCountryLink.ts — Apollo link chokepoint; intercepts every DeviceLocationUpdate mutation and refuses to forward if reverse-geocode resolves to a banned country. Catches Settings, Discover, Likes scanner, and ApiExplorer raw GraphQL alike. - useLocation.tsx — setLocation throws BannedCountryError; saveLocation gate; sanitize banned entries on localStorage and server hydration - Settings.tsx — block at search, saved-location pick, and save-current - Likes.tsx — skip banned saved locations in scanForLikes and "Fuck It" scan - server/index.js — PUT /api/saved-locations filters; readSavedLocations filters legacy banned entries so rotation cron is safe too - nginx.conf — route additions for new backend endpoints Plus the broader rc/realign-graphql-ops session work: GraphQL query/mutation realignment after Feeld API changes, ApiExplorer updates, Profile/Discover/Likes refinements, useFavorites hook, dataSync extensions, vite proxy adjustments. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+126
-4
@@ -315,6 +315,81 @@ app.delete('/api/sent-pings/:targetProfileId', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/favorites - Get all saved/favorited profiles
|
||||
app.get('/api/favorites', (req, res) => {
|
||||
const filePath = path.join(DATA_DIR, 'favorites.json');
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
res.json(data);
|
||||
} catch (e) {
|
||||
console.error('Failed to read favorites.json:', e);
|
||||
res.json({ favorites: [] });
|
||||
}
|
||||
} else {
|
||||
res.json({ favorites: [] });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/favorites - Add a favorited profile (idempotent)
|
||||
app.post('/api/favorites', (req, res) => {
|
||||
const { targetProfileId, profile } = req.body;
|
||||
if (!targetProfileId) {
|
||||
return res.status(400).json({ success: false, error: 'targetProfileId required' });
|
||||
}
|
||||
const filePath = path.join(DATA_DIR, 'favorites.json');
|
||||
|
||||
let data = { favorites: [], updatedAt: null };
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
} catch (e) {
|
||||
console.error('Failed to read favorites.json:', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.favorites.some(f => f.targetProfileId === targetProfileId)) {
|
||||
data.favorites.unshift({
|
||||
targetProfileId,
|
||||
savedAt: Date.now(),
|
||||
profile: profile || null,
|
||||
});
|
||||
data.updatedAt = new Date().toISOString();
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
res.json({ success: true, favorites: data.favorites });
|
||||
});
|
||||
|
||||
// DELETE /api/favorites/:targetProfileId - Remove a favorite
|
||||
app.delete('/api/favorites/:targetProfileId', (req, res) => {
|
||||
const { targetProfileId } = req.params;
|
||||
const filePath = path.join(DATA_DIR, 'favorites.json');
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
data.favorites = data.favorites.filter(f => f.targetProfileId !== targetProfileId);
|
||||
data.updatedAt = new Date().toISOString();
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||
res.json({ success: true, favorites: data.favorites });
|
||||
} catch (e) {
|
||||
console.error('Failed to update favorites.json:', e);
|
||||
res.status(500).json({ success: false, error: e.message });
|
||||
}
|
||||
} else {
|
||||
res.json({ success: true, favorites: [] });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/favorites - Clear all favorites
|
||||
app.delete('/api/favorites', (req, res) => {
|
||||
const filePath = path.join(DATA_DIR, 'favorites.json');
|
||||
const data = { favorites: [], updatedAt: new Date().toISOString() };
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||
res.json({ success: true, favorites: [] });
|
||||
});
|
||||
|
||||
// GET /api/disliked-profiles - Get all disliked profiles
|
||||
app.get('/api/disliked-profiles', (req, res) => {
|
||||
const filePath = path.join(DATA_DIR, 'dislikedProfiles.json');
|
||||
@@ -1186,6 +1261,35 @@ const AUTH_TOKENS_FILE = path.join(DATA_DIR, 'auth-tokens.json');
|
||||
const ROTATION_STATE_FILE = path.join(DATA_DIR, 'locationRotation.json');
|
||||
const SAVED_LOCATIONS_FILE = path.join(DATA_DIR, 'savedLocations.json');
|
||||
|
||||
// Countries where Feeld is NOT available — mirror of web/src/config/bannedCountries.ts.
|
||||
// Keep in sync if that file changes. Used to filter incoming saved-location syncs.
|
||||
const BANNED_COUNTRY_CODES = new Set([
|
||||
'AL','DZ','AO','AM','AZ','BH','BD','BJ','BW','BN','BF','CM','TD','CN','CG','CU',
|
||||
'EG','GA','GM','HK','IR','IQ','JO','KZ','KE','LA','LR','MO','MG','MW','ML','MR',
|
||||
'MU','MM','MA','MZ','NA','NE','OM','PK','PG','PH','QA','RU','SA','SN','SL','SD',
|
||||
'TW','TJ','TZ','TH','TG','TN','TM','UG','UZ','YE','ZM','ZW',
|
||||
]);
|
||||
const BANNED_COUNTRY_NAMES = new Set([
|
||||
'albania','algeria','angola','armenia','azerbaijan','bahrain','bangladesh','benin','botswana',
|
||||
'brunei darussalam','brunei','burkina faso','cameroon','chad','china',"people's republic of china",
|
||||
'republic of the congo','congo','congo, republic of','congo-brazzaville','cuba','egypt','gabon',
|
||||
'gambia','the gambia','hong kong','iran','islamic republic of iran','iraq','jordan','kazakhstan',
|
||||
'kenya','laos',"lao people's democratic republic",'lao pdr','liberia','macau','macao','madagascar',
|
||||
'malawi','mali','mauritania','mauritius','myanmar','burma','mayanmar','morocco','mozambique',
|
||||
'namibia','niger','oman','pakistan','papua new guinea','philippines','qatar','russia',
|
||||
'russian federation','saudi arabia','senegal','sierra leone','sudan','taiwan',
|
||||
'taiwan, province of china','republic of china','tajikistan','tanzania',
|
||||
'united republic of tanzania','thailand','togo','tunisia','turkmenistan','uganda','uzbekistan',
|
||||
'yemen','zambia','zimbabwe',
|
||||
]);
|
||||
|
||||
function isBannedLocation(loc) {
|
||||
if (!loc) return false;
|
||||
if (loc.countryCode && BANNED_COUNTRY_CODES.has(String(loc.countryCode).trim().toUpperCase())) return true;
|
||||
if (loc.country && BANNED_COUNTRY_NAMES.has(String(loc.country).trim().toLowerCase())) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
class FeeldAPIClient {
|
||||
constructor() {
|
||||
this.accessToken = null;
|
||||
@@ -1393,7 +1497,11 @@ function writeRotationState(state) {
|
||||
function readSavedLocations() {
|
||||
if (fs.existsSync(SAVED_LOCATIONS_FILE)) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(SAVED_LOCATIONS_FILE, 'utf8'));
|
||||
const raw = JSON.parse(fs.readFileSync(SAVED_LOCATIONS_FILE, 'utf8'));
|
||||
const arr = Array.isArray(raw) ? raw : [];
|
||||
// Strip any banned-country entries that might be left over from before
|
||||
// the country filter was added. Rotation and selection both go through here.
|
||||
return arr.filter((loc) => !isBannedLocation(loc));
|
||||
} catch (e) {
|
||||
console.error('[Rotation] Failed to read saved locations:', e.message);
|
||||
}
|
||||
@@ -1683,14 +1791,28 @@ app.get('/api/saved-locations', (req, res) => {
|
||||
res.json(readSavedLocations());
|
||||
});
|
||||
|
||||
// PUT /api/saved-locations — Browser syncs its saved locations here
|
||||
// PUT /api/saved-locations — Browser syncs its saved locations here.
|
||||
// Strips any locations in Feeld-restricted countries (defense in depth against
|
||||
// stale clients or directly-crafted requests).
|
||||
app.put('/api/saved-locations', (req, res) => {
|
||||
const { locations } = req.body;
|
||||
if (!Array.isArray(locations)) {
|
||||
return res.status(400).json({ success: false, error: 'locations array required' });
|
||||
}
|
||||
writeSavedLocations(locations);
|
||||
res.json({ success: true, count: locations.length });
|
||||
const filtered = [];
|
||||
const rejected = [];
|
||||
for (const loc of locations) {
|
||||
if (isBannedLocation(loc)) {
|
||||
rejected.push({ id: loc.id, name: loc.name, country: loc.country, countryCode: loc.countryCode });
|
||||
} else {
|
||||
filtered.push(loc);
|
||||
}
|
||||
}
|
||||
if (rejected.length) {
|
||||
console.warn(`[saved-locations] dropped ${rejected.length} banned-country location(s):`, rejected);
|
||||
}
|
||||
writeSavedLocations(filtered);
|
||||
res.json({ success: true, count: filtered.length, rejected: rejected.length });
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user