Skip to content

Latest commit

 

History

History
1806 lines (1540 loc) · 54.8 KB

File metadata and controls

1806 lines (1540 loc) · 54.8 KB

NetWorkr API - Implementation Plan

Project Scope: Small project with focus on UX features over security Developer Role: Dev A (User, Profile, Social, Network, Search, Project domains) Total Estimated Time: 80-105 hours across 7 phases (5 base + 2 enhancement phases) Last Updated: December 13, 2025


Phase 1: Critical Auth Fixes ✅ COMPLETED

Estimated Time: 4-6 hours Status: ✅ Done

1.1 OTP Expiry Validation ✅

Files Modified:

  • src/models/user/accountModel.js - Added verificationTokenExpires field
  • src/services/accountService.js - Set 10-minute expiry, validate in verifyAccount

Implementation Details:

// Account model now has:
verificationTokenExpires: { type: Date }

// OTP expires after 10 minutes
const getOTPExpiry = () => {
    return new Date(Date.now() + OTP_EXPIRY_MINUTES * 60 * 1000)
}

// Validation in verifyAccount:
if (account.verificationTokenExpires && new Date() > account.verificationTokenExpires) {
    throw new Error('OTP has expired. Please request a new verification code.')
}

1.2 Password Reset Completion Flow ✅

Files Modified:

  • src/services/accountService.js - Added completePasswordReset(token, newPassword)
  • src/validations/accountValidation.js - Added completePasswordResetSchema
  • src/controllers/accountController.js - Added completePasswordReset controller
  • src/routes/v1/authRoute.js - Added PATCH /password-resets route

API Endpoint:

PATCH /api/v1/password-resets
Body: { token: string, newPassword: string (min 8 chars) }
Response: { message: "Password has been reset successfully" }

Flow:

  1. User clicks reset link in email → Frontend extracts token from URL
  2. Frontend calls PATCH /password-resets with token + new password
  3. Backend validates token hash, checks expiry, updates password
  4. Confirmation email sent to user

1.3 Email Service Integration ✅

Files Created:

  • src/services/emailService.js - Full Brevo SDK integration

Features:

  • Automatic fallback to mock emails if BREVO_API_KEY not configured
  • Beautiful HTML email templates with consistent branding
  • Plain text fallbacks for email clients

Email Templates:

Template Function Use Case
OTP Verification sendOTPEmail() Registration, resend OTP
Password Reset sendPasswordResetEmail() Forgot password
Welcome sendWelcomeEmail() After account verification
Password Changed sendPasswordChangedEmail() After password change

Environment Variables Required:

BREVO_API_KEY=your-brevo-api-key
EMAIL_SENDER_NAME=NetWorkr
EMAIL_SENDER_EMAIL=noreply@networkr.com
FRONTEND_URL=http://localhost:3000

To Enable Real Emails:

  1. Create Brevo account at https://www.brevo.com (free: 300 emails/day)
  2. Get API key from Settings → SMTP & API → API Keys
  3. Add BREVO_API_KEY to .env file
  4. Restart server

Phase 2: Media Uploads ✅ COMPLETED

Estimated Time: 6-8 hours Status: ✅ Done Dependencies: Cloudinary account (free tier available)

2.1 Cloudinary Configuration

Files to Create:

  • src/config/cloudinary.js

Implementation:

import { v2 as cloudinary } from 'cloudinary'

cloudinary.config({
    cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
    api_key: process.env.CLOUDINARY_API_KEY,
    api_secret: process.env.CLOUDINARY_API_SECRET
})

export const uploadToCloudinary = async (buffer, options = {}) => {
    return new Promise((resolve, reject) => {
        const uploadStream = cloudinary.uploader.upload_stream(
            { 
                folder: 'networkr',
                resource_type: 'auto',
                ...options 
            },
            (error, result) => {
                if (error) reject(error)
                else resolve(result)
            }
        )
        streamifier.createReadStream(buffer).pipe(uploadStream)
    })
}

export { cloudinary }

Environment Variables:

CLOUDINARY_CLOUD_NAME=your-cloud-name
CLOUDINARY_API_KEY=your-api-key
CLOUDINARY_API_SECRET=your-api-secret

2.2 Upload Middleware

Files to Create:

  • src/middlewares/uploadMiddleware.js

Implementation:

import multer from 'multer'

// Memory storage for Cloudinary streaming
const storage = multer.memoryStorage()

// File filter for images
const imageFilter = (req, file, cb) => {
    if (file.mimetype.startsWith('image/')) {
        cb(null, true)
    } else {
        cb(new Error('Only image files are allowed'), false)
    }
}

// File filter for images and videos
const mediaFilter = (req, file, cb) => {
    if (file.mimetype.startsWith('image/') || file.mimetype.startsWith('video/')) {
        cb(null, true)
    } else {
        cb(new Error('Only image and video files are allowed'), false)
    }
}

export const uploadAvatar = multer({
    storage,
    fileFilter: imageFilter,
    limits: { fileSize: 5 * 1024 * 1024 } // 5MB
}).single('avatar')

export const uploadBackground = multer({
    storage,
    fileFilter: imageFilter,
    limits: { fileSize: 10 * 1024 * 1024 } // 10MB
}).single('background')

export const uploadPostMedia = multer({
    storage,
    fileFilter: mediaFilter,
    limits: { fileSize: 50 * 1024 * 1024 } // 50MB
}).array('media', 10) // Max 10 files

2.3 Profile Picture Upload

Files to Modify:

  • src/services/profileService.js - Add uploadAvatar(), uploadBackground()
  • src/controllers/profileController.js - Add upload controllers
  • src/routes/v1/userRoute.js - Add upload routes

API Endpoints:

POST /api/v1/users/me/avatar
- Content-Type: multipart/form-data
- Body: avatar (file, max 5MB, image only)
- Response: { avatarUrl: string }

POST /api/v1/users/me/background
- Content-Type: multipart/form-data
- Body: background (file, max 10MB, image only)
- Response: { backgroundUrl: string }

2.4 Post Media Upload

Files to Modify:

  • src/services/socialService.js - Modify createPost() for file uploads
  • src/controllers/socialController.js - Handle multipart data
  • src/routes/v1/socialRoute.js - Add multer middleware to POST /posts

API Changes:

POST /api/v1/posts
- Content-Type: multipart/form-data
- Body: 
  - content (text)
  - visibility (enum)
  - media (files, max 10, max 50MB each)
- Response: { post with media array }

Phase 3: Engagement Features

