mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-07-13 02:53:45 +00:00
fix(#55): implement user data isolation to prevent cross-user data access
- Add server-side authentication validation for all protected API endpoints - Create auth utility with requireAuth() function for session validation - Update CombinedDataRepository to filter data by authenticated user ID - Add findByUserId() method to CatchRecordRepository for user-specific queries - Replace client-side userId parameters with server-side session extraction - Use MongoDB aggregation with $lookup and $expr for secure user filtering - Return 401 errors for unauthenticated requests - Fix critical security vulnerability where users could see others' catch records Fixes: User data isolation bug where one user's catch records were used for everyone Security: Prevents unauthorized access to other users' Pokemon tracking data
This commit is contained in:
@@ -9,6 +9,10 @@ class CatchRecordRepository {
|
||||
return CatchRecordModel.find().exec();
|
||||
}
|
||||
|
||||
async findByUserId(userId: string): Promise<CatchRecord[]> {
|
||||
return CatchRecordModel.find({ userId }).exec();
|
||||
}
|
||||
|
||||
async create(data: Partial<CatchRecord>): Promise<CatchRecord> {
|
||||
return CatchRecordModel.create(data);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import PokedexEntryModel, { type PokedexEntry } from '$lib/models/PokedexEntry';
|
||||
import CatchRecordModel, { type CatchRecord } from '$lib/models/CatchRecord';
|
||||
import { CombinedData } from '$lib/models/CombinedData';
|
||||
import { type CombinedData } from '$lib/models/CombinedData';
|
||||
|
||||
class CombinedDataRepository {
|
||||
async findAllCombinedData(
|
||||
userId: string,
|
||||
enableForms: boolean = true,
|
||||
region: string = '',
|
||||
game: string = ''
|
||||
@@ -12,8 +13,19 @@ class CombinedDataRepository {
|
||||
{
|
||||
$lookup: {
|
||||
from: 'catchrecords',
|
||||
localField: '_id',
|
||||
foreignField: 'pokedexEntryId',
|
||||
let: { entryId: '$_id' },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$and: [
|
||||
{ $eq: ['$pokedexEntryId', '$$entryId'] },
|
||||
{ $eq: ['$userId', userId] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
as: 'catchRecord'
|
||||
}
|
||||
},
|
||||
@@ -58,16 +70,14 @@ class CombinedDataRepository {
|
||||
|
||||
const combinedData: CombinedData[] = await PokedexEntryModel.aggregate(pipeline).exec();
|
||||
|
||||
// Filter out entries with no catch records
|
||||
const filteredData = combinedData.filter((data) => data.catchRecord);
|
||||
|
||||
return filteredData.map((data) => ({
|
||||
return combinedData.map((data) => ({
|
||||
pokedexEntry: data as PokedexEntry,
|
||||
catchRecord: data.catchRecord as CatchRecord
|
||||
}));
|
||||
}
|
||||
|
||||
async findCombinedData(
|
||||
userId: string,
|
||||
page: number = 1,
|
||||
limit: number = 20,
|
||||
enableForms: boolean = true,
|
||||
@@ -79,8 +89,19 @@ class CombinedDataRepository {
|
||||
{
|
||||
$lookup: {
|
||||
from: 'catchrecords',
|
||||
localField: '_id',
|
||||
foreignField: 'pokedexEntryId',
|
||||
let: { entryId: '$_id' },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$and: [
|
||||
{ $eq: ['$pokedexEntryId', '$$entryId'] },
|
||||
{ $eq: ['$userId', userId] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
as: 'catchRecord'
|
||||
}
|
||||
},
|
||||
@@ -127,33 +148,71 @@ class CombinedDataRepository {
|
||||
|
||||
const combinedData: CombinedData[] = await PokedexEntryModel.aggregate(pipeline).exec();
|
||||
|
||||
// Filter out entries with no catch records
|
||||
const filteredData = combinedData.filter((data) => data.catchRecord);
|
||||
|
||||
return filteredData.map((data) => ({
|
||||
return combinedData.map((data) => ({
|
||||
pokedexEntry: data as PokedexEntry,
|
||||
catchRecord: data.catchRecord as CatchRecord
|
||||
}));
|
||||
}
|
||||
|
||||
async countCombinedData(enableForms: boolean, region: string, game: string): Promise<number> {
|
||||
const filter: any = {};
|
||||
async countCombinedData(userId: string, enableForms: boolean, region: string, game: string): Promise<number> {
|
||||
const pipeline: any[] = [
|
||||
{
|
||||
$lookup: {
|
||||
from: 'catchrecords',
|
||||
let: { entryId: '$_id' },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$and: [
|
||||
{ $eq: ['$pokedexEntryId', '$$entryId'] },
|
||||
{ $eq: ['$userId', userId] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
as: 'catchRecord'
|
||||
}
|
||||
},
|
||||
{
|
||||
$unwind: {
|
||||
path: '$catchRecord',
|
||||
preserveNullAndEmptyArrays: true
|
||||
}
|
||||
},
|
||||
{
|
||||
$count: 'total'
|
||||
}
|
||||
];
|
||||
|
||||
if (!enableForms) {
|
||||
filter['boxPlacement.box'] = { $ne: null };
|
||||
pipeline.unshift({
|
||||
$match: {
|
||||
'boxPlacement.box': { $ne: null }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (region.length > 0) {
|
||||
filter['regionToCatchIn'] = region;
|
||||
pipeline.unshift({
|
||||
$match: {
|
||||
regionToCatchIn: region
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (game.length > 0) {
|
||||
filter['gamesToCatchIn'] = { $in: [game] };
|
||||
pipeline.unshift({
|
||||
$match: {
|
||||
gamesToCatchIn: { $in: [game] }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const count = await PokedexEntryModel.countDocuments(filter).exec();
|
||||
return count;
|
||||
const result = await PokedexEntryModel.aggregate(pipeline).exec();
|
||||
return result.length > 0 ? result[0].total : 0;
|
||||
} catch (error) {
|
||||
console.error('Error counting documents:', error);
|
||||
throw error;
|
||||
|
||||
Reference in New Issue
Block a user