diff --git a/.env.example b/.env.example index a2da8de..6e06cca 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,4 @@ REDIS_AUTH = Redis password you get while provisioning a Redis DB REDIS_URL = Redis DB connection string REDIS_PORT = Any port you want. REDIS_USERNAME = Username of the user that has access to the Redis DB -INSTANCE = ec2 instance id +INSTANCE = ec2 instance id \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 89b89fa..23772df 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,45 +1,45 @@ name: Lint on: - push: - branches: [master] - pull_request: - branches: [master] + push: + branches: [master] + pull_request: + branches: [master] jobs: - build: - runs-on: ubuntu-latest + build: + runs-on: ubuntu-latest - strategy: - matrix: - node-version: [18.x] + strategy: + matrix: + node-version: [18.x] - steps: - - name: Checkout repository - uses: actions/checkout@v2 + steps: + - name: Checkout repository + uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - cache: "npm" + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: npm - - name: Clean npm cache and remove node_modules - run: | - rm -rf node_modules package-lock.json - npm cache clean --force + - name: Clean npm cache and remove node_modules + run: | + rm -rf node_modules package-lock.json + npm cache clean --force - - name: Install dependencies - run: npm install + - name: Install dependencies + run: npm install - - name: Verify package-lock.json - run: git diff --exit-code package-lock.json || echo "⚠️ package-lock.json changed, please update it." + - name: Verify package-lock.json + run: git diff --exit-code package-lock.json || echo "⚠️ package-lock.json changed, please update it." - - name: Run npm ci - run: npm ci + - name: Run npm ci + run: npm ci - - name: Run Prettier - run: npx prettier -c "**/*.js" + - name: Run Prettier + run: npx prettier -c "**/*.js" - - name: Run ESLint - run: npx eslint . + - name: Run ESLint + run: npx eslint . diff --git a/graphql/resolvers.js b/graphql/resolvers.js index 4523d66..8903769 100644 --- a/graphql/resolvers.js +++ b/graphql/resolvers.js @@ -28,7 +28,7 @@ const resolvers = { .populate("leader followers") .populate({ path: "landmarks", - populate: { path: "createdBy", select: "name email" }, + populate: { path: "createdBy", select: "name email imageUrl" }, }); if (!beacon) return new UserInputError("No beacon exists with that id."); // return error iff user not in beacon @@ -181,11 +181,11 @@ const resolvers = { }, oAuth: async (_parent, { userInput }) => { - const { name, email } = userInput; + const { name, email, imageUrl } = userInput; let user = await User.findOne({ email }); if (!user) { - const newUser = new User({ name, email, isVerified: true }); + const newUser = new User({ name, email, isVerified: true, imageUrl }); user = await newUser.save(); } @@ -265,9 +265,8 @@ const resolvers = { return verificationCode; }, - completeVerification: async (_, { userId }, { user }) => { + completeVerification: async (_, { userId }) => { let currentUser = await User.findById(userId); - console.log("Current user: ", currentUser, user); currentUser.isVerified = true; await currentUser.save(); return currentUser; @@ -531,32 +530,39 @@ const resolvers = { }, createLandmark: async (_, { landmark, beaconID }, { user, pubsub }) => { - const beacon = await Beacon.findById(beaconID); - // to save on a db call to populate leader, we just use the stored id to compare + try { + const beacon = await Beacon.findById(beaconID); + // to save on a db call to populate leader, we just use the stored id to compare - if (!beacon) return new UserInputError("Beacon doesn't exist"); + if (!beacon) return new UserInputError("Beacon doesn't exist"); - if (!beacon.followers.includes(user.id) && beacon.leader != user.id) - return new UserInputError("User should be part of beacon"); - const newLandmark = new Landmark({ createdBy: user.id, ...landmark }); - const populatedLandmark = await newLandmark.save().then(lan => lan.populate("createdBy")); + if (!beacon.followers.includes(user.id) && beacon.leader != user.id) + return new UserInputError("User should be part of beacon"); + const newLandmark = new Landmark({ createdBy: user.id, ...landmark }); - beacon.landmarks.push(newLandmark.id); + const savedLandmark = await newLandmark.save(); + const populatedLandmark = await savedLandmark.populate("createdBy"); - pubsub.publish("BEACON_LOCATIONS", { - beaconLocations: { - userSOS: null, - route: null, - updatedUser: null, - landmark: populatedLandmark, - }, - beaconID: beacon.id, - followers: beacon.followers, - leaderID: beacon.leader, - }); - await beacon.save(); + beacon.landmarks.push(newLandmark.id); + + pubsub.publish("BEACON_LOCATIONS", { + beaconLocations: { + userSOS: null, + route: null, + updatedUser: null, + landmark: populatedLandmark, + }, + beaconID: beacon.id, + followers: beacon.followers, + leaderID: beacon.leader, + }); + await beacon.save(); - return populatedLandmark; + return populatedLandmark; + } catch (error) { + console.log("error", error); + throw new Error("Failed to create landmark"); + } }, updateUserLocation: async (_, { id, location }, { user, pubsub }) => { @@ -613,7 +619,7 @@ const resolvers = { const currentDate = new Date(); if (new Date(beacon.expiresAt) < currentDate) return new UserInputError("Beacon is already expired!"); - + // console.log("inside sos", user); pubsub.publish("BEACON_LOCATIONS", { beaconLocations: { userSOS: user, @@ -632,7 +638,6 @@ const resolvers = { deleteUser: async (_, { credentials }) => { try { const userToDelete = await User.findOne({ email: credentials.email }); - console.log("User to delete:", userToDelete); if (!userToDelete) { throw new UserInputError("User not found"); } @@ -694,6 +699,24 @@ const resolvers = { return false; } }, + + updateUserImage: async (_parent, { userId, imageUrl }, context) => { + // Optional: Add authentication check + if (!context.user) throw new AuthenticationError("Not authenticated"); + + try { + const updatedUser = await User.findByIdAndUpdate(userId, { imageUrl }, { new: true }); + + if (!updatedUser) { + throw new UserInputError("User not found"); + } + + return updatedUser; + } catch (error) { + console.log("error", error); + throw new Error("Failed to update user image"); + } + }, }, ...(process.env._HANDLER == null && { Subscription: { @@ -706,28 +729,55 @@ const resolvers = { const { beaconLocations, leaderID, followers, beaconID } = payload; const { userSOS, route, updatedUser, landmark } = beaconLocations; + // Check if user is part of this beacon const isFollower = followers.includes(user.id); const isLeader = leaderID == user.id; - const istrue = variables.id === beaconID && (isFollower || isLeader); + const isBeaconParticipant = variables.id === beaconID && (isFollower || isLeader); + + // If user is not part of this beacon, don't send updates + if (!isBeaconParticipant) { + return false; + } - if (userSOS != null && user.id != userSOS._id) { + // Handle userSOS updates + if (userSOS != null) { + // Don't send SOS updates to the user who triggered the SOS + if (user.id == userSOS._id) { + return false; + } payload.beaconLocations.userSOS = parseUserObject(userSOS); - return istrue; + return true; } - if (route != null && leaderID != user.id) { - return istrue; + + // Handle route updates + if (route != null) { + // Don't send route updates to the leader who created the route + if (leaderID == user.id) { + return false; + } + return true; } - // stopping user who has updated the location - if (updatedUser != null && updatedUser._id != user.id) { + // Handle user location updates + if (updatedUser != null) { + // Don't send location updates to the user who updated their location + if (updatedUser._id == user.id) { + return false; + } payload.beaconLocations.updatedUser = parseUserObject(updatedUser); - return istrue; + return true; } - // stopping the creator of landmark - if (landmark != null && landmark.createdBy._id != user.id) { + + // Handle landmark updates + if (landmark != null) { + // Don't send landmark updates to the user who created the landmark + if (landmark.createdBy._id == user.id) { + return false; + } payload.beaconLocations.landmark = parseLandmarkObject(landmark); - return istrue; + return true; } + // If none of the above conditions are met, don't send the update return false; } ), diff --git a/graphql/schema.js b/graphql/schema.js index efb7bb2..e9c7049 100644 --- a/graphql/schema.js +++ b/graphql/schema.js @@ -49,6 +49,7 @@ const typeDefs = gql` _id: ID! createdAt: Float! title: String! + icon: String! location: Location! createdBy: User! } @@ -56,6 +57,7 @@ const typeDefs = gql` input LandmarkInput { title: String! location: LocationInput! + icon: String! } type User { @@ -73,6 +75,7 @@ const typeDefs = gql` location: Location beacons: [Beacon!]! groups: [Group!]! + imageUrl: String! } input AuthPayload { @@ -112,6 +115,7 @@ const typeDefs = gql` input oAuthInput { email: String name: String + imageUrl: String } type UpdatedGroupPayload { @@ -159,6 +163,7 @@ const typeDefs = gql` deleteBeacon(id: ID!): Boolean! sos(id: ID!): User! deleteUser(credentials: AuthPayload!): Boolean! + updateUserImage(userId: ID!, imageUrl: String!): User! } type Subscription { diff --git a/index.mjs b/index.mjs index dc03f57..c8828d9 100644 --- a/index.mjs +++ b/index.mjs @@ -64,7 +64,6 @@ app.use( app.get("/", (req, res) => res.send("Hello World! This is a GraphQL API. Check out /graphql")); app.get("/j/:shortcode", async (_req, res) => { - console.log(`shortcode route hit`); res.send("this should open in the app eventually"); }); diff --git a/models/landmark.js b/models/landmark.js index 0fe9ccc..780f35b 100644 --- a/models/landmark.js +++ b/models/landmark.js @@ -8,6 +8,7 @@ const landmarkSchema = new Schema( { title: { type: String, required: true }, location: { type: LocationSchema, required: true }, + icon: { type: String, required: true }, createdBy: { type: Schema.Types.ObjectId, required: true, ref: "User" }, }, { diff --git a/models/user.js b/models/user.js index 9d83f0c..184590c 100644 --- a/models/user.js +++ b/models/user.js @@ -14,6 +14,10 @@ const UserSchema = new Schema( location: LocationSchema, beacons: { type: [Schema.Types.ObjectId], ref: "Beacon", default: [] }, groups: { type: [Schema.Types.ObjectId], ref: "Group", default: [] }, + imageUrl: { + type: String, + default: "https://cdn.jsdelivr.net/gh/alohe/avatars/png/memo_35.png", // default avatar URL + }, }, { timestamps: true, diff --git a/parsing.js b/parsing.js index 1834feb..ab07c82 100644 --- a/parsing.js +++ b/parsing.js @@ -1,76 +1,113 @@ const { default: mongoose } = require("mongoose"); -function parseBeaconObject(beaconObject) { - if (typeof beaconObject === "string") { - return convertToObjectId(beaconObject); +function parseUserObject(userObject) { + if (!userObject) { + return null; } - var model = { - _id: convertToObjectId(beaconObject._id), - title: beaconObject.title, - shortcode: beaconObject.shortcode, - startsAt: convertToDate(beaconObject.startsAt), - expiresAt: convertToDate(beaconObject.expiresAt), - group: parseGroupObject(beaconObject.group), - leader: parseUserObject(beaconObject.leader), - location: beaconObject.location, - followers: Array.isArray(beaconObject.followers) - ? beaconObject.followers.map(follower => parseUserObject(follower)) - : [], - landmarks: Array.isArray(beaconObject.landmarks) - ? beaconObject.landmarks.map(landmark => parseLandmarkObject(landmark)) - : [], - route: beaconObject.route.map(single => parseLocationObject(single)), - geofence: beaconObject.geofence, - - updatedAt: convertToDate(beaconObject.updatedAt), - __v: beaconObject.__v, - }; - - return model; -} - -function parseUserObject(userObject) { if (typeof userObject === "string") { return convertToObjectId(userObject); } + let model = null; + try { - var model = { - _id: convertToObjectId(userObject._id), + model = { + _id: safeObjectId(userObject._id), name: userObject.name, email: userObject.email, password: userObject.password, - groups: Array.isArray(userObject.groups) ? userObject.groups.map(group => parseGroupObject(group)) : [], + groups: Array.isArray(userObject.groups) + ? userObject.groups + .filter(Boolean) + .map(group => safeParse(parseGroupObject, group)) + .filter(Boolean) + : [], beacons: Array.isArray(userObject.beacons) - ? userObject.beacons.map(beacon => parseBeaconObject(beacon)) + ? userObject.beacons + .filter(Boolean) + .map(beacon => safeParse(parseBeaconObject, beacon)) + .filter(Boolean) : [], - location: userObject.location, + location: parseLocationObject(userObject.location), createdAt: convertToDate(userObject.createdAt), updatedAt: convertToDate(userObject.updatedAt), __v: userObject.__v, }; } catch (error) { - console.log(error); + return null; } return model; } +function parseBeaconObject(beaconObject) { + if (!beaconObject) { + return null; + } + + if (typeof beaconObject === "string") { + return convertToObjectId(beaconObject); + } + + const model = { + _id: safeObjectId(beaconObject._id), + title: beaconObject.title, + shortcode: beaconObject.shortcode, + startsAt: convertToDate(beaconObject.startsAt), + expiresAt: convertToDate(beaconObject.expiresAt), + group: safeParse(parseGroupObject, beaconObject.group), + leader: safeParse(parseUserObject, beaconObject.leader), + location: parseLocationObject(beaconObject.location), + followers: Array.isArray(beaconObject.followers) + ? beaconObject.followers + .filter(Boolean) + .map(follower => safeParse(parseUserObject, follower)) + .filter(Boolean) + : [], + landmarks: Array.isArray(beaconObject.landmarks) + ? beaconObject.landmarks + .filter(Boolean) + .map(landmark => safeParse(parseLandmarkObject, landmark)) + .filter(Boolean) + : [], + route: Array.isArray(beaconObject.route) ? beaconObject.route.filter(Boolean).map(parseLocationObject) : [], + geofence: beaconObject.geofence, + updatedAt: convertToDate(beaconObject.updatedAt), + __v: beaconObject.__v, + }; + + return model; +} + function parseGroupObject(groupObject) { + if (!groupObject) { + return null; + } + if (typeof groupObject === "string") { return convertToObjectId(groupObject); } - var model = { - _id: convertToObjectId(groupObject._id), + const model = { + _id: safeObjectId(groupObject._id), title: groupObject.title, shortcode: groupObject.shortcode, - createdAt: new Date(groupObject.createdAt), - updatedAt: new Date(groupObject.updatedAtt), - leader: parseUserObject(groupObject.leader), - members: Array.isArray(groupObject.members) ? groupObject.members.map(member => parseUserObject(member)) : [], - beacons: Array.isArray(groupObject.beacons) ? groupObject.beacons.map(beacon => parseBeaconObject(beacon)) : [], + createdAt: convertToDate(groupObject.createdAt), + updatedAt: convertToDate(groupObject.updatedAt), // ← FIXED TYPO + leader: safeParse(parseUserObject, groupObject.leader), + members: Array.isArray(groupObject.members) + ? groupObject.members + .filter(Boolean) + .map(member => safeParse(parseUserObject, member)) + .filter(Boolean) + : [], + beacons: Array.isArray(groupObject.beacons) + ? groupObject.beacons + .filter(Boolean) + .map(beacon => safeParse(parseBeaconObject, beacon)) + .filter(Boolean) + : [], __v: groupObject.__v, }; @@ -78,15 +115,19 @@ function parseGroupObject(groupObject) { } function parseLandmarkObject(landmarkObject) { + if (!landmarkObject) { + return null; + } + if (typeof landmarkObject === "string") { return convertToObjectId(landmarkObject); } - var model = { - _id: convertToObjectId(landmarkObject._id), + const model = { + _id: safeObjectId(landmarkObject._id), title: landmarkObject.title, location: parseLocationObject(landmarkObject.location), - createdBy: parseUserObject(landmarkObject.createdBy), + createdBy: safeParse(parseUserObject, landmarkObject.createdBy), createdAt: convertToDate(landmarkObject.createdAt), updatedAt: convertToDate(landmarkObject.updatedAt), __v: landmarkObject.__v, @@ -96,25 +137,37 @@ function parseLandmarkObject(landmarkObject) { } function parseLocationObject(locationObject) { - console.log(JSON.stringify(locationObject)); - if (locationObject == null) return undefined; + if (!locationObject) return undefined; - var model = { + return { lat: locationObject.lat, lon: locationObject.lon, }; - - return model; } function convertToObjectId(id) { return new mongoose.Types.ObjectId(id); } +function safeObjectId(id) { + if (!id) return null; + return convertToObjectId(id); +} + function convertToDate(date) { + if (!date) return null; return new Date(date); } +function safeParse(fn, obj) { + if (!obj) return null; + try { + return fn(obj); + } catch (err) { + return null; + } +} + module.exports = { parseUserObject, parseBeaconObject,