Estimated Time: 8-12 hours Status: 🔄 Not Started Dependencies: None (Socket.io already installed)

3.1 Socket.io Setup

Files to Create:

  • src/config/socket.js

Files to Modify:

  • src/server.js - Initialize Socket.io with HTTP server

Implementation:

// src/config/socket.js
import { Server } from 'socket.io'
import jwt from 'jsonwebtoken'

let io = null

export const initSocket = (httpServer) => {
    io = new Server(httpServer, {
        cors: {
            origin: process.env.CORS_WHITELIST?.split(',') || '*',
            methods: ['GET', 'POST']
        }
    })

    // Authentication middleware
    io.use((socket, next) => {
        const token = socket.handshake.auth.token
        if (!token) return next(new Error('Authentication required'))
        
        try {
            const decoded = jwt.verify(token, process.env.JWT_SECRET)
            socket.userId = decoded.personId
            socket.accountId = decoded.accountId
            next()
        } catch (err) {
            next(new Error('Invalid token'))
        }
    })

    io.on('connection', (socket) => {
        // Join user's personal room for notifications
        socket.join(`user:${socket.userId}`)
        console.log(`User ${socket.userId} connected`)

        socket.on('disconnect', () => {
            console.log(`User ${socket.userId} disconnected`)
        })
    })

    return io
}

export const getIO = () => {
    if (!io) throw new Error('Socket.io not initialized')
    return io
}

export const emitToUser = (userId, event, data) => {
    if (io) io.to(`user:${userId}`).emit(event, data)
}

Server.js Changes:

import http from 'http'
import { initSocket } from './config/socket.js'

const httpServer = http.createServer(app)
initSocket(httpServer)

httpServer.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`)
})

3.2 Notification Service

Files to Create:

  • src/services/notificationService.js
  • src/controllers/notificationController.js
  • src/routes/v1/notificationRoute.js

Notification Model (already exists at src/models/communication/notificationModel.js):

// Types: LIKE, COMMENT, SHARE, FOLLOW, JOB_APPLICATION, MESSAGE
// Fields: recipientId, senderId, type, referenceId, referenceType, message, isRead

Service Functions:

// Create and emit notification
const createNotification = async (recipientId, senderId, type, referenceId, referenceType, message) => {
    const notification = await Notification.create({...})
    
    // Real-time emit
    emitToUser(recipientId, 'notification', notification)
    
    return notification
}

// Get notifications with pagination
const getNotifications = async (userId, page = 1, limit = 20) => {...}

// Mark single as read
const markAsRead = async (notificationId, userId) => {...}

// Mark all as read
const markAllAsRead = async (userId) => {...}

// Get unread count
const getUnreadCount = async (userId) => {...}

API Endpoints:

GET /api/v1/notifications
- Query: page, limit
- Response: { notifications[], totalCount, unreadCount }

PATCH /api/v1/notifications/:id/read
- Response: { notification with isRead: true }

PATCH /api/v1/notifications/read-all
- Response: { message: "All notifications marked as read" }

GET /api/v1/notifications/unread-count
- Response: { count: number }

3.3 Integrate Notifications into Social Actions

Files to Modify:

  • src/services/socialService.js

Trigger Points:

  • handleInteraction() → Create LIKE/SHARE notification to post owner
  • addComment() → Create COMMENT notification to post owner
  • handleConnection() → Create FOLLOW notification (when implemented)

3.4 Profile Completeness API

Files to Modify:

  • src/services/profileService.js - Enhance calculateProfileScore()
  • src/controllers/profileController.js - Add endpoint
  • src/routes/v1/userRoute.js - Add route

API Endpoint:

GET /api/v1/users/me/completeness
Response: {
    score: 75,
    level: "Intermediate", // Beginner (0-33), Intermediate (34-66), Advanced (67-100)
    sections: {
        basicInfo: { score: 100, max: 20, completed: ["firstName", "lastName", "email"] },
        professionalInfo: { score: 50, max: 25, completed: ["headline"], missing: ["summary", "currentCompany"] },
        experience: { score: 80, max: 25, count: 2, suggestion: "Add more recent experience" },
        skills: { score: 60, max: 15, count: 3, suggestion: "Add at least 5 skills" },
        education: { score: 0, max: 10, count: 0, suggestion: "Add your education" },
        photo: { score: 100, max: 5, hasPhoto: true }
    },
    tips: [
        "Add a professional summary to increase visibility",
        "Add your education history",
        "Consider adding 2 more skills"
    ]
}

Phase 4: Enhanced Features

Estimated Time: 12-16 hours Status: 🔄 Not Started

4.1 Smart Feed Algorithm

Files to Modify:

  • src/services/socialService.js - Replace chronological viewFeed() with viewPersonalizedFeed()

Algorithm Factors:

  1. Engagement Score: likes * 1 + comments * 2 + shares * 3
  2. Connection Weight: Posts from connections score higher
  3. Recency Decay: score * Math.exp(-hoursOld / 72) (72-hour half-life)
  4. Content Match: Skills/industry overlap (optional, advanced)

Implementation:

const viewPersonalizedFeed = async (userId, page = 1, limit = 20) => {
    // Get user's connections
    const connections = await Connection.find({ 
        $or: [{ fromUserId: userId }, { toUserId: userId }],
        status: 'accepted'
    })
    const connectionIds = connections.map(c => 
        c.fromUserId.equals(userId) ? c.toUserId : c.fromUserId
    )

    // Aggregate with scoring
    const posts = await Post.aggregate([
        { $match: { _destroy: false, visibility: { $ne: 'PRIVATE' } } },
        {
            $addFields: {
                engagementScore: {
                    $add: [
                        { $ifNull: ['$likeCount', 0] },
                        { $multiply: [{ $ifNull: ['$commentCount', 0] }, 2] },
                        { $multiply: [{ $ifNull: ['$shareCount', 0] }, 3] }
                    ]
                },
                isConnection: { $in: ['$authorId', connectionIds] },
                hoursOld: {
                    $divide: [
                        { $subtract: [new Date(), '$createdAt'] },
                        3600000
                    ]
                }
            }
        },
        {
            $addFields: {
                finalScore: {
                    $multiply: [
                        { $add: ['$engagementScore', { $cond: ['$isConnection', 50, 0] }] },
                        { $exp: { $divide: [{ $multiply: ['$hoursOld', -1] }, 72] } }
                    ]
                }
            }
        },
        { $sort: { finalScore: -1 } },
        { $skip: (page - 1) * limit },
        { $limit: limit }
    ])

    return posts
}

4.2 Enhanced User Search

Files to Modify:

  • src/services/profileService.js - Enhance searchUsers()
  • src/validations/profileValidation.js - Add filter validation
  • src/routes/v1/userRoute.js - Update Swagger docs

New Query Parameters:

GET /api/v1/users
Query:
  - q: string (name search, existing)
  - skills: string[] (filter by skills)
  - location: string (city/country filter)
  - industry: string (industry filter)
  - company: string (current/past company)
  - experienceLevel: enum (Entry, Mid, Senior, Executive)
  - page, limit (pagination)

Implementation (MongoDB Aggregation):

const searchUsers = async (filters, page = 1, limit = 20) => {
    const pipeline = []
    
    // Match base criteria
    pipeline.push({ $match: { _destroy: false } })
    
    // Name search
    if (filters.q) {
        pipeline.push({
            $match: {
                $or: [
                    { firstName: { $regex: filters.q, $options: 'i' } },
                    { lastName: { $regex: filters.q, $options: 'i' } }
                ]
            }
        })
    }
    
    // Lookup skills
    if (filters.skills?.length) {
        pipeline.push(
            { $lookup: { from: 'skills', localField: '_id', foreignField: 'personId', as: 'skillDocs' } },
            { $match: { 'skillDocs.name': { $in: filters.skills } } }
        )
    }
    
    // Lookup experiences for company/industry
    if (filters.company || filters.industry) {
        pipeline.push(
            { $lookup: { from: 'experiences', localField: '_id', foreignField: 'personId', as: 'experienceDocs' } }
        )
        if (filters.company) {
            pipeline.push({ $match: { 'experienceDocs.companyName': { $regex: filters.company, $options: 'i' } } })
        }
        if (filters.industry) {
            pipeline.push({ $match: { 'experienceDocs.industry': filters.industry } })
        }
    }
    
    // Location filter
    if (filters.location) {
        pipeline.push({
            $match: {
                $or: [
                    { 'contactInfo.city': { $regex: filters.location, $options: 'i' } },
                    { 'contactInfo.country': { $regex: filters.location, $options: 'i' } }
                ]
            }
        })
    }
    
    // Pagination
    pipeline.push({ $skip: (page - 1) * limit }, { $limit: limit })
    
    return Person.aggregate(pipeline)
}

4.3 Comment Threading (Nested Replies)

Files to Modify:

  • src/models/social/commentModel.js - Add depth field (max 3)
  • src/services/socialService.js - Add getThreadedComments()

Model Update:

// Add to commentSchema:
depth: {
    type: Number,
    default: 0,
    max: 3
}

Implementation:

const getThreadedComments = async (postId, page = 1, limit = 20) => {
    // Get top-level comments with replies
    const comments = await Comment.aggregate([
        { $match: { postId: ObjectId(postId), parentCommentId: null, _destroy: false } },
        { $sort: { createdAt: -1 } },
        { $skip: (page - 1) * limit },
        { $limit: limit },
        // Lookup nested replies using $graphLookup
        {
            $graphLookup: {
                from: 'comments',
                startWith: '$_id',
                connectFromField: '_id',
                connectToField: 'parentCommentId',
                as: 'replies',
                maxDepth: 2, // depth 0 + 2 more levels = max 3
                depthField: 'replyDepth',
                restrictSearchWithMatch: { _destroy: false }
            }
        },
        // Populate author info
        { $lookup: { from: 'persons', localField: 'authorId', foreignField: '_id', as: 'author' } },
        { $unwind: '$author' }
    ])

    // Build nested tree structure
    return comments.map(comment => buildCommentTree(comment))
}

const buildCommentTree = (comment) => {
    const replies = comment.replies || []
    const directReplies = replies
        .filter(r => r.parentCommentId?.equals(comment._id))
        .map(r => ({
            ...r,
            replies: replies.filter(rr => rr.parentCommentId?.equals(r._id))
        }))
    
    return { ...comment, replies: directReplies }
}

Phase 5: Premium Features

Estimated Time: 12-16 hours Status: 🔄 Not Started Dependencies: PDFKit or Puppeteer, OpenAI API (optional)

5.1 CV/PDF Generation

Packages to Install:

yarn add pdfkit
# OR for HTML-to-PDF with better styling:
yarn add puppeteer

Files to Create:

  • src/services/cvService.js
  • src/templates/cv/ (HTML templates if using Puppeteer)

Files to Modify:

  • src/controllers/profileController.js
  • src/routes/v1/userRoute.js

API Endpoint:

GET /api/v1/users/me/cv/download
Query: template=modern|professional|minimal (default: modern)
Response: PDF file stream (Content-Type: application/pdf)

PDFKit Implementation:

import PDFDocument from 'pdfkit'

const generateCVPdf = async (personId, template = 'modern') => {
    // Fetch all profile data
    const person = await Person.findById(personId)
    const experiences = await Experience.find({ personId, _destroy: false })
    const skills = await Skill.find({ personId, _destroy: false })
    const education = await Education.find({ personId, _destroy: false })

    const doc = new PDFDocument({ size: 'A4', margin: 50 })
    
    // Header with name and contact
    doc.fontSize(24).font('Helvetica-Bold').text(`${person.firstName} ${person.lastName}`)
    doc.fontSize(12).font('Helvetica').text(person.headline || '')
    doc.text(person.contactInfo?.email || '')
    doc.text(person.contactInfo?.phone || '')
    doc.moveDown()

    // Summary
    if (person.summary) {
        doc.fontSize(14).font('Helvetica-Bold').text('Professional Summary')
        doc.fontSize(11).font('Helvetica').text(person.summary)
        doc.moveDown()
    }

    // Experience
    if (experiences.length) {
        doc.fontSize(14).font('Helvetica-Bold').text('Experience')
        experiences.forEach(exp => {
            doc.fontSize(12).font('Helvetica-Bold').text(exp.title)
            doc.fontSize(11).font('Helvetica').text(`${exp.companyName} | ${exp.startDate} - ${exp.endDate || 'Present'}`)
            if (exp.description) doc.text(exp.description)
            doc.moveDown(0.5)
        })
    }

    // Skills
    if (skills.length) {
        doc.fontSize(14).font('Helvetica-Bold').text('Skills')
        doc.fontSize(11).font('Helvetica').text(skills.map(s => s.name).join(' • '))
        doc.moveDown()
    }

    // Education
    if (education.length) {
        doc.fontSize(14).font('Helvetica-Bold').text('Education')
        education.forEach(edu => {
            doc.fontSize(12).font('Helvetica-Bold').text(edu.degree)
            doc.fontSize(11).font('Helvetica').text(`${edu.institution} | ${edu.graduationYear}`)
            doc.moveDown(0.5)
        })
    }

    return doc
}

// Controller:
const downloadCV = async (req, res) => {
    const doc = await cvService.generateCVPdf(req.user.personId, req.query.template)
    
    res.setHeader('Content-Type', 'application/pdf')
    res.setHeader('Content-Disposition', `attachment; filename=CV-${req.user.personId}.pdf`)
    
    doc.pipe(res)
    doc.end()
}

5.2 AI Profile Enhancement

Packages to Install:

yarn add openai

Files to Create:

  • src/services/aiService.js

Environment Variables:

OPENAI_API_KEY=sk-your-api-key

Implementation:

import OpenAI from 'openai'

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })

const enhanceSummary = async (currentSummary, experiences, skills) => {
    const prompt = `
    Enhance this professional summary for a LinkedIn-style profile:
    
    Current Summary: ${currentSummary || 'None provided'}
    
    Experience: ${experiences.map(e => `${e.title} at ${e.companyName}`).join(', ')}
    
    Skills: ${skills.map(s => s.name).join(', ')}
    
    Write a compelling 2-3 sentence professional summary that:
    - Highlights key achievements
    - Mentions top skills
    - Is written in first person
    - Is professional but personable
    `

    const completion = await openai.chat.completions.create({
        model: 'gpt-3.5-turbo',
        messages: [{ role: 'user', content: prompt }],
        max_tokens: 200
    })

    return completion.choices[0].message.content
}

const suggestSkills = async (currentSkills, experiences, industry) => {
    const prompt = `
    Based on this professional profile, suggest 5 additional skills they should add:
    
    Current Skills: ${currentSkills.map(s => s.name).join(', ')}
    Experience: ${experiences.map(e => `${e.title} at ${e.companyName} (${e.industry})`).join(', ')}
    Industry: ${industry || 'Not specified'}
    
    Return only a JSON array of skill names: ["skill1", "skill2", ...]
    `

    const completion = await openai.chat.completions.create({
        model: 'gpt-3.5-turbo',
        messages: [{ role: 'user', content: prompt }],
        max_tokens: 100
    })

    return JSON.parse(completion.choices[0].message.content)
}

API Endpoints:

POST /api/v1/users/me/enhance/summary
Response: { enhancedSummary: string, originalSummary: string }

GET /api/v1/users/me/suggestions/skills
Response: { suggestedSkills: string[] }

Cost Management:

  • Rate limit: 5 requests per user per day
  • Implement request caching (24-hour TTL)
  • Monitor usage via OpenAI dashboard
  • Fallback to rule-based suggestions if API fails

Dependencies Summary

Already Installed ✅

  • @getbrevo/brevo - Email service
  • cloudinary - File storage
  • multer - File upload middleware
  • streamifier - Buffer to stream conversion
  • socket.io - Real-time communications

To Install

# For CV generation (choose one):
yarn add pdfkit
# OR
yarn add puppeteer

# For AI features (optional):
yarn add openai

External Accounts Needed

Service Purpose Free Tier
Brevo Email sending 300 emails/day
Cloudinary File storage 25GB storage, 25GB bandwidth
OpenAI AI features $5 credit (new accounts)

Environment Variables Template

# Server
PORT=8017
BUILD_MODE=dev
JWT_SECRET=your-super-secret-key
JWT_EXPIRES_IN=7d
FRONTEND_URL=http://localhost:3000

# MongoDB
MONGODB_URI=mongodb://localhost:27017/networkr

# Email (Brevo)
BREVO_API_KEY=your-brevo-api-key
EMAIL_SENDER_NAME=NetWorkr
EMAIL_SENDER_EMAIL=noreply@networkr.com

# File Storage (Cloudinary)
CLOUDINARY_CLOUD_NAME=your-cloud-name
CLOUDINARY_API_KEY=your-api-key
CLOUDINARY_API_SECRET=your-api-secret

# AI (OpenAI) - Optional
OPENAI_API_KEY=sk-your-api-key

# CORS
CORS_WHITELIST=http://localhost:3000,http://localhost:5173

Quick Reference: API Endpoints Added

Phase 1 (Auth) ✅

Method Endpoint Description
POST /registrations Register (OTP now expires in 10 min)
POST /verifications Verify OTP (checks expiry)
POST /verifications/resend Resend OTP (sets new expiry)
POST /password-resets Request password reset
PATCH /password-resets NEW Complete reset with token
PATCH /sessions/password Change password (authenticated)

Phase 2 (Uploads) - Planned

Method Endpoint Description
POST /users/me/avatar Upload profile picture
POST /users/me/background Upload background image
POST /posts Create post with media

Phase 3 (Engagement) - Planned

Method Endpoint Description
GET /notifications Get notifications
PATCH /notifications/:id/read Mark as read
PATCH /notifications/read-all Mark all as read
GET /notifications/unread-count Get unread count
GET /users/me/completeness Profile completeness

Phase 4 (Enhanced) - Planned

Method Endpoint Description
GET /feeds/main Personalized feed (algorithm)
GET /users Enhanced search with filters

Phase 5 (Premium) - Planned

Method Endpoint Description
GET /users/me/cv/download Download CV as PDF
POST /users/me/enhance/summary AI-enhanced summary
GET /users/me/suggestions/skills AI skill suggestions

Phase 6: Dev A Week 5 - Core Enhancements ✅ CURRENT PHASE

Estimated Time: 20-25 hours Status: 🔄 In Progress Dependencies: Cloudinary account, coordination with Dev B for notifications

Dev A Responsibility: User, Profile, Social, Network, Search, Project domains Dev B Responsibility: Jobs, Company, Applications, Admin (not covered here)

Current Implementation Status

✅ Completed (Weeks 1-4):

  • Week 1: AccountController (Auth with OTP expiry, password reset, email service)
  • Week 1-2: ProfileController (complete profile CRUD)
  • Week 2: SocialController (posts, comments, interactions)
  • Week 3: NetworkController (connections, invitations)
  • Week 3-4: SearchController (members, jobs, companies, candidate filtering)
  • Week 4: ProjectController (collaboration invitations)

🚨 Critical Gaps in Dev A Domain:

  1. No media upload capability (profile photos, post media)
  2. Notification triggers not implemented (3 TODOs in code)
  3. No profile completeness guidance
  4. Feed is purely chronological (no engagement algorithm)
  5. Comment threading not implemented

6.1 Media Uploads - Profile & Social

Estimated Time: 8-10 hours Priority: 🔴 Critical Status: 🔄 Not Started

Current Issue: Users cannot upload actual files, must use external URLs

Files to Create:

  • src/config/cloudinary.js
  • src/middlewares/uploadMiddleware.js

Files to Modify:

  • src/services/profileService.js - Add uploadAvatar(), uploadBackground()
  • src/controllers/profileController.js - Add upload handlers
  • src/routes/v1/userRoute.js - Add upload routes with Swagger docs
  • src/services/socialService.js - Enhance createPost() for file uploads
  • src/controllers/socialController.js - Handle multipart/form-data
  • src/routes/v1/socialRoute.js - Update POST /posts route

Implementation - Cloudinary Config:

// src/config/cloudinary.js
import { v2 as cloudinary } from 'cloudinary'
import streamifier from 'streamifier'

cloudinary.config({
    cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
    api_key: process.env.CLOUDINARY_API_KEY,
    api_secret: process.env.CLOUDINARY_API_SECRET
})

export const uploadToCloudinary = async (fileBuffer, folder, options = {}) => {
    return new Promise((resolve, reject) => {
        const uploadStream = cloudinary.uploader.upload_stream({
            folder: `networkr/${folder}`,
            resource_type: 'auto',
            transformation: options.transformation,
            ...options
        }, (error, result) => {
            if (error) reject(error)
            else resolve(result.secure_url)
        })
        streamifier.createReadStream(fileBuffer).pipe(uploadStream)
    })
}

export { cloudinary }

Implementation - Upload Middleware:

// src/middlewares/uploadMiddleware.js
import multer from 'multer'

const storage = multer.memoryStorage()

const imageFilter = (req, file, cb) => {
    if (file.mimetype.startsWith('image/')) {
        cb(null, true)
    } else {
        cb(new Error('Only image files are allowed'), false)
    }
}

const mediaFilter = (req, file, cb) => {
    if (file.mimetype.startsWith('image/') || file.mimetype.startsWith('video/')) {
        cb(null, true)
    } else {
        cb(new Error('Only image and video files allowed'), false)
    }
}

export const uploadAvatar = multer({
    storage,
    fileFilter: imageFilter,
    limits: { fileSize: 5 * 1024 * 1024 } // 5MB
}).single('avatar')

export const uploadBackground = multer({
    storage,
    fileFilter: imageFilter,
    limits: { fileSize: 10 * 1024 * 1024 } // 10MB
}).single('background')

export const uploadPostMedia = multer({
    storage,
    fileFilter: mediaFilter,
    limits: { fileSize: 50 * 1024 * 1024 } // 50MB per file
}).array('media', 10) // Max 10 files

New API Endpoints:

POST /api/v1/users/me/avatar
- Content-Type: multipart/form-data
- Body: avatar (image file, max 5MB)
- Cloudinary transformation: width 400, height 400, crop: fill
- Response: { avatarUrl: string }

POST /api/v1/users/me/background
- Content-Type: multipart/form-data
- Body: background (image file, max 10MB)
- Cloudinary transformation: width 1200, height 400, crop: fill
- Response: { backgroundUrl: string }

POST /api/v1/posts (updated)
- Content-Type: multipart/form-data
- Body: 
  - content: string
  - visibility: enum
  - media: file[] (max 10 files, 50MB each)
- Response: { post with media array }

Environment Variables Required:

CLOUDINARY_CLOUD_NAME=your-cloud-name
CLOUDINARY_API_KEY=your-api-key
CLOUDINARY_API_SECRET=your-api-secret

6.2 Profile Completeness Score

Estimated Time: 4-5 hours Priority: 🟡 Important Status: 🔄 Not Started

Current Issue: No guidance for users to complete profiles

Files to Modify:

  • src/services/profileService.js - Add getProfileCompleteness(personId)
  • src/controllers/profileController.js - Add controller
  • src/routes/v1/userRoute.js - Add route with Swagger

Implementation:

// src/services/profileService.js
const getProfileCompleteness = async (personId) => {
    const person = await Person.findById(personId)
    const experiences = await Experience.find({ personId, _destroy: false })
    const skills = await Skill.find({ personId, _destroy: false })
    const educations = await Education.find({ personId, _destroy: false })
    const projects = await Project.find({ personId, _destroy: false })

    // Scoring weights (total 100 points)
    const weights = {
        basicInfo: 20,      // firstName, lastName, email
        photo: 10,          // avatar
        headline: 10,       // headline/title
        summary: 15,        // professional summary
        experience: 20,     // work experience (min 2)
        skills: 15,         // skills (min 5)
        education: 10       // education (min 1)
    }

    const sections = {
        basicInfo: {
            max: weights.basicInfo,
            score: person.firstName && person.lastName && person.email ? weights.basicInfo : 0,
            status: person.firstName && person.lastName && person.email ? 'complete' : 'incomplete',
            completed: [person.firstName && 'firstName', person.lastName && 'lastName', person.email && 'email'].filter(Boolean)
        },
        photo: {
            max: weights.photo,
            score: person.avatar ? weights.photo : 0,
            hasAvatar: !!person.avatar,
            status: person.avatar ? 'complete' : 'missing'
        },
        headline: {
            max: weights.headline,
            score: person.headline ? weights.headline : 0,
            hasHeadline: !!person.headline,
            status: person.headline ? 'complete' : 'missing'
        },
        summary: {
            max: weights.summary,
            score: person.summary && person.summary.length >= 50 ? weights.summary : 0,
            hasSummary: !!person.summary,
            status: person.summary && person.summary.length >= 50 ? 'complete' : 'missing',
            currentLength: person.summary?.length || 0,
            minLength: 50
        },
        experience: {
            max: weights.experience,
            count: experiences.length,
            min: 2,
            score: Math.min(experiences.length / 2, 1) * weights.experience,
            status: experiences.length >= 2 ? 'complete' : 'partial'
        },
        skills: {
            max: weights.skills,
            count: skills.length,
            min: 5,
            score: Math.min(skills.length / 5, 1) * weights.skills,
            status: skills.length >= 5 ? 'complete' : 'partial'
        },
        education: {
            max: weights.education,
            count: educations.length,
            min: 1,
            score: educations.length >= 1 ? weights.education : 0,
            status: educations.length >= 1 ? 'complete' : 'missing'
        }
    }

    const totalScore = Object.values(sections).reduce((sum, section) => sum + section.score, 0)
    const level = totalScore < 34 ? 'Beginner' : totalScore < 67 ? 'Intermediate' : 'Advanced'

    const missingItems = []
    if (!person.avatar) missingItems.push('Add a profile photo')
    if (!person.headline) missingItems.push('Add a professional headline')
    if (!person.summary || person.summary.length < 50) missingItems.push('Add a professional summary (min 50 characters)')
    if (experiences.length < 2) missingItems.push(`Add ${2 - experiences.length} more work experience(s)`)
    if (skills.length < 5) missingItems.push(`Add ${5 - skills.length} more skill(s)`)
    if (educations.length < 1) missingItems.push('Add your education history')

    return {
        score: Math.round(totalScore),
        level,
        sections,
        missingItems,
        nextSteps: missingItems.slice(0, 3) // Top 3 priorities
    }
}

New API Endpoint:

GET /api/v1/users/me/completeness
Response: {
    score: 65,
    level: "Intermediate",
    sections: {
        basicInfo: { max: 20, score: 20, status: "complete" },
        photo: { max: 10, score: 10, hasAvatar: true, status: "complete" },
        headline: { max: 10, score: 10, hasHeadline: true, status: "complete" },
        summary: { max: 15, score: 0, status: "missing", currentLength: 0, minLength: 50 },
        experience: { max: 20, score: 10, count: 1, min: 2, status: "partial" },
        skills: { max: 15, score: 9, count: 3, min: 5, status: "partial" },
        education: { max: 10, score: 0, count: 0, min: 1, status: "missing" }
    },
    missingItems: [
        "Add a professional summary (min 50 characters)",
        "Add 1 more work experience(s)",
        "Add 2 more skill(s)",
        "Add your education history"
    ],
    nextSteps: [
        "Add a professional summary (min 50 characters)",
        "Add 1 more work experience(s)",
        "Add 2 more skill(s)"
    ]
}

6.3 Notification Integration (Wait for Dev B)

Estimated Time: 8-10 hours Priority: 🟡 Important Status: ⏸️ Blocked (waiting for Dev B) Dependencies: Dev B must create notification infrastructure first

Current TODOs in Code:

// src/services/networkService.js:81
// TODO: Notification trigger - CONNECTION_REQUEST

// src/services/networkService.js:134
// TODO: Notification trigger - CONNECTION_ACCEPTED

// src/services/projectService.js:106
// TODO: Trigger email notification to invitee

// src/services/projectService.js:188
// TODO: Trigger notification to project owner

Dev B Must Complete First:

  • Create src/services/notificationService.js with create() method
  • Create src/controllers/notificationController.js
  • Create src/routes/v1/notificationRoute.js
  • Setup Socket.io in src/server.js
  • Create src/config/socket.js

Then Dev A Integration:

Files to Modify:

  1. src/services/networkService.js - Add notification triggers
  2. src/services/projectService.js - Add notification + email triggers
  3. src/services/socialService.js - Add notification triggers for likes, comments, shares

Implementation Pattern:

// After Dev B creates notificationService
import { notificationService } from './notificationService.js'

// In networkService.sendInvitation():
await notificationService.create({
    recipientId: targetMemberId,
    senderId: currentUserId,
    type: 'CONNECTION_REQUEST',
    referenceId: invitation._id,
    referenceType: 'ConnectionRequest',
    message: `${senderName} sent you a connection request`
})

// In networkService.acceptInvitation():
await notificationService.create({
    recipientId: invitation.senderId,
    senderId: currentUserId,
    type: 'CONNECTION_ACCEPTED',
    referenceId: invitation._id,
    referenceType: 'ConnectionRequest',
    message: `${accepterName} accepted your connection request`
})

// In projectService.inviteCollaborator():
await emailService.sendProjectInvitation(email, projectTitle, inviterName)
await notificationService.create({
    recipientId: inviteeId,
    senderId: currentUserId,
    type: 'PROJECT_INVITATION',
    referenceId: invitation._id,
    referenceType: 'ProjectInvitation',
    message: `${inviterName} invited you to collaborate on ${projectTitle}`
})

// In socialService.handleInteraction() - LIKE:
if (type === 'LIKE' && post.authorId.toString() !== userId) {
    await notificationService.create({
        recipientId: post.authorId,
        senderId: userId,
        type: 'LIKE',
        referenceId: postId,
        referenceType: 'Post',
        message: `${userName} liked your post`
    })
}

// In socialService.addComment():
if (post.authorId.toString() !== userId) {
    await notificationService.create({
        recipientId: post.authorId,
        senderId: userId,
        type: 'COMMENT',
        referenceId: commentId,
        referenceType: 'Comment',
        message: `${userName} commented on your post`
    })
}

Coordination: Schedule integration for Week 5 Day 4-5 after Dev B completes notification infrastructure.


Phase 7: Dev A Week 6 - Polish & Enhancement

Estimated Time: 17-22 hours Status: 🔄 Not Started Priority: Nice-to-have improvements

7.1 Smart Feed Algorithm

Estimated Time: 6-8 hours Priority: 🟢 Enhancement Status: 🔄 Not Started

Current Issue: Feed is purely chronological, no engagement-based ranking

Files to Modify:

  • src/services/socialService.js - Replace viewFeed() with scoring logic
  • src/controllers/socialController.js - Add optional query param algorithm=smart|chronological
  • src/routes/v1/socialRoute.js - Update Swagger docs

Implementation - Personalized Feed:

// src/services/socialService.js
const viewPersonalizedFeed = async (userId, page = 1, limit = 20) => {
    // Get user's connections for weighting
    const connectionRequests = await ConnectionRequest.find({
        $or: [
            { sender: userId, status: 'accepted' },
            { recipient: userId, status: 'accepted' }
        ],
        _destroy: false
    })
    
    const connectionIds = connectionRequests.map(cr => 
        cr.sender.toString() === userId ? cr.recipient.toString() : cr.sender.toString()
    )

    // Aggregation pipeline with scoring
    const posts = await Post.aggregate([
        { 
            $match: { 
                _destroy: false,
                $or: [
                    { visibility: 'public' },
                    { visibility: 'connections', authorId: { $in: connectionIds } }
                ]
            }
        },
        // Add engagement score
        {
            $addFields: {
                // Base engagement: likes + comments*2 + shares*3
                engagementScore: {
                    $add: [
                        { $size: { $ifNull: ['$likes', []] } },
                        { $multiply: [{ $size: { $ifNull: ['$comments', []] } }, 2] },
                        { $multiply: [{ $size: { $ifNull: ['$shares', []] } }, 3] }
                    ]
                },
                // Connection boost flag
                isConnection: { $in: ['$authorId', connectionIds.map(id => mongoose.Types.ObjectId(id))] },
                // Calculate hours since posted
                hoursOld: {
                    $divide: [
                        { $subtract: [new Date(), '$createdAt'] },
                        3600000 // milliseconds to hours
                    ]
                }
            }
        },
        // Calculate final score with decay
        {
            $addFields: {
                finalScore: {
                    $multiply: [
                        // Base score + connection bonus
                        {
                            $add: [
                                '$engagementScore',
                                { $cond: ['$isConnection', 50, 0] } // +50 for connections
                            ]
                        },
                        // Exponential decay: e^(-t/72) where t is hours
                        // 72-hour half-life means content older than 3 days has low visibility
                        {
                            $exp: {
                                $divide: [
                                    { $multiply: ['$hoursOld', -1] },
                                    72
                                ]
                            }
                        }
                    ]
                }
            }
        },
        // Sort by calculated score
        { $sort: { finalScore: -1, createdAt: -1 } },
        { $skip: (page - 1) * limit },
        { $limit: limit },
        // Populate author info
        {
            $lookup: {
                from: 'persons',
                localField: 'authorId',
                foreignField: '_id',
                as: 'author'
            }
        },
        { $unwind: '$author' }
    ])

    return posts
}

// Update viewFeed to support both modes
const viewFeed = async (userId, algorithm = 'smart', page = 1, limit = 20) => {
    if (algorithm === 'smart') {
        return await viewPersonalizedFeed(userId, page, limit)
    }
    
    // Fallback to chronological
    return await viewChronologicalFeed(userId, page, limit)
}

API Update:

GET /api/v1/feeds/main?algorithm=smart
Query params:
  - algorithm: enum (smart, chronological) default: smart
  - page: integer
  - limit: integer

7.2 Enhanced Profile Search

Estimated Time: 4-5 hours Priority: 🟢 Enhancement Status: 🔄 Not Started

Current Issue: Search only by name, limited filtering

Files to Modify:

  • src/services/searchService.js - Enhance searchMembers() with aggregation
  • src/validations/searchValidation.js - Add new filter schemas
  • src/routes/v1/searchRoute.js - Update Swagger docs

Implementation:

// src/services/searchService.js
const searchMembers = async (filters, page = 1, limit = 20) => {
    const { name, skills, location, company } = filters
    const skip = (page - 1) * limit
    
    const pipeline = []
    
    // Base match
    pipeline.push({ $match: { _destroy: false } })
    
    // Name search (existing)
    if (name && name.trim()) {
        const searchRegex = new RegExp(name.trim(), 'i')
        pipeline.push({
            $match: {
                $or: [
                    { firstName: searchRegex },
                    { lastName: searchRegex },
                    { $expr: {
                        $regexMatch: {
                            input: { $concat: ['$firstName', ' ', '$lastName'] },
                            regex: name.trim(),
                            options: 'i'
                        }
                    }}
                ]
            }
        })
    }
    
    // Skills filter (NEW)
    if (skills && skills.length > 0) {
        pipeline.push(
            {
                $lookup: {
                    from: 'skills',
                    localField: '_id',
                    foreignField: 'personId',
                    as: 'userSkills'
                }
            },
            {
                $match: {
                    'userSkills.name': { 
                        $in: skills.map(s => new RegExp(`^${s}$`, 'i')) 
                    }
                }
            }
        )
    }
    
    // Location filter (NEW)
    if (location && location.trim()) {
        const locationRegex = new RegExp(location.trim(), 'i')
        pipeline.push({
            $match: { location: locationRegex }
        })
    }
    
    // Company filter (NEW) - current or past
    if (company && company.trim()) {
        pipeline.push(
            {
                $lookup: {
                    from: 'experiences',
                    localField: '_id',
                    foreignField: 'personId',
                    as: 'experiences'
                }
            },
            {
                $match: {
                    'experiences.company': new RegExp(company.trim(), 'i')
                }
            }
        )
    }
    
    // Pagination
    pipeline.push(
        { $skip: skip },
        { $limit: limit }
    )
    
    const members = await Person.aggregate(pipeline)
    
    // Get total count (without pagination)
    const countPipeline = pipeline.filter(stage => !stage.$skip && !stage.$limit)
    countPipeline.push({ $count: 'total' })
    const countResult = await Person.aggregate(countPipeline)
    const totalCount = countResult[0]?.total || 0
    
    return {
        members,
        filters: { name, skills, location, company },
        pagination: {
            currentPage: page,
            totalPages: Math.ceil(totalCount / limit),
            totalCount,
            hasNextPage: page * limit < totalCount,
            hasPrevPage: page > 1
        }
    }
}

API Update:

GET /api/v1/search/members
Query params:
  - name: string (existing)
  - skills: string[] (NEW - comma-separated)
  - location: string (NEW)
  - company: string (NEW)
  - page, limit
  
Example: /api/v1/search/members?name=john&skills=JavaScript,React&location=San Francisco

7.3 Comment Threading

Estimated Time: 3-4 hours Priority: 🟢 Enhancement Status: 🔄 Not Started

Current Issue: Comments are flat, no nested reply structure

Files to Modify:

  • src/models/social/commentModel.js - Add depth field
  • src/services/socialService.js - Add getThreadedComments()
  • src/controllers/socialController.js - Add controller
  • src/routes/v1/socialRoute.js - Add route

Model Update:

// src/models/social/commentModel.js
// Add to schema:
depth: {
    type: Number,
    default: 0,
    max: 3,
    min: 0
}

// Update validation in addComment to enforce depth limit

Implementation:

// src/services/socialService.js
const getThreadedComments = async (postId, page = 1, limit = 20) => {
    const skip = (page - 1) * limit
    
    // Get top-level comments
    const topLevelComments = await Comment.aggregate([
        {
            $match: {
                postId: mongoose.Types.ObjectId(postId),
                parentCommentId: null,
                _destroy: false
            }
        },
        { $sort: { createdAt: -1 } },
        { $skip: skip },
        { $limit: limit },
        // Use $graphLookup to get all nested replies
        {
            $graphLookup: {
                from: 'comments',
                startWith: '$_id',
                connectFromField: '_id',
                connectToField: 'parentCommentId',
                as: 'replies',
                maxDepth: 2, // depth 0 + 2 = max 3 levels
                depthField: 'replyDepth',
                restrictSearchWithMatch: { _destroy: false }
            }
        },
        // Populate author info
        {
            $lookup: {
                from: 'persons',
                localField: 'authorId',
                foreignField: '_id',
                as: 'author'
            }
        },
        { $unwind: '$author' }
    ])
    
    // Build nested tree structure
    const threaded = topLevelComments.map(comment => buildCommentTree(comment))
    
    // Get total count of top-level comments
    const totalCount = await Comment.countDocuments({
        postId,
        parentCommentId: null,
        _destroy: false
    })
    
    return {
        comments: threaded,
        pagination: {
            currentPage: page,
            totalPages: Math.ceil(totalCount / limit),
            totalCount,
            hasNextPage: page * limit < totalCount,
            hasPrevPage: page > 1
        }
    }
}

const buildCommentTree = (comment) => {
    const replies = comment.replies || []
    
    // Build tree recursively
    const directReplies = replies
        .filter(r => r.parentCommentId?.toString() === comment._id.toString())
        .map(reply => {
            const nestedReplies = replies.filter(
                rr => rr.parentCommentId?.toString() === reply._id.toString()
            )
            return {
                ...reply,
                replies: nestedReplies.map(nr => ({
                    ...nr,
                    replies: [] // Max depth 3, no more nesting
                }))
            }
        })
    
    return {
        ...comment,
        replies: directReplies
    }
}

New API Endpoint:

GET /api/v1/posts/:id/comments/threaded
Query params:
  - page: integer
  - limit: integer

Response: {
    comments: [
        {
            id: "...",
            content: "Top level comment",
            author: {...},
            replies: [
                {
                    id: "...",
                    content: "Reply to top level",
                    author: {...},
                    replies: [
                        {
                            id: "...",
                            content: "Reply to reply",
                            author: {...},
                            replies: [] // Max depth
                        }
                    ]
                }
            ]
        }
    ],
    pagination: {...}
}

7.4 Testing & Documentation

Estimated Time: 4-5 hours Priority: 🔴 Critical Status: 🔄 Not Started

Tasks:

  1. Integration Testing (2-3 hours)

    • Test Profile CRUD + media uploads
    • Test Post creation with media files
    • Test Comment + Reply flow
    • Test Connection request/accept flow
    • Test Search with all filters
    • Test Project collaboration flow
  2. API Documentation (1-2 hours)

    • Verify all Swagger docs are complete
    • Test all endpoints in Swagger UI
    • Create Postman collection for Dev A domain
    • Document authentication flow
  3. Setup Documentation (1 hour)

    • Update README with:
      • Cloudinary setup instructions
      • Environment variables for Dev A domain
      • How to test media uploads locally
      • How to use the API

Environment Variables Checklist:

# Required for Dev A domain:
CLOUDINARY_CLOUD_NAME=your-cloud-name
CLOUDINARY_API_KEY=your-api-key
CLOUDINARY_API_SECRET=your-api-secret
BREVO_API_KEY=your-brevo-key (already configured)
EMAIL_SENDER_EMAIL=noreply@networkr.com (already configured)

Dev A Implementation Summary

Week 5 (Core Enhancements)

Day Task Hours Priority Status
1-2 Media Uploads (Profile + Social) 8-10 🔴 Critical 🔄 Not Started
3 Profile Completeness API 4-5 🟡 Important 🔄 Not Started
4-5 Notification Integration 8-10 🟡 Important ⏸️ Blocked by Dev B

Week 6 (Polish & Enhancement)

Day Task Hours Priority Status
1-2 Smart Feed Algorithm 6-8 🟢 Nice-to-have 🔄 Not Started
3 Enhanced Profile Search 4-5 🟢 Nice-to-have 🔄 Not Started
4 Comment Threading 3-4 🟢 Nice-to-have 🔄 Not Started
5 Testing & Documentation 4-5 🔴 Important 🔄 Not Started

Total Estimated Time: 37-47 hours

Priority Recommendation

Must Do (Week 5):

  1. ✅ Media Uploads - Users need real photo upload capability
  2. ✅ Profile Completeness - Guides user onboarding
  3. ⏸️ Notification Integration - Wait for Dev B, then integrate triggers

Should Do (Week 6): 4. ⭐ Smart Feed - Better content discovery and engagement 5. ⭐ Enhanced Search - More useful member search 6. ✅ Testing & Documentation - Always critical

Could Do (Week 6): 7. 📝 Comment Threading - Better discussion UX

Coordination with Dev B

Dev B must complete first (Week 5 Day 1-4):

  • Notification Service infrastructure (src/services/notificationService.js)
  • Notification Controller & Routes (src/controllers/notificationController.js)
  • Socket.io setup (src/config/socket.js and src/server.js)

Then Dev A integrates (Week 5 Day 4-5):

  • Add notification triggers in networkService, projectService, socialService
  • Test real-time notification delivery
  • Verify email notifications for project invitations

Quick Reference: Dev A API Endpoints

Phase 1 (Auth) ✅ Completed

Method Endpoint Description
POST /registrations Register (OTP expires in 10 min)
POST /verifications Verify OTP (checks expiry)
POST /verifications/resend Resend OTP (sets new expiry)
POST /password-resets Request password reset
PATCH /password-resets Complete reset with token
PATCH /sessions/password Change password

Phase 2-4 ✅ Completed

Method Endpoint Description
GET /users Search members
GET /users/me Get my profile
PATCH /users/me/profile Edit summary
POST /users/me/experiences Add experience
POST /posts Create post (URL-based media)
POST /posts/:id/interactions Like/Share/Save
POST /posts/:id/comments Add comment
POST /network/invitations Send connection
PUT /network/invitations/:id/accept Accept connection
GET /search/members Search members
GET /search/companies Search companies
POST /search/candidates/filter Filter candidates
POST /projects/:id/collaborators/invite Invite to project

Phase 6 (Week 5) 🔄 In Progress

Method Endpoint Description
POST /users/me/avatar Upload avatar (5MB)
POST /users/me/background Upload background (10MB)
POST /posts Create post with media files
GET /users/me/completeness Profile score & tips

Phase 7 (Week 6) - Planned

Method Endpoint Description
GET /feeds/main?algorithm=smart Personalized feed
GET /search/members Enhanced search with filters
GET /posts/:id/comments/threaded Nested comments