Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# Environment variables
backend/.env

# Uploads
backend/uploads/
506 changes: 472 additions & 34 deletions README.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
PORT=5000
DATABASE_URL=postgresql://username:password@localhost:5432/database_name
JWT_SECRET=your_jwt_secret_here
NODE_ENV=development
210 changes: 210 additions & 0 deletions backend/controllers/adminController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { query } from '../db.js';


/**
* @desc Get aggregate platform metrics (user/host/venue counts, platform earnings, volume)
* @route GET /api/admin/stats
* @access Private (Admin role only)
*/
export const getDashboardStats = async (req, res) => {
try {
// user counts
const usersCountRes = await query("SELECT COUNT(*)::int AS count FROM users WHERE role = 'user'");
const hostsCountRes = await query("SELECT COUNT(*)::int AS count FROM users WHERE role = 'venue_owner'");

// venue counts
const venuesCountRes = await query("SELECT COUNT(*)::int AS total, COUNT(*) FILTER (WHERE status = 'pending')::int AS pending, COUNT(*) FILTER (WHERE status = 'approved')::int AS approved FROM venues");

// financialtransactions
const financialRes = await query(`
SELECT COALESCE(SUM(total_price), 0)::int AS "totalVolume"
FROM bookings
WHERE payment_status = 'paid' AND status != 'cancelled'
`);

const totalVolume = financialRes.rows[0].totalVolume;
const platformFeePercentage = 10;
const platformEarnings = Math.round(totalVolume * (platformFeePercentage / 100));
const hostEarnings = totalVolume - platformEarnings;

res.json({
success: true,
stats: {
totalUsers: usersCountRes.rows[0].count,
totalHosts: hostsCountRes.rows[0].count,
totalVenues: venuesCountRes.rows[0].total,
pendingVenues: venuesCountRes.rows[0].pending,
approvedVenues: venuesCountRes.rows[0].approved,
totalVolume,
platformFeePercentage,
platformEarnings,
hostEarnings
}
});
} catch (error) {
console.error('Error fetching dashboard stats:', error);
res.status(500).json({ message: 'Error fetching admin dashboard stats' });
}
};

/**
* @desc Get all venues across the platform for approval review
* @route GET /api/admin/venues
* @access Private (Admin role only)
*/
export const getAllVenues = async (req, res) => {
try {
const result = await query(`
SELECT v.*, u.name AS "hostName", u.email AS host_email
FROM venues v
LEFT JOIN users u ON v.host_id = u.id
ORDER BY v.id DESC
`);

// Map database rows using same camelCase logic
const mappedVenues = result.rows.map(row => ({
id: row.id.toString(),
title: row.title,
description: row.description,
location: row.location,
fullAddress: row.full_address,
latitude: row.latitude !== null && row.latitude !== undefined ? Number(row.latitude) : null,
longitude: row.longitude !== null && row.longitude !== undefined ? Number(row.longitude) : null,
capacity: Number(row.capacity),
squareFeet: Number(row.square_feet),
pricePerNight: Number(row.price_per_night),
hostEmail: row.host_email,
hostName: row.hostName || 'Unknown Host',
hostType: row.host_type,
rating: Number(row.rating),
isTopRated: row.is_top_rated,
dateRange: row.date_range,
parking: row.parking,
catering: row.catering,
images: row.images,
amenities: row.amenities,
rules: row.rules,
eventTypes: row.event_types,
status: row.status || 'pending',
rejectionReason: row.rejection_reason || '',
createdAt: row.created_at
}));

res.json(mappedVenues);
} catch (error) {
console.error('Error fetching all venues:', error);
res.status(500).json({ message: 'Error fetching venues list' });
}
};

/**
* @desc Approve or decline a pending venue listing
* @route PUT /api/admin/venues/:id/status
* @access Private (Admin role only)
*/
export const updateVenueStatus = async (req, res) => {
const { id } = req.params;
const { status, rejectionReason } = req.body; // 'approved' or 'declined'

if (!status || !['approved', 'declined', 'pending'].includes(status)) {
return res.status(400).json({ message: 'Invalid status value. Must be approved, declined, or pending.' });
}

try {
const checkRes = await query('SELECT * FROM venues WHERE id = $1', [id]);
if (checkRes.rows.length === 0) {
return res.status(404).json({ message: 'Venue not found' });
}

let result;
if (status === 'declined') {
result = await query(
'UPDATE venues SET status = $1, rejection_reason = $2 WHERE id = $3 RETURNING *',
[status, rejectionReason || '', id]
);
} else {
result = await query(
'UPDATE venues SET status = $1, rejection_reason = NULL WHERE id = $2 RETURNING *',
[status, id]
);
}

res.json({
success: true,
message: `Venue status successfully updated to ${status}`,
venue: result.rows[0]
});
} catch (error) {
console.error('Error updating venue status:', error);
res.status(500).json({ message: 'Error updating venue status' });
}
};

/**
* @desc Get list of all bookings across the platform
* @route GET /api/admin/bookings
* @access Private (Admin role only)
*/
export const getAllBookings = async (req, res) => {
try {
const result = await query(`
SELECT
b.id,
b.venue_id AS "venueId",
v.title AS "venueTitle",
v.location AS "venueLocation",
v.full_address AS "fullAddress",
v.latitude,
v.longitude,
v.images[1] AS "venueImage",
b.start_date AS "startDate",
b.end_date AS "endDate",
b.guests,
b.total_price AS "totalPrice",
b.status,
b.booking_date AS "bookingDate",
b.payment_status AS "paymentStatus",
b.created_at AS "createdAt",
COALESCE(b.renter_name, u.name) AS "renterName",
COALESCE(b.renter_email, u.email) AS "renterEmail",
b.renter_phone AS "renterPhone",
h.name AS "hostName",
h.email AS "hostMail"
FROM bookings b
JOIN venues v ON b.venue_id = v.id
JOIN users h ON v.host_id = h.id
LEFT JOIN users u ON b.user_id = u.id
ORDER BY b.created_at DESC
`);

const mappedBookings = result.rows.map(row => ({
...row,
latitude: row.latitude !== null && row.latitude !== undefined ? Number(row.latitude) : null,
longitude: row.longitude !== null && row.longitude !== undefined ? Number(row.longitude) : null,
}));

res.json(mappedBookings);
} catch (error) {
console.error('Error fetching all bookings:', error);
res.status(500).json({ message: 'Error fetching bookings' });
}
};

/**
* @desc Get list of all registered platform users and hosts
* @route GET /api/admin/users
* @access Private (Admin role only)
*/
export const getAllUsers = async (req, res) => {
try {
const result = await query(`
SELECT id, name, email, role, created_at AS "createdAt"
FROM users
ORDER BY role ASC, id ASC
`);
res.json(result.rows);
} catch (error) {
console.error('Error fetching all users:', error);
res.status(500).json({ message: 'Error fetching users list' });
}
};
122 changes: 122 additions & 0 deletions backend/controllers/authController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { query } from '../db.js';

/**
* Generates a signed JSON Web Token (JWT) containing user identifiers.
* The token has a standard expiration duration of 7 days.
* @param {number|string} id - The user database identifier
* @param {string} email - The user email address
* @param {string} role - The user authority role ('user', 'venue_owner', 'admin')
* @returns {string} Signed JWT token string
*/
const generateToken = (id, email, role) => {
return jwt.sign(
{ id, email, role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
};

/**
* @desc Register a new user profile with hashed credentials
* @route POST /api/auth/signup
* @access Public (Enforces lowercase emails, basic length limits, and restricts admin self-creation)
*/
export const registerUser = async (req, res) => {
const { name, email, password, role } = req.body;

try {
// 1. Basic validation
if (!name || !email || !password) {
return res.status(400).json({ message: 'Please provide name, email, and password' });
}

if (password.length < 6) {
return res.status(400).json({ message: 'Password must be at least 6 characters long' });
}

// 2. Check if user already exists
const emailCheckResult = await query('SELECT * FROM users WHERE email = $1', [email.toLowerCase().trim()]);
if (emailCheckResult.rows.length > 0) {
return res.status(400).json({ message: 'A user with this email address already exists' });
}

// 3. Secure password hashing
const salt = await bcrypt.genSalt(15);
const hashedPassword = await bcrypt.hash(password, salt);

// Validate and restrict role to prevent unauthorized admin creations
const userRole = (role === 'venue_owner' || role === 'user') ? role : 'user';

// 4. Insert new user into database
const insertResult = await query(
'INSERT INTO users (name, email, password, role) VALUES ($1, $2, $3, $4) RETURNING id, name, email, role',
[name.trim(), email.toLowerCase().trim(), hashedPassword, userRole]
);

const newUser = insertResult.rows[0];

// 5. Generate and send JWT response
const token = generateToken(newUser.id, newUser.email, newUser.role);

res.status(201).json({
success: true,
user: {
name: newUser.name,
email: newUser.email,
role: newUser.role
},
token
});
} catch (error) {
console.error('Error during registration:', error);
res.status(500).json({ message: 'Server error during user registration. Please try again.' });
}
};

/**
* @desc Authenticate user credentials and generate active session JWT token
* @route POST /api/auth/login
* @access Public (Validates email and bcrypt password match)
*/
export const loginUser = async (req, res) => {
const { email, password } = req.body;

try {
// 1. Validation
if (!email || !password) {
return res.status(400).json({ message: 'Please provide email and password' });
}

// 2. Search for user by email
const userResult = await query('SELECT * FROM users WHERE email = $1', [email.toLowerCase().trim()]);
if (userResult.rows.length === 0) {
return res.status(401).json({ message: 'Invalid email or password' });
}

const user = userResult.rows[0];

// 3. Verify bcrypt-hashed password
const isPasswordCorrect = await bcrypt.compare(password, user.password);
if (!isPasswordCorrect) {
return res.status(401).json({ message: 'Invalid email or password' });
}

// 4. Generate JWT
const token = generateToken(user.id, user.email, user.role);

res.json({
success: true,
user: {
name: user.name,
email: user.email,
role: user.role
},
token
});
} catch (error) {
console.error('Error during login:', error);
res.status(500).json({ message: 'Server error during login. Please try again.' });
}
};
Loading