diff --git a/src/resolvers/Query/user.ts b/src/resolvers/Query/user.ts index dc6233a011..1d252dd84d 100644 --- a/src/resolvers/Query/user.ts +++ b/src/resolvers/Query/user.ts @@ -1,21 +1,55 @@ import { USER_NOT_FOUND_ERROR } from "../../constants"; import { errors } from "../../libraries"; import type { InterfaceAppUserProfile, InterfaceUser } from "../../models"; -import { AppUserProfile, User } from "../../models"; +import { AppUserProfile, User, Organization } from "../../models"; import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; /** * This query fetch the user from the database. + * + * This function ensure that users can only query their own data and not access details of other users , protecting sensitive data. + * * @param _parent- * @param args - An object that contains `id` for the user. * @param context- * @returns An object that contains user data. If the user is not found then it throws a `NotFoundError` error. */ export const user: QueryResolvers["user"] = async (_parent, args, context) => { + // Check if the current user exists in the system const currentUserExists = !!(await User.exists({ _id: context.userId, })); - if (currentUserExists === false) { + if (!currentUserExists) { + throw new errors.NotFoundError( + USER_NOT_FOUND_ERROR.DESC, + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM, + ); + } + + const [userOrganization, superAdminProfile] = await Promise.all([ + Organization.exists({ + members: args.id, + admins: context.userId, + }), + AppUserProfile.exists({ + userId: context.userId, + isSuperAdmin: true, + }), + ]); + + if (!userOrganization && context.userId !== args.id && !superAdminProfile) { + throw new errors.UnauthorizedError( + "Access denied. Only the user themselves, organization admins, or super admins can view this profile.", + ); + } + + // Fetch the user data from the database based on the provided ID (args.id) + const user: InterfaceUser = (await User.findById( + args.id, + ).lean()) as InterfaceUser; + + if (!user) { throw new errors.NotFoundError( USER_NOT_FOUND_ERROR.DESC, USER_NOT_FOUND_ERROR.CODE, @@ -23,9 +57,6 @@ export const user: QueryResolvers["user"] = async (_parent, args, context) => { ); } - const user: InterfaceUser = (await User.findOne({ - _id: args.id, - }).lean()) as InterfaceUser; const userAppProfile: InterfaceAppUserProfile = (await AppUserProfile.findOne( { userId: user._id, diff --git a/tests/resolvers/Query/userAccess.spec.ts b/tests/resolvers/Query/userAccess.spec.ts new file mode 100644 index 0000000000..18becef383 --- /dev/null +++ b/tests/resolvers/Query/userAccess.spec.ts @@ -0,0 +1,187 @@ +import { USER_NOT_FOUND_ERROR, BASE_URL } from "../../../src/constants"; +import { user as userResolver } from "../../../src/resolvers/Query/user"; +import { User, Organization, AppUserProfile } from "../../../src/models"; +import { connect, disconnect } from "../../helpers/db"; +import type { TestUserType } from "../../helpers/userAndOrg"; +import { createTestUser } from "../../helpers/userAndOrg"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type mongoose from "mongoose"; +import { Types } from "mongoose"; +import { FundraisingCampaignPledge } from "../../../src/models/FundraisingCampaignPledge"; +import { deleteUserFromCache } from "../../../src/services/UserCache/deleteUserFromCache"; + +let testUser: TestUserType; +let anotherTestUser: TestUserType; +let adminUser: TestUserType; +let superAdminUser: TestUserType; +let MONGOOSE_INSTANCE: typeof mongoose; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + await deleteUserFromCache(testUser?.id); + const pledges = await FundraisingCampaignPledge.find({ + _id: new Types.ObjectId(), + }).lean(); + console.log(pledges); + try { + testUser = await createTestUser(); + if (!testUser?.id) throw new Error("Failed to create test user"); + anotherTestUser = await createTestUser(); + if (!anotherTestUser?.id) + throw new Error("Failed to create another test user"); + adminUser = await createTestUser(); + if (!adminUser?.id) throw new Error("Failed to create admin user"); + superAdminUser = await createTestUser(); + if (!superAdminUser?.id) + throw new Error("Failed to create super admin user"); + + const org = await Organization.create({ + creatorId: adminUser?.id, + members: [anotherTestUser?.id], + admins: [adminUser?.id], + name: "Test Organization", + description: "A test organization for user query testing", + }); + if (!org) throw new Error("Failed to create organization"); + + const profile = await AppUserProfile.create({ + userId: superAdminUser?.id, + isSuperAdmin: true, + }); + if (!profile) throw new Error("Failed to create super admin profile"); + } catch (error) { + console.error("Failed to set up test data:", error); + throw error; + } +}); + +afterAll(async () => { + await Promise.all([ + User.deleteMany({ + _id: { + $in: [ + testUser?.id, + anotherTestUser?.id, + adminUser?.id, + superAdminUser?.id, + ], + }, + }), + Organization.deleteMany({}), + AppUserProfile.deleteMany({ userId: superAdminUser?.id }), + ]); + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("user Query", () => { + // Test case 1: Invalid user ID scenario + it("throws error if user doesn't exist", async () => { + expect.assertions(2); + const args = { + id: new Types.ObjectId().toString(), + }; + + const context = { + userId: testUser?.id, + }; + + try { + await userResolver?.({}, args, context); + } catch (error: unknown) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.DESC); + } + }); + + // Test case 2: Unauthorized access scenario + it("throws unauthorized error when trying to access another user's data", async () => { + expect.assertions(1); + const args = { + id: anotherTestUser?.id, + }; + + const context = { + userId: testUser?.id, + }; + + try { + await userResolver?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual( + "Access denied. Only the user themselves, organization admins, or super admins can view this profile.", + ); + } + }); + + // Test case 3: Admin access scenario + it("allows an admin to access another user's data within the same organization", async () => { + expect.assertions(2); + const args = { + id: anotherTestUser?.id, + }; + + const context = { + userId: adminUser?.id, + apiRootURL: BASE_URL, + }; + + const org = await Organization.findOne({ admins: adminUser?.id }); + expect(org?.members).toContain(anotherTestUser?.id); + + const result = await userResolver?.({}, args, context); + + const user = await User.findById(anotherTestUser?._id).lean(); + + expect(result?.user).toEqual({ + ...user, + organizationsBlockedBy: [], + image: user?.image ? `${BASE_URL}${user.image}` : null, + }); + }); + + // Test case 4: SuperAdmin access scenario + it("allows a super admin to access any user's data", async () => { + expect.assertions(1); + const args = { + id: anotherTestUser?.id, + }; + + const context = { + userId: superAdminUser?.id, + apiRootURL: BASE_URL, + }; + + const result = await userResolver?.({}, args, context); + + const user = await User.findById(anotherTestUser?._id).lean(); + + expect(result?.user).toEqual({ + ...user, + organizationsBlockedBy: [], + image: user?.image ? `${BASE_URL}${user.image}` : null, + }); + }); + + // Test case 5: Successful access to own profile + it("successfully returns user data when accessing own profile", async () => { + expect.assertions(1); + const args = { + id: testUser?.id, + }; + + const context = { + userId: testUser?.id, + apiRootURL: BASE_URL, + }; + + const result = await userResolver?.({}, args, context); + + const user = await User.findById(testUser?._id).lean(); + + expect(result?.user).toEqual({ + ...user, + organizationsBlockedBy: [], + image: user?.image ? `${BASE_URL}${user.image}` : null, + }); + }); +});