Skip to content

Commit c59e11a

Browse files
fix: secure admin pages with server-side authentication and role-based access (#845)
* fix: secure admin pages with server-side authentication and role-based access BREAKING CHANGES: - Admin pages now require server-side authentication - Hardcoded admin email replaced with database role checks - All admin access now requires ADMIN role from database Security Improvements: - Convert all admin pages from getStaticProps to getServerSideProps - Add server-side authentication checks before rendering admin pages - Block unauthorized access at server level (not client-side) - Replace hardcoded 'jeromehardaway' email with role-based checks Pages Updated: - admin/users.tsx: Fetch real user data from database, added role badges - admin/courses.tsx: Fetch real course data with enrollment counts - admin/index.tsx: Display real dashboard statistics, remove dev-session - admin/blog-images.tsx: Add server-side auth protection - courses/index.tsx: Use role check for admin dashboard link Database Integration: - admin/users: Query users with enrollment counts - admin/courses: Query courses with module/enrollment counts - admin/index: Aggregate platform statistics from database This fixes critical security vulnerability where admin pages were: 1. Publicly accessible as static pages 2. Using client-side auth checks (bypassable) 3. Displaying mock data instead of real database data 4. Hardcoding admin access to single GitHub user * fix: add permissions to playwright workflow and use VWC_GITHUB_TOKEN - Add explicit permissions block for workflow scoping - Use VWC_GITHUB_TOKEN secret instead of default GITHUB_TOKEN - Fixes build failure when fetching GitHub project data
1 parent 189b0e6 commit c59e11a

File tree

6 files changed

+482
-423
lines changed

6 files changed

+482
-423
lines changed

.github/workflows/playwright.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ on:
66
pull_request:
77
branches: [main, master]
88

9+
permissions:
10+
contents: read
11+
issues: read
12+
pull-requests: read
13+
914
env:
10-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
15+
GITHUB_TOKEN: ${{ secrets.VWC_GITHUB_TOKEN }}
1116

1217
jobs:
1318
test:

src/pages/admin/blog-images.tsx

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
/**
22
* Admin page for managing blog images
33
* Accessible at /admin/blog-images
4+
* Protected by server-side authentication
45
*/
56

67
import React, { useEffect, useState } from 'react';
7-
import { useSession } from 'next-auth/react';
8-
import { useRouter } from 'next/router';
8+
import type { GetServerSideProps } from 'next';
9+
import { getServerSession } from 'next-auth/next';
10+
import { options } from '@/pages/api/auth/options';
911
import BlogImageManager from '@/components/blog-image-manager';
1012
import {
1113
getAllBlogImages,
@@ -15,44 +17,17 @@ import {
1517
} from '@/lib/blog-images';
1618

1719
const BlogImagesAdminPage: React.FC = () => {
18-
const { data: session, status } = useSession();
19-
const router = useRouter();
2020
const [stats, setStats] = useState<ReturnType<typeof getBlogImageStats> | null>(null);
2121
const [allImages, setAllImages] = useState<BlogImageInfo[]>([]);
2222
const [nonCloudinaryImages, setNonCloudinaryImages] = useState<BlogImageInfo[]>([]);
2323
const [activeTab, setActiveTab] = useState<'manager' | 'stats' | 'list'>('manager');
2424

2525
useEffect(() => {
26-
if (status === 'loading') return;
27-
28-
if (!session) {
29-
router.push('/login');
30-
return;
31-
}
32-
33-
// Check if user is admin
34-
if (session.user?.role !== 'ADMIN') {
35-
router.push('/dashboard');
36-
return;
37-
}
38-
39-
// Load blog image data
26+
// Load blog image data on mount
4027
setStats(getBlogImageStats());
4128
setAllImages(getAllBlogImages());
4229
setNonCloudinaryImages(getBlogsWithoutCloudinaryImages());
43-
}, [session, status, router]);
44-
45-
if (status === 'loading') {
46-
return (
47-
<div style={{ padding: '40px', textAlign: 'center' }}>
48-
<p>Loading...</p>
49-
</div>
50-
);
51-
}
52-
53-
if (!session || session.user?.role !== 'ADMIN') {
54-
return null;
55-
}
30+
}, []);
5631

5732
return (
5833
<div style={{ minHeight: '100vh', backgroundColor: '#f5f5f5' }}>
@@ -290,4 +265,33 @@ const BlogImagesAdminPage: React.FC = () => {
290265
);
291266
};
292267

268+
export const getServerSideProps: GetServerSideProps = async (context) => {
269+
// Check authentication
270+
const session = await getServerSession(context.req, context.res, options);
271+
272+
// Redirect if not authenticated
273+
if (!session?.user) {
274+
return {
275+
redirect: {
276+
destination: '/login?callbackUrl=/admin/blog-images',
277+
permanent: false,
278+
},
279+
};
280+
}
281+
282+
// Check for ADMIN role
283+
if (session.user.role !== 'ADMIN') {
284+
return {
285+
redirect: {
286+
destination: '/',
287+
permanent: false,
288+
},
289+
};
290+
}
291+
292+
return {
293+
props: {},
294+
};
295+
};
296+
293297
export default BlogImagesAdminPage;

0 commit comments

Comments
 (0)