Skip to content

Commit 22de23c

Browse files
committed
fix: fix linting problems and block course creator from self-enroll
1 parent 398131f commit 22de23c

File tree

6 files changed

+221
-189
lines changed

6 files changed

+221
-189
lines changed

apps/api/src/admin/admin.service.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Test, type TestingModule } from '@nestjs/testing';
22

33
import { AdminService } from './admin.service';
4-
import { UsersService } from '../users/users.service';
54
import { CoursesService } from '../courses/courses.service';
5+
import { UsersService } from '../users/users.service';
66

77
describe('AdminService', () => {
88
let service: AdminService;

apps/api/src/courses/courses.service.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,18 @@ describe('CoursesService', () => {
500500
ConflictException,
501501
);
502502
});
503+
504+
it('should throw ConflictException when instructor attempts to enroll in own course', async () => {
505+
const course = mockCourse('course-1', 'Test Course', ['react']);
506+
course.instructor = { id: 'instructor-1', fullName: 'Test Instructor' } as any;
507+
singleQueryBuilder.getOne.mockResolvedValue(course);
508+
const courseRepo = courseRepository;
509+
courseRepo.createQueryBuilder.mockReturnValue(singleQueryBuilder);
510+
511+
await expect(
512+
service.enroll('instructor-1', 'course-1'),
513+
).rejects.toThrow(ConflictException);
514+
});
503515
});
504516

505517
describe('checkEnrollment', () => {

apps/api/src/courses/courses.service.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export class CoursesService {
7373
private lessonRepository: Repository<Lesson>,
7474
@InjectRepository(CourseSection)
7575
private sectionRepository: Repository<CourseSection>,
76-
) {}
76+
) { }
7777

7878
async findAllPublished(
7979
options: CourseFilterOptions,
@@ -198,7 +198,11 @@ export class CoursesService {
198198
async enroll(userId: string, courseId: string): Promise<Enrollment> {
199199
// Check if course exists and is accessible
200200
// Passing generic user object to allow enrollment only if visible
201-
await this.findOne(courseId, { id: userId } as User);
201+
const course = await this.findOne(courseId, { id: userId } as User);
202+
203+
if (course.instructor?.id === userId) {
204+
throw new ConflictException('Cannot enroll in your own course');
205+
}
202206

203207
// Check if already enrolled
204208
const existing = await this.enrollmentRepository.findOne({

apps/web/src/pages/admin/admin-courses-page.tsx

Lines changed: 145 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -10,163 +10,163 @@ import { Skeleton } from '@/components/ui/skeleton';
1010
import { adminApi } from '@/features/admin/api/admin-api';
1111

1212
export function AdminCoursesPage() {
13-
const queryClient = useQueryClient();
13+
const queryClient = useQueryClient();
1414

15-
const { data: courses, isLoading } = useQuery({
15+
const { data: courses, isLoading } = useQuery({
16+
queryKey: ['admin-pending-courses'],
17+
queryFn: adminApi.getPendingCourses,
18+
});
19+
20+
const approveMutation = useMutation({
21+
mutationFn: adminApi.approveCourse,
22+
onSuccess: () => {
23+
toast.success('Course approved');
24+
void queryClient.invalidateQueries({
1625
queryKey: ['admin-pending-courses'],
17-
queryFn: adminApi.getPendingCourses,
18-
});
26+
});
27+
},
28+
});
1929

20-
const approveMutation = useMutation({
21-
mutationFn: adminApi.approveCourse,
22-
onSuccess: () => {
23-
toast.success('Course approved');
24-
void queryClient.invalidateQueries({
25-
queryKey: ['admin-pending-courses'],
26-
});
27-
},
28-
});
30+
const rejectMutation = useMutation({
31+
mutationFn: adminApi.rejectCourse,
32+
onSuccess: () => {
33+
toast.success('Course rejected');
34+
void queryClient.invalidateQueries({
35+
queryKey: ['admin-pending-courses'],
36+
});
37+
},
38+
});
2939

30-
const rejectMutation = useMutation({
31-
mutationFn: adminApi.rejectCourse,
32-
onSuccess: () => {
33-
toast.success('Course rejected');
34-
void queryClient.invalidateQueries({
35-
queryKey: ['admin-pending-courses'],
36-
});
37-
},
38-
});
40+
const handleApprove = (id: string) => {
41+
// eslint-disable-next-line no-alert
42+
if (window.confirm('Approve this course and publish it?')) {
43+
approveMutation.mutate(id);
44+
}
45+
};
3946

40-
const handleApprove = (id: string) => {
41-
// eslint-disable-next-line no-alert
42-
if (window.confirm('Approve this course and publish it?')) {
43-
approveMutation.mutate(id);
44-
}
45-
};
47+
const handleReject = (id: string) => {
48+
// eslint-disable-next-line no-alert
49+
if (window.confirm('Reject this course?')) {
50+
rejectMutation.mutate(id);
51+
}
52+
};
4653

47-
const handleReject = (id: string) => {
48-
// eslint-disable-next-line no-alert
49-
if (window.confirm('Reject this course?')) {
50-
rejectMutation.mutate(id);
51-
}
52-
};
54+
return (
55+
<PageContainer>
56+
<div className="space-y-8 animate-fade-in">
57+
<div className="flex items-center justify-between">
58+
<div>
59+
<div className="flex items-center gap-2 text-muted-foreground mb-2">
60+
<Shield className="w-5 h-5" />
61+
<span className="text-sm font-medium">Administration</span>
62+
</div>
63+
<h1 className="text-3xl font-bold">Course Moderation</h1>
64+
<p className="text-muted-foreground">
65+
Review incoming courses from instructors
66+
</p>
67+
</div>
68+
</div>
5369

54-
return (
55-
<PageContainer>
56-
<div className="space-y-8 animate-fade-in">
57-
<div className="flex items-center justify-between">
58-
<div>
59-
<div className="flex items-center gap-2 text-muted-foreground mb-2">
60-
<Shield className="w-5 h-5" />
61-
<span className="text-sm font-medium">Administration</span>
62-
</div>
63-
<h1 className="text-3xl font-bold">Course Moderation</h1>
64-
<p className="text-muted-foreground">
65-
Review incoming courses from instructors
66-
</p>
67-
</div>
68-
</div>
70+
<div className="grid gap-6">
71+
{isLoading && (
72+
<div className="space-y-4">
73+
{[1, 2].map((i) => (
74+
<Skeleton key={i} className="h-24 w-full rounded-xl" />
75+
))}
76+
</div>
77+
)}
6978

70-
<div className="grid gap-6">
71-
{isLoading && (
72-
<div className="space-y-4">
73-
{[1, 2].map((i) => (
74-
<Skeleton key={i} className="h-24 w-full rounded-xl" />
75-
))}
76-
</div>
77-
)}
79+
{!isLoading && (!courses || courses.length === 0) && (
80+
<Card className="p-12 text-center bg-muted/30 border-dashed">
81+
<div className="max-w-sm mx-auto space-y-4">
82+
<div className="w-16 h-16 mx-auto rounded-full bg-muted flex items-center justify-center">
83+
<Check className="w-8 h-8 text-green-500" />
84+
</div>
85+
<h3 className="text-xl font-semibold">All Caught Up!</h3>
86+
<p className="text-muted-foreground">
87+
There are no pending courses to review at the moment.
88+
</p>
89+
</div>
90+
</Card>
91+
)}
7892

79-
{!isLoading && (!courses || courses.length === 0) && (
80-
<Card className="p-12 text-center bg-muted/30 border-dashed">
81-
<div className="max-w-sm mx-auto space-y-4">
82-
<div className="w-16 h-16 mx-auto rounded-full bg-muted flex items-center justify-center">
83-
<Check className="w-8 h-8 text-green-500" />
84-
</div>
85-
<h3 className="text-xl font-semibold">All Caught Up!</h3>
86-
<p className="text-muted-foreground">
87-
There are no pending courses to review at the moment.
88-
</p>
89-
</div>
90-
</Card>
93+
{!isLoading &&
94+
courses?.map((course) => (
95+
<Card
96+
key={course.id}
97+
className="overflow-hidden hover:shadow-md transition-shadow"
98+
>
99+
<CardContent className="p-6 flex flex-col md:flex-row gap-6 items-start md:items-center">
100+
<div className="w-full md:w-48 h-32 bg-muted rounded-lg shrink-0 overflow-hidden">
101+
{course.thumbnailUrl ? (
102+
<img
103+
src={course.thumbnailUrl}
104+
alt={course.title}
105+
className="w-full h-full object-cover"
106+
/>
107+
) : (
108+
<div className="w-full h-full flex items-center justify-center text-muted-foreground bg-muted">
109+
No Image
110+
</div>
91111
)}
112+
</div>
92113

93-
{!isLoading &&
94-
courses?.map((course) => (
95-
<Card
96-
key={course.id}
97-
className="overflow-hidden hover:shadow-md transition-shadow"
98-
>
99-
<CardContent className="p-6 flex flex-col md:flex-row gap-6 items-start md:items-center">
100-
<div className="w-full md:w-48 h-32 bg-muted rounded-lg shrink-0 overflow-hidden">
101-
{course.thumbnailUrl ? (
102-
<img
103-
src={course.thumbnailUrl}
104-
alt={course.title}
105-
className="w-full h-full object-cover"
106-
/>
107-
) : (
108-
<div className="w-full h-full flex items-center justify-center text-muted-foreground bg-muted">
109-
No Image
110-
</div>
111-
)}
112-
</div>
113-
114-
<div className="flex-1 min-w-0 space-y-2">
115-
<h3 className="text-xl font-bold truncate">
116-
{course.title}
117-
</h3>
118-
<div className="flex items-center gap-2 text-sm text-muted-foreground">
119-
<span>
120-
By {course.instructor?.fullName ?? 'Unknown Instructor'}
121-
</span>
122-
<span></span>
123-
<span className="capitalize">{course.level}</span>
124-
<span></span>
125-
{/* Date formatting could go here */}
126-
<span>Submitted Recently</span>
127-
</div>
128-
<p className="text-sm text-muted-foreground line-clamp-2">
129-
{course.description}
130-
</p>
131-
</div>
114+
<div className="flex-1 min-w-0 space-y-2">
115+
<h3 className="text-xl font-bold truncate">
116+
{course.title}
117+
</h3>
118+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
119+
<span>
120+
By {course.instructor?.fullName ?? 'Unknown Instructor'}
121+
</span>
122+
<span></span>
123+
<span className="capitalize">{course.level}</span>
124+
<span></span>
125+
{/* Date formatting could go here */}
126+
<span>Submitted Recently</span>
127+
</div>
128+
<p className="text-sm text-muted-foreground line-clamp-2">
129+
{course.description}
130+
</p>
131+
</div>
132132

133-
<div className="flex flex-row md:flex-col gap-2 shrink-0 w-full md:w-auto">
134-
<Button
135-
variant="outline"
136-
size="sm"
137-
asChild
138-
className="w-full justify-start"
139-
>
140-
<Link to={`/courses/${course.id}`} target="_blank">
141-
<Eye className="w-4 h-4 mr-2" /> Preview
142-
</Link>
143-
</Button>
144-
<div className="flex gap-2 w-full">
145-
<Button
146-
variant="primary"
147-
size="sm"
148-
className="flex-1 bg-green-600 hover:bg-green-700"
149-
onClick={() => handleApprove(course.id)}
150-
>
151-
<Check className="w-4 h-4 mr-2" /> Approve
152-
</Button>
153-
<Button
154-
variant="destructive"
155-
size="sm"
156-
className="flex-1"
157-
onClick={() => handleReject(course.id)}
158-
>
159-
<X className="w-4 h-4 mr-2" /> Reject
160-
</Button>
161-
</div>
162-
</div>
163-
</CardContent>
164-
</Card>
165-
))}
166-
</div>
167-
</div>
168-
</PageContainer>
169-
);
133+
<div className="flex flex-row md:flex-col gap-2 shrink-0 w-full md:w-auto">
134+
<Button
135+
variant="outline"
136+
size="sm"
137+
asChild
138+
className="w-full justify-start"
139+
>
140+
<Link to={`/courses/${course.id}`} target="_blank">
141+
<Eye className="w-4 h-4 mr-2" /> Preview
142+
</Link>
143+
</Button>
144+
<div className="flex gap-2 w-full">
145+
<Button
146+
variant="primary"
147+
size="sm"
148+
className="flex-1 bg-green-600 hover:bg-green-700"
149+
onClick={() => handleApprove(course.id)}
150+
>
151+
<Check className="w-4 h-4 mr-2" /> Approve
152+
</Button>
153+
<Button
154+
variant="destructive"
155+
size="sm"
156+
className="flex-1"
157+
onClick={() => handleReject(course.id)}
158+
>
159+
<X className="w-4 h-4 mr-2" /> Reject
160+
</Button>
161+
</div>
162+
</div>
163+
</CardContent>
164+
</Card>
165+
))}
166+
</div>
167+
</div>
168+
</PageContainer>
169+
);
170170
}
171171

172172
export default AdminCoursesPage;

0 commit comments

Comments
 (0)