185 lines
6.0 KiB
TypeScript
185 lines
6.0 KiB
TypeScript
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
|
|
import { PrismaService } from '../common/prisma/prisma.service';
|
|
|
|
@Injectable()
|
|
export class GroupsService {
|
|
constructor(private prisma: PrismaService) {}
|
|
|
|
async createGroup(courseId: string, userId: string, name: string, description?: string): Promise<any> {
|
|
const course = await this.prisma.course.findFirst({ where: { id: courseId, authorId: userId } });
|
|
if (!course) throw new ForbiddenException('Only course author can create groups');
|
|
|
|
return this.prisma.courseGroup.create({
|
|
data: { courseId, name, description },
|
|
});
|
|
}
|
|
|
|
async ensureDefaultGroup(courseId: string): Promise<any> {
|
|
const existing = await this.prisma.courseGroup.findFirst({
|
|
where: { courseId, isDefault: true },
|
|
});
|
|
if (existing) return existing;
|
|
|
|
return this.prisma.courseGroup.create({
|
|
data: {
|
|
courseId,
|
|
name: 'Основная группа',
|
|
description: 'Обсуждение курса и вопросы преподавателю',
|
|
isDefault: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
async getDefaultGroup(courseId: string, userId: string): Promise<any> {
|
|
const group = await this.ensureDefaultGroup(courseId);
|
|
await this.assertCanReadGroup(group.id, userId);
|
|
|
|
const [messages, members] = await Promise.all([
|
|
this.getGroupMessages(group.id, userId),
|
|
this.getGroupMembers(group.id, userId),
|
|
]);
|
|
return { group, messages, members };
|
|
}
|
|
|
|
async addMember(groupId: string, requesterId: string, targetUserId: string, role = 'student'): Promise<any> {
|
|
const group = await this.prisma.courseGroup.findUnique({
|
|
where: { id: groupId },
|
|
include: { course: { select: { authorId: true } } },
|
|
});
|
|
if (!group) throw new NotFoundException('Group not found');
|
|
if (group.course.authorId !== requesterId) {
|
|
throw new ForbiddenException('Only course author can add members manually');
|
|
}
|
|
|
|
return this.prisma.groupMember.upsert({
|
|
where: { groupId_userId: { groupId, userId: targetUserId } },
|
|
create: { groupId, userId: targetUserId, role },
|
|
update: { role },
|
|
});
|
|
}
|
|
|
|
async getGroupMembers(groupId: string, userId: string): Promise<any> {
|
|
await this.assertCanReadGroup(groupId, userId);
|
|
return this.prisma.groupMember.findMany({
|
|
where: { groupId },
|
|
include: {
|
|
user: {
|
|
select: { id: true, name: true, email: true, avatarUrl: true },
|
|
},
|
|
},
|
|
orderBy: { joinedAt: 'asc' },
|
|
});
|
|
}
|
|
|
|
async getGroupMessages(groupId: string, userId: string): Promise<any> {
|
|
await this.assertCanReadGroup(groupId, userId);
|
|
|
|
return this.prisma.groupMessage.findMany({
|
|
where: { groupId },
|
|
include: { user: { select: { id: true, name: true, avatarUrl: true } } },
|
|
orderBy: { createdAt: 'asc' },
|
|
take: 200,
|
|
});
|
|
}
|
|
|
|
async sendMessage(groupId: string, userId: string, content: string): Promise<any> {
|
|
await this.assertCanReadGroup(groupId, userId);
|
|
|
|
return this.prisma.groupMessage.create({
|
|
data: { groupId, userId, content },
|
|
include: { user: { select: { id: true, name: true, avatarUrl: true } } },
|
|
});
|
|
}
|
|
|
|
async createInviteLink(groupId: string, userId: string): Promise<any> {
|
|
const group = await this.prisma.courseGroup.findUnique({
|
|
where: { id: groupId },
|
|
include: { course: { select: { authorId: true } } },
|
|
});
|
|
if (!group) throw new NotFoundException('Group not found');
|
|
if (group.course.authorId !== userId) {
|
|
throw new ForbiddenException('Only course author can create invite links');
|
|
}
|
|
|
|
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3080';
|
|
return {
|
|
groupId,
|
|
inviteUrl: `${appUrl}/dashboard/groups/${groupId}`,
|
|
};
|
|
}
|
|
|
|
async joinByInvite(groupId: string, userId: string): Promise<any> {
|
|
const group = await this.prisma.courseGroup.findUnique({
|
|
where: { id: groupId },
|
|
include: {
|
|
course: {
|
|
select: { id: true, authorId: true, price: true },
|
|
},
|
|
},
|
|
});
|
|
if (!group) throw new NotFoundException('Group not found');
|
|
|
|
const hasEnrollment = await this.prisma.enrollment.findUnique({
|
|
where: { userId_courseId: { userId, courseId: group.course.id } },
|
|
});
|
|
const hasPurchase = await this.prisma.purchase.findUnique({
|
|
where: { userId_courseId: { userId, courseId: group.course.id } },
|
|
});
|
|
|
|
if (!hasEnrollment && !hasPurchase && group.course.authorId !== userId) {
|
|
throw new ForbiddenException('Enroll or purchase course first');
|
|
}
|
|
|
|
return this.prisma.groupMember.upsert({
|
|
where: { groupId_userId: { groupId, userId } },
|
|
create: { groupId, userId, role: group.course.authorId === userId ? 'teacher' : 'student' },
|
|
update: {},
|
|
});
|
|
}
|
|
|
|
async isMember(groupId: string, userId: string): Promise<boolean> {
|
|
const member = await this.prisma.groupMember.findUnique({
|
|
where: { groupId_userId: { groupId, userId } },
|
|
select: { id: true },
|
|
});
|
|
return Boolean(member);
|
|
}
|
|
|
|
private async assertCanReadGroup(groupId: string, userId: string): Promise<void> {
|
|
const group = await this.prisma.courseGroup.findUnique({
|
|
where: { id: groupId },
|
|
include: {
|
|
course: {
|
|
select: { id: true, authorId: true },
|
|
},
|
|
},
|
|
});
|
|
if (!group) {
|
|
throw new NotFoundException('Group not found');
|
|
}
|
|
|
|
if (group.course.authorId === userId) return;
|
|
|
|
const member = await this.prisma.groupMember.findUnique({
|
|
where: { groupId_userId: { groupId, userId } },
|
|
select: { id: true },
|
|
});
|
|
if (member) return;
|
|
|
|
const enrollment = await this.prisma.enrollment.findUnique({
|
|
where: { userId_courseId: { userId, courseId: group.course.id } },
|
|
select: { id: true },
|
|
});
|
|
if (enrollment) {
|
|
await this.prisma.groupMember.upsert({
|
|
where: { groupId_userId: { groupId, userId } },
|
|
create: { groupId, userId, role: 'student' },
|
|
update: {},
|
|
});
|
|
return;
|
|
}
|
|
|
|
throw new ForbiddenException('No access to this group');
|
|
}
|
|
}
|