- Overview
- System Architecture
- Setup & Installation
- Authentication & User Management
- Core Features
- Technical Implementations
- Security
- Deployment
- Known Isuues
SkillVerse is a comprehensive freelance marketplace platform that connects clients with freelancers across various service categories. It is a platform that facilitates job posting, job applications, milestone tracking, file sharing, and secure payment processing. The backend is built with Node.js, Express, and MongoDB, providing RESTful API endpoints for the frontend to interact with. The frontend is built with React and Bootstrap CSS, providing users with a responsive, easy-to-navigate, modern UI.
The app is built using the MERN stack - MongoDB, Express, React, and Node.js. Specific technology stacks for the frontend and backend are given below.
Frontend
- React 18+ with functional components and hooks
- React Router for client-side navigation
- Axios for HTTP requests with credentials
- React Bootstrap for UI components
- Bootstrap CSS for modern styling
Backend
- Node.js with Express.js framework
- MongoDB with Mongoose ORM
- Passport.js with Google OAuth 2.0
- Multer for file management
- Express-session with MongoStore
External Services
- Google OAuth for user authentication
- Paystack for payment processing
- Azure Web Services for hosting
- Node.js (v16.0.0 or higher)
- MongoDB (v5.0 or higher)
- npm or yarn
- Google OAuth credentials
- Paystack API keys
MONGO_URI:<your-mongo-uri>
PORT:<port>
GOOGLE_CLIENT_ID:<your-google-client-id>
GOOGLE_CLIENT_SECRET:<your-google-client-secret>
COOKIE_KEY:<your-cookie-key>
PAYSTACK_SECRET_KEY:<your-paystack-secret-key>-
Clone the Repository
git clone https://github.com/SkillVerser/freelancer-management-platform.git cd freelancer-management-platform -
Install Dependencies
# Install backend dependencies cd backend npm install
# Install frontend dependencies cd frontend npm install
-
Start Development Servers
# Start backend server cd backend npm run dev
# Start frontend server cd frontend npm start
-
Visit Site
- Frontend is hosted at
localhost:3000by default
- Frontend is hosted at
The app makes use of Passport.js with the Google OAuth 2.0 strategy for user authentication. Users sign up and login via Google, and no sensitive information, such as emails or passwords, are stored in the database.
- User initiates Google OAuth login
- Authentication callback processes user data
- New users: redirect to role selection (client / freelancer)
- Existing users: redirect to role-appropriate dashboard
- Session persistence with MongoDB store and express-session
Clients
- Post service requests different categories
- Review freelancer applications
- Create and track project milestones with payment integration
- Access project files and monitor progress
- View comprehensive job dashboard with progress indicators
Freelancers
- Browse and apply for available projects
- Manage profile, skills, and portfolio
- Upload deliverables and update project progress in real-time
- View accepted jobs dashboard with project details
Admins
- Monitor comprehensive platform statistics and analytics
- Manage user accounts and role change requests
- Oversee all job listings
- Process support tickets and user issues
- Delete user accounts and manage permissions
Sessions are managed using express-session with MongoDB persistence. This ensures scalability across multiple server instances.
app.use(
session({
secret: process.env.COOKIE_KEY,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000,
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
secure: process.env.NODE_ENV === "production",
},
store: MongoStore.create({
mongoUrl: process.env.MONGO_URI,
collectionName: "sessions",
}),
})
);The service request system forms the core of the marketplace, allowing clients to post jobs and freelancers to apply for them.
The platform supports five primary service categories:
- Software Development: Custom application and web development
- Data Science: Analytics, machine learning, and data processing
- Creating Logos: Brand identity and logo design
- Graphic Designer: Visual design and creative services
- Digital Marketing: SEO, social media, and marketing campaigns
- Browse Jobs: View available job listings with filtering options
- Job Selection: Choose a job to apply for with detailed requirements
- Application Form: Submit cover letter and proposed fee
- Validation: Form validation with error handling
- Confirmation: Receive success/error feedback
- Tracking: Monitor application status and responses
- View all applications for posted jobs
- Review freelancer profiles and proposed fees
- Accept suitable applications with confirmation workflow
- Navigate to project management upon acceptance
Milestones break large projects into manageable chunks and provide clear progress tracking for both parties.
- Dynamic Creation: Add/remove milestones during project setup
- Payment Integration: Milestone-based payments
- Date Management: Due date tracking with formatted display
- Status Management: Track completion status with visual indicators
The dashboard provides comprehensive project oversight with real-time updates and intuitive navigation, to both clients and freelancers.
- Job Overview: Grid display of active projects with status indicators
- Payment Tracking: Outstanding payments and payment history
- Milestone Management: Create and track project milestones
- File Access: Access deliverables uploaded by freelancer
- Application Management: Review and manage freelancer applications
- Progress Monitoring: Real-time project status updates and completion percentages
- No Freelancer Assigned: "View Applications" button
- Freelancer Assigned: "View Details" button for project management
- Progress Visualization: Completion percentages
- Payment Status: Amount owed calculations and payment buttons
- Active Projects: Display of accepted jobs with client information
- Project Details: Access to job requirements and milestone information
- File Management: Upload and manage project deliverables
- Progress Updates: Mark milestones as complete and update status
- Payment Information: View received and outstanding payments
This section provides the technical implementations of various aspects of the app in the form of code snippets.
The following are the core models of the system. There are others not shown, but they are are significantly less importance
//Application Model
{
jobId: {
type: mongoose.Schema.Types.ObjectId,
ref: "ServiceRequest",
required: true,
},
freelancerId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
coverLetter: {
type: String,
required: false,
},
price: {
type: Number,
required: true,
},
status: {
type: String,
default: "Pending",
enum: ["Pending", "Accepted", "Rejected"],
},
createdAt: {
type: Date,
default: Date.now,
},
}
//File Model
{
jobId: {
type: mongoose.Schema.Types.ObjectId,
ref: "ServiceRequest",
required: true,
},
filename: {
type: String,
required: true,
},
originalName: {
type: String,
required: true,
},
fileSize: {
type: Number,
required: true,
},
uploadedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
contentType: {
type: String,
required: true,
},
storagePath: {
type: String,
required: true,
},
}
//Milestone Model
{
jobId: {
type: mongoose.Schema.Types.ObjectId,
ref: "ServiceRequest",
required: true
},
description: {
type: String,
required: true
},
dueDate: {
type: Date,
required: true
},
status: {
type: String,
enum: ["Pending", "Completed"],
default: "Pending"
}
}
//ServiceRequest Model
{
clientId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
serviceType: {
type: String,
required: true,
},
freelancerId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
default: null,
},
status: {
type: String,
enum: ["Pending", "Accepted", "Completed", "In Progress"],
default: "Pending",
},
progressActual: {
type: Number,
default: 0,
},
progressPaid: {
type: Number,
default: 0,
},
price: {
type: Number,
default: 0,
},
paymentsMade: {
type: Number,
default: 0,
},
paymentsPending: {
type: Number,
default: 0,
},
}
//User Model
{
googleID: {
type: String,
required: true,
unique: true,
},
username: {
type: String,
required: true,
unique: false,
},
role: {
type: String,
enum: ["client", "freelancer", "admin"],
},
}-
Service Request Creation
const handleServiceSelection = async (category) => { const response = await axios.post( `${API_URL}/api/service-requests/create`, { clientId: user._id, serviceType: category, description: `Request for ${category}`, }, { withCredentials: true } ); };
-
Application Submission
const handleSubmitApplication = async (e) => { e.preventDefault(); await axios.post( `${API_URL}/api/service-requests/applications`, { jobId, freelancerId: user._id, coverLetter, proposedFee: parseFloat(fee), }, { withCredentials: true } ); };
-
Accept Application
const handleAcceptConfirmed = async () => { try { await axios.post( `${API_URL}/api/applications/jobs/accept/${pendingApplicationId}`, {}, { withCredentials: true } ); setShowConfirmModal(false); setShowSuccessModal(true); } catch (error) { console.error("Error accepting application:", error); alert("Error accepting application"); } };
-
Milestone Completion Update
const handleMilestoneComplete = async (milestoneId) => { const res = await fetch( `${API_URL}/api/milestones/complete/${milestoneId}`, { method: "PUT", credentials: "include", } ); };
-
File Upload Configuration
const storage = multer.diskStorage({ destination: function (req, file, cb) { const uploadDir = path.join(__dirname, "../uploads"); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } cb(null, uploadDir); }, filename: function (req, file, cb) { const uniqueName = Date.now() + "-" + file.originalname + path.extname(file.originalname); cb(null, uniqueName); }, }); const upload = multer({ storage: storage, limits: { fileSize: 5 _ 1024 _ 1024 }, });
const { progressActual, progressPaid } = serviceRequest;
const progressDelta = progressActual - progressPaid;
const amountDue = (progressDelta / 100) * serviceRequest.price;-
Client
const fetchJobs = async () => { if (!userId || userId === null) { console.error("User ID is not available or null"); return; } try { const res = await fetch( `${API_URL}/api/service-requests/client/jobs/${userId}`, { method: "GET", credentials: "include", } ); if (res.ok) { const data = await res.json(); setJobs(data); setError(""); setSuccess("Jobs fetched successfully!"); console.log(success); } else { setError("Failed to fetch jobs"); console.log(error); } } catch (err) { setError("Error fetching jobs: " + err.message); } };
-
Freelancer
const fetchAcceptedJobs = async (freelancerId) => { try { const res = await fetch( `${API_URL}/api/service-requests/freelancer/jobs/${freelancerId}`, { credentials: "include", } ); if (res.ok) { const jobsData = await res.json(); setAcceptedJobs(jobsData); console.log("Accepted jobs:", jobsData); } else { console.error("Failed to fetch accepted jobs"); } } catch (err) { console.error("Error fetching accepted jobs:", err); } finally { setLoading(false); } };
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/admin/stats |
Get platform statistics for admin dashboard |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/applications/jobs/:jobId |
Get all applications for a job |
| POST | /api/applications/jobs/accept/:applicationId |
Accept a freelancer's application |
| Method | Endpoint | Description |
|---|---|---|
| GET | /auth/google |
Initiates Google OAuth authentication |
| GET | /auth/google/callback |
Google OAuth callback URL |
| GET | /auth/logout |
Logs out the user |
| GET | /auth/me |
Returns the currently authenticated user's data |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/files/upload/:jobId |
Upload a file for a job |
| GET | /api/files/job/:jobId |
Get all files for a job |
| GET | /api/files/download/:fileId |
Download a specific file |
| DELETE | /api/files/delete/:fileId |
Delete a file |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/milestones/:jobId |
Create milestones for a job |
| GET | /api/milestones/job/:jobId |
Get all milestones for a job |
| PUT | /api/milestones/complete/:milestoneId |
Mark a milestone as completed |
| Method | Endpoint | Description |
|---|---|---|
| POST | /payments/create-checkout-session |
Create a Paystack checkout session |
| POST | /webhook/paystack |
Webhook endpoint for Paystack payment notifications |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/service-requests/create |
Create a new service request |
| GET | /api/service-requests/all |
Get all available service requests |
| POST | /api/service-requests/applications |
Submit an application for a service request |
| GET | /api/service-requests/client/jobs/:id |
Get all jobs posted by a client |
| GET | /api/service-requests/manage-jobs |
Get all service requests (admin function) |
| GET | /api/service-requests/freelancer/jobs/:id |
Get all jobs assigned to a freelancer |
| GET | /api/service-requests/job/:id |
Get details of a specific job |
| Method | Endpoint | Description |
|---|---|---|
| POST | /users |
Create a new user |
| PUT | /users/:id |
Update a user |
| GET | /users |
Get all users (admin function) |
| DELETE | /users/:id |
Delete a user (admin function) |
| POST | /users/create-user |
Create or update user profile details |
| POST | /users/set-role |
Set a user's role after signup |
| POST | /users/request-role-change |
Request a role change |
| GET | /users/alltickets |
Get all pending role change requests |
| POST | /users/process-request |
Process a role change request |
| GET | /users/freelancer/:freelancerId |
Get freelancer details |
| GET | /users/profile/:userId |
Get a user's profile details |
- 3rd-Party authentication via Google OAuth for secure authentication
- Session encryption with
express-sessionand MongoDB persistence - Paystack webhook for signature validation for payments
- Secure and unique file naming and file upload restrictions
- Role-based access control
- CORS protection for cross-origin requests
This app has been deployed via an Azure Static Web App, and be accessed from this URL: https://lively-coast-034460503.6.azurestaticapps.net/
Currently, the file sharing system makes use of local storage on the server. In a real-world app, this presents persistence issues, as the contents in the server's local storage would be lost upon server restarts. Additionally, data in local storage is not shared across instances, which vastly limits the scalability of the app.
Ideally, file sharing would be implemented using Azure Blob Storage. This has several benefits:
- Scalable: Can store much more data
- Accessible: Files are available to all instances of the application
- Resilient: Files aren't affected by server restarts or scaling events
However, since this app was built as a university project, and is not intended for real-world use, the local storage implementation, which is suitable for demonstration purporses, is sufficent.