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
5,803 changes: 5,803 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"dotenv": "^17.0.0",
"express": "^5.1.0",
"express-jwt": "^8.5.1",
"express-oauth2-jwt-bearer": "^1.6.1",
"file-saver": "^2.0.5",
"html-to-image": "^1.11.13",
"html2canvas": "^1.4.1",
Expand Down
2 changes: 2 additions & 0 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,14 @@ const savedChartsRouter = require('./routes/savedCharts');
const healthRouter = require('./routes/health');
const userProfileRouter = require('./routes/userProfile');
const notificationsRouter = require('./routes/notifications');
const adminRouter = require('./routes/admin');

app.use('/history', historyRouter);
app.use('/saved-charts', savedChartsRouter);
app.use('/health', healthRouter);
app.use('/user-profile', userProfileRouter);
app.use('/notifications', notificationsRouter);
app.use('/admin', adminRouter);

// Legacy health endpoint for backward compatibility
app.get('/ping', (req, res) => {
Expand Down
14 changes: 14 additions & 0 deletions server/middleware/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { auth } = require('express-oauth2-jwt-bearer');
const dotenv = require('dotenv');
const path = require('path');

if (process.env.NODE_ENV !== 'production') {
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
}

const checkJwt = auth({
audience: process.env.AUTH0_AUDIENCE,
issuerBaseURL: process.env.AUTH0_ISSUER_BASE_URL,
});

module.exports = checkJwt;
5 changes: 5 additions & 0 deletions server/models/userProfile.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,11 @@ const userProfileSchema = new Schema({
min: 0,
max: 100,
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user',
},
}, {
timestamps: true,
toJSON: { virtuals: true },
Expand Down
128 changes: 128 additions & 0 deletions server/routes/admin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
const express = require('express');
const router = express.Router();
const UserProfile = require('../models/userProfile.model');
const SavedChart = require('../models/savedChart.model');
const FileHistory = require('../models/fileHistory.model');

const checkJwt = require('../middleware/auth');

// Middleware to check for admin role
const isAdmin = async (req, res, next) => {
try {
const user = await UserProfile.findOne({ userId: req.auth.payload.sub });
if (user && user.role === 'admin') {
next();
} else {
res.status(403).json({ message: 'Forbidden' });
}
} catch (error) {
res.status(500).json({ message: 'Internal Server Error' });
}
};

// Apply admin middleware to all routes in this file
router.use(checkJwt, isAdmin);

// User Management
router.get('/users', async (req, res) => {
try {
const users = await UserProfile.find();
res.json(users);
} catch (error) {
res.status(500).json({ message: 'Error fetching users' });
}
});

router.put('/users/:userId/role', async (req, res) => {
try {
const { role } = req.body;
if (!['user', 'admin'].includes(role)) {
return res.status(400).json({ message: 'Invalid role' });
}
const updatedUser = await UserProfile.findOneAndUpdate({ userId: req.params.userId }, { role }, { new: true });
if (!updatedUser) {
return res.status(404).json({ message: 'User not found' });
}
res.json(updatedUser);
} catch (error) {
res.status(500).json({ message: 'Error updating user role' });
}
});

router.delete('/users/:userId', async (req, res) => {
try {
const deletedUser = await UserProfile.findOneAndDelete({ userId: req.params.userId });
if (!deletedUser) {
return res.status(404).json({ message: 'User not found' });
}
res.json({ message: 'User deleted successfully' });
} catch (error) {
res.status(500).json({ message: 'Error deleting user' });
}
});

// Chart Management
router.get('/charts', async (req, res) => {
try {
const charts = await SavedChart.find({ isPublic: true });
res.json(charts);
} catch (error) {
res.status(500).json({ message: 'Error fetching public charts' });
}
});

router.delete('/charts/:chartId', async (req, res) => {
try {
const deletedChart = await SavedChart.findByIdAndDelete(req.params.chartId);
if (!deletedChart) {
return res.status(404).json({ message: 'Chart not found' });
}
res.json({ message: 'Chart deleted successfully' });
} catch (error) {
res.status(500).json({ message: 'Error deleting chart' });
}
Comment on lines +66 to +83
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation using spaces instead of the established 2-space pattern used throughout the rest of the file.

Suggested change
try {
const charts = await SavedChart.find({ isPublic: true });
res.json(charts);
} catch (error) {
res.status(500).json({ message: 'Error fetching public charts' });
}
});
router.delete('/charts/:chartId', async (req, res) => {
try {
const deletedChart = await SavedChart.findByIdAndDelete(req.params.chartId);
if (!deletedChart) {
return res.status(404).json({ message: 'Chart not found' });
}
res.json({ message: 'Chart deleted successfully' });
} catch (error) {
res.status(500).json({ message: 'Error deleting chart' });
}
try {
const charts = await SavedChart.find({ isPublic: true });
res.json(charts);
} catch (error) {
res.status(500).json({ message: 'Error fetching public charts' });
}
});
router.delete('/charts/:chartId', async (req, res) => {
try {
const deletedChart = await SavedChart.findByIdAndDelete(req.params.chartId);
if (!deletedChart) {
return res.status(404).json({ message: 'Chart not found' });
}
res.json({ message: 'Chart deleted successfully' });
} catch (error) {
res.status(500).json({ message: 'Error deleting chart' });
}

Copilot uses AI. Check for mistakes.
});

// File Management
router.get('/files', async (req, res) => {
try {
const files = await FileHistory.find();
res.json(files);
} catch (error) {
res.status(500).json({ message: 'Error fetching files' });
}
});

router.delete('/files/:fileId', async (req, res) => {
try {
const deletedFile = await FileHistory.findByIdAndDelete(req.params.fileId);
if (!deletedFile) {
return res.status(404).json({ message: 'File not found' });
}
res.json({ message: 'File deleted successfully' });
} catch (error) {
res.status(500).json({ message: 'Error deleting file' });
}
Comment on lines +66 to +105
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation using spaces instead of the established 2-space pattern used throughout the rest of the file.

Suggested change
try {
const charts = await SavedChart.find({ isPublic: true });
res.json(charts);
} catch (error) {
res.status(500).json({ message: 'Error fetching public charts' });
}
});
router.delete('/charts/:chartId', async (req, res) => {
try {
const deletedChart = await SavedChart.findByIdAndDelete(req.params.chartId);
if (!deletedChart) {
return res.status(404).json({ message: 'Chart not found' });
}
res.json({ message: 'Chart deleted successfully' });
} catch (error) {
res.status(500).json({ message: 'Error deleting chart' });
}
});
// File Management
router.get('/files', async (req, res) => {
try {
const files = await FileHistory.find();
res.json(files);
} catch (error) {
res.status(500).json({ message: 'Error fetching files' });
}
});
router.delete('/files/:fileId', async (req, res) => {
try {
const deletedFile = await FileHistory.findByIdAndDelete(req.params.fileId);
if (!deletedFile) {
return res.status(404).json({ message: 'File not found' });
}
res.json({ message: 'File deleted successfully' });
} catch (error) {
res.status(500).json({ message: 'Error deleting file' });
}
try {
const charts = await SavedChart.find({ isPublic: true });
res.json(charts);
} catch (error) {
res.status(500).json({ message: 'Error fetching public charts' });
}
});
router.delete('/charts/:chartId', async (req, res) => {
try {
const deletedChart = await SavedChart.findByIdAndDelete(req.params.chartId);
if (!deletedChart) {
return res.status(404).json({ message: 'Chart not found' });
}
res.json({ message: 'Chart deleted successfully' });
} catch (error) {
res.status(500).json({ message: 'Error deleting chart' });
}
});
// File Management
router.get('/files', async (req, res) => {
try {
const files = await FileHistory.find();
res.json(files);
} catch (error) {
res.status(500).json({ message: 'Error fetching files' });
}
});
router.delete('/files/:fileId', async (req, res) => {
try {
const deletedFile = await FileHistory.findByIdAndDelete(req.params.fileId);
if (!deletedFile) {
return res.status(404).json({ message: 'File not found' });
}
res.json({ message: 'File deleted successfully' });
} catch (error) {
res.status(500).json({ message: 'Error deleting file' });
}

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +105
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation using spaces instead of the established 2-space pattern used throughout the rest of the file.

Suggested change
try {
const charts = await SavedChart.find({ isPublic: true });
res.json(charts);
} catch (error) {
res.status(500).json({ message: 'Error fetching public charts' });
}
});
router.delete('/charts/:chartId', async (req, res) => {
try {
const deletedChart = await SavedChart.findByIdAndDelete(req.params.chartId);
if (!deletedChart) {
return res.status(404).json({ message: 'Chart not found' });
}
res.json({ message: 'Chart deleted successfully' });
} catch (error) {
res.status(500).json({ message: 'Error deleting chart' });
}
});
// File Management
router.get('/files', async (req, res) => {
try {
const files = await FileHistory.find();
res.json(files);
} catch (error) {
res.status(500).json({ message: 'Error fetching files' });
}
});
router.delete('/files/:fileId', async (req, res) => {
try {
const deletedFile = await FileHistory.findByIdAndDelete(req.params.fileId);
if (!deletedFile) {
return res.status(404).json({ message: 'File not found' });
}
res.json({ message: 'File deleted successfully' });
} catch (error) {
res.status(500).json({ message: 'Error deleting file' });
}
try {
const charts = await SavedChart.find({ isPublic: true });
res.json(charts);
} catch (error) {
res.status(500).json({ message: 'Error fetching public charts' });
}
});
router.delete('/charts/:chartId', async (req, res) => {
try {
const deletedChart = await SavedChart.findByIdAndDelete(req.params.chartId);
if (!deletedChart) {
return res.status(404).json({ message: 'Chart not found' });
}
res.json({ message: 'Chart deleted successfully' });
} catch (error) {
res.status(500).json({ message: 'Error deleting chart' });
}
});
// File Management
router.get('/files', async (req, res) => {
try {
const files = await FileHistory.find();
res.json(files);
} catch (error) {
res.status(500).json({ message: 'Error fetching files' });
}
});
router.delete('/files/:fileId', async (req, res) => {
try {
const deletedFile = await FileHistory.findByIdAndDelete(req.params.fileId);
if (!deletedFile) {
return res.status(404).json({ message: 'File not found' });
}
res.json({ message: 'File deleted successfully' });
} catch (error) {
res.status(500).json({ message: 'Error deleting file' });
}

Copilot uses AI. Check for mistakes.
});
Comment on lines +96 to +106
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation using spaces instead of the established 2-space pattern used throughout the rest of the file.

Copilot uses AI. Check for mistakes.


// Analytics
router.get('/analytics', async (req, res) => {
try {
const userCount = await UserProfile.countDocuments();
const chartCount = await SavedChart.countDocuments();
const fileCount = await FileHistory.countDocuments();
const publicChartCount = await SavedChart.countDocuments({ isPublic: true });

res.json({
userCount,
chartCount,
fileCount,
publicChartCount,
});
} catch (error) {
res.status(500).json({ message: 'Error fetching analytics' });
}
});

module.exports = router;
11 changes: 9 additions & 2 deletions server/routes/userProfile.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const router = require('express').Router();
const UserProfile = require('../models/userProfile.model');
const checkJwt = require('../middleware/auth');

const generateRandomNickname = () => {
const adjectives = [
Expand Down Expand Up @@ -56,9 +57,12 @@ const generateRandomAvatar = () => {
return `${randomEmoji}|${randomGradient}`;
};

router.get('/:userId', async (req, res) => {
router.get('/:userId', checkJwt, async (req, res) => {
try {
const { userId } = req.params;
if (req.auth.payload.sub !== userId) {
return res.status(403).json({ error: 'Forbidden' });
}
let profile = await UserProfile.findOne({ userId });

if (!profile) {
Expand All @@ -83,9 +87,12 @@ router.get('/:userId', async (req, res) => {
}
});

router.put('/:userId', async (req, res) => {
router.put('/:userId', checkJwt, async (req, res) => {
try {
const { userId } = req.params;
if (req.auth.payload.sub !== userId) {
return res.status(403).json({ error: 'Forbidden' });
}
const updateData = req.body;

if (updateData.nickname && updateData.nickname.trim().length === 0) {
Expand Down
10 changes: 10 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import Explore from './pages/Explore';
import History from './pages/History';
import SavedCharts from './pages/SavedCharts';
import Settings from './pages/Settings';
import AdminDashboard from './pages/AdminDashboard';
import Layout from './components/Layout';
import AdminRoute from './components/auth/AdminRoute';

const ProtectedRoute = ({ children }) => {
const { isAuthenticated, isLoading } = useAuth();
Expand Down Expand Up @@ -85,6 +87,14 @@ function App() {
</ProtectedRoute>
}
/>
<Route
path="/admin"
element={
<AdminRoute>
<Layout><AdminDashboard /></Layout>
</AdminRoute>
}
/>

{/* Fallback route - redirect to homepage */}
<Route path="*" element={<Navigate to="/" />} />
Expand Down
62 changes: 62 additions & 0 deletions src/components/admin/Analytics.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import api from '../../config/api';
import { useAuth0 } from '@auth0/auth0-react';

const Analytics = () => {
const [analytics, setAnalytics] = useState(null);
const [loading, setLoading] = useState(true);
const { getAccessTokenSilently } = useAuth0();

useEffect(() => {
const fetchAnalytics = async () => {
try {
const token = await getAccessTokenSilently();
const response = await axios.get(`${api.API_BASE_URL}/admin/analytics`, {
headers: { Authorization: `Bearer ${token}` },
});
setAnalytics(response.data);
} catch (error) {
console.error('Error fetching analytics', error);
} finally {
setLoading(false);
}
};

fetchAnalytics();
}, []);
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fetchAnalytics function should be included in the useEffect dependency array to follow React hooks best practices and prevent potential stale closure issues.

Suggested change
}, []);
}, [getAccessTokenSilently]);

Copilot uses AI. Check for mistakes.

if (loading) {
return <div>Loading analytics...</div>;
}

if (!analytics) {
return <div>Could not load analytics.</div>;
}

return (
<div>
<h2 className="text-xl font-semibold mb-4">Analytics Overview</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-white p-4 shadow rounded">
<h3 className="text-lg font-semibold">Total Users</h3>
<p className="text-3xl">{analytics.userCount}</p>
</div>
<div className="bg-white p-4 shadow rounded">
<h3 className="text-lg font-semibold">Total Charts</h3>
<p className="text-3xl">{analytics.chartCount}</p>
</div>
<div className="bg-white p-4 shadow rounded">
<h3 className="text-lg font-semibold">Public Charts</h3>
<p className="text-3xl">{analytics.publicChartCount}</p>
</div>
<div className="bg-white p-4 shadow rounded">
<h3 className="text-lg font-semibold">Total Files Uploaded</h3>
<p className="text-3xl">{analytics.fileCount}</p>
</div>
</div>
</div>
);
};

export default Analytics;
81 changes: 81 additions & 0 deletions src/components/admin/ChartManagement.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import api from '../../config/api';
import { useAuth0 } from '@auth0/auth0-react';

const ChartManagement = () => {
const [charts, setCharts] = useState([]);
const [loading, setLoading] = useState(true);
const { getAccessTokenSilently } = useAuth0();

const fetchCharts = async () => {
try {
const token = await getAccessTokenSilently();
const response = await axios.get(`${api.API_BASE_URL}/admin/charts`, {
headers: { Authorization: `Bearer ${token}` },
});
setCharts(response.data);
} catch (error) {
console.error('Error fetching charts', error);
} finally {
setLoading(false);
}
};

useEffect(() => {
fetchCharts();
}, []);
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fetchCharts function should be included in the useEffect dependency array to follow React hooks best practices and prevent potential stale closure issues.

Copilot uses AI. Check for mistakes.

const deleteChart = async (chartId) => {
if (window.confirm('Are you sure you want to delete this chart?')) {
try {
const token = await getAccessTokenSilently();
await axios.delete(`${api.API_BASE_URL}/admin/charts/${chartId}`, {
headers: { Authorization: `Bearer ${token}` },
});
fetchCharts(); // Refresh charts after delete
} catch (error) {
console.error('Error deleting chart', error);
}
}
};

if (loading) {
return <div>Loading charts...</div>;
}

return (
<div>
<h2 className="text-xl font-semibold mb-4">Chart Management</h2>
<table className="min-w-full bg-white">
<thead>
<tr>
<th className="py-2">Chart Name</th>
<th className="py-2">Chart Type</th>
<th className="py-2">Created By</th>
<th className="py-2">Actions</th>
</tr>
</thead>
<tbody>
{charts.map((chart) => (
<tr key={chart._id}>
<td className="border px-4 py-2">{chart.chartName}</td>
<td className="border px-4 py-2">{chart.chartType}</td>
<td className="border px-4 py-2">{chart.userId}</td>
<td className="border px-4 py-2">
<button
onClick={() => deleteChart(chart._id)}
className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};

export default ChartManagement;
Loading