feat: move community supabase request to api layer (#612)
* feat: move community supabase request to api layer * chore: pr review checkpoint * chore: PR fixes * feat: implement role-based authorization middleware and align the community features with the developer guide. - Added role-based authorization middleware for organization and course membership checks, including `orgMemberMiddleware`, `orgTeamMemberMiddleware`, and `courseMemberMiddleware`. - Introduced route-specific middleware for comment and question authorization, ensuring only authors or team members can modify content. - Updated community routes to utilize the new middleware for enhanced security and access control. - Refactored community services to streamline upvote functionality and improve error handling. - Enhanced UI components for community interactions, including new comment and question pages, and improved state management for user actions. - Updated documentation to reflect new middleware usage and best practices for authorization in the API layer. * feat: enhance course and organization routes with middleware for improved access control - Added `courseMemberMiddleware` to course download and clone routes to enforce membership checks. - Updated lesson route to include `courseMemberMiddleware` for PDF downloads. - Integrated `orgMemberMiddleware` into organization routes for team and audience access, ensuring only authorized members can retrieve data. - Refactored lesson and organization routes for consistency and clarity in middleware usage. * chore: streamline dashboard and API development scripts in package.json - Consolidated dashboard and API development commands to include all relevant filters in a single line for improved clarity and ease of use. - Removed redundant full command variants to simplify the script section. --------- Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
co-authored by
Rotimi Best
parent
9fb6c7cbaa
commit
bc9e3b41ae
@@ -0,0 +1,204 @@
|
||||
import { AppError, ErrorCodes } from '@api/utils/errors';
|
||||
import type { TCommunityAnswer, TCommunityQuestion, TCourse } from '@db/types';
|
||||
import {
|
||||
createCommunityQuestion,
|
||||
deleteCommentById,
|
||||
deleteCommentByQuestionId,
|
||||
deleteQuestionByQuestionId,
|
||||
editCommunityQuestion,
|
||||
getCommunityQuestion,
|
||||
getCommunityQuestions,
|
||||
submitComment,
|
||||
upvoteAnswer,
|
||||
upvoteQuestion
|
||||
} from '@cio/db/queries/community';
|
||||
|
||||
import { getCoursesByOrgId } from './organization';
|
||||
|
||||
export async function fetchCommunityQuestions(orgId: string): Promise<Partial<TCommunityQuestion>[]> {
|
||||
let discussions: Partial<TCommunityQuestion>[] = [];
|
||||
|
||||
let courses: TCourse[];
|
||||
|
||||
if (orgId) {
|
||||
courses = await getCoursesByOrgId(orgId);
|
||||
} else {
|
||||
throw new AppError(
|
||||
'Organization not found for the given site name',
|
||||
ErrorCodes.COMMUNITY_QUESTIONS_FETCH_FAILED,
|
||||
404
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const courseIds = courses.map((course) => course.id);
|
||||
discussions = await getCommunityQuestions(courseIds);
|
||||
|
||||
return discussions;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch community questions:', error);
|
||||
throw new AppError('Failed to fetch community questions', ErrorCodes.COMMUNITY_QUESTIONS_FETCH_FAILED, 500);
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCommunityQuestion({ slug }: Partial<TCommunityQuestion>) {
|
||||
try {
|
||||
const result = await getCommunityQuestion(slug);
|
||||
|
||||
if (!result) {
|
||||
throw new AppError('Community question not found', ErrorCodes.COMMUNITY_QUESTION_NOT_FOUND, 404);
|
||||
}
|
||||
|
||||
return {
|
||||
id: result.id,
|
||||
title: result.title,
|
||||
body: result.body,
|
||||
votes: result.votes,
|
||||
createdAt: result.createdAt,
|
||||
courseId: result.courseId,
|
||||
courseTitle: result.course.title,
|
||||
slug: result.slug,
|
||||
authorId: result.author.id,
|
||||
authorFullname: result.author.fullname,
|
||||
authorAvatarUrl: result.author.avatarUrl,
|
||||
organizationId: result.organizationId,
|
||||
comments: result.comments
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch community question:', error);
|
||||
if (error instanceof AppError) throw error;
|
||||
throw new AppError('Failed to fetch community question', ErrorCodes.COMMUNITY_QUESTION_NOT_FOUND, 500);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createQuestion({
|
||||
title,
|
||||
body,
|
||||
courseId,
|
||||
organizationId,
|
||||
authorProfileId,
|
||||
votes,
|
||||
slug
|
||||
}: Partial<TCommunityQuestion>) {
|
||||
try {
|
||||
const result = await createCommunityQuestion({
|
||||
title,
|
||||
body,
|
||||
courseId,
|
||||
organizationId,
|
||||
authorProfileId,
|
||||
votes,
|
||||
slug
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new AppError('Failed to create community question', ErrorCodes.ADD_COMMUNITY_QUESTION_FAILED, 404);
|
||||
}
|
||||
|
||||
return {
|
||||
id: result.id,
|
||||
title: result.title,
|
||||
body: result.body,
|
||||
votes: result.votes,
|
||||
createdAt: result.createdAt,
|
||||
courseId: result.courseId,
|
||||
courseTitle: result.title,
|
||||
slug: result.slug,
|
||||
authorId: result.authorId
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to add community question:', error);
|
||||
if (error instanceof AppError) throw error;
|
||||
throw new AppError('Failed to add community question', ErrorCodes.ADD_COMMUNITY_QUESTION_FAILED, 500);
|
||||
}
|
||||
}
|
||||
|
||||
export async function editQuestion({ id, title, body, courseId }: Partial<TCommunityQuestion>) {
|
||||
try {
|
||||
const result = await editCommunityQuestion({ id, title, body, courseId: courseId });
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to update community question:', error);
|
||||
if (error instanceof AppError) throw error;
|
||||
throw new AppError('Failed to update community question', ErrorCodes.UPDATE_COMMUNITY_QUESTION_FAILED, 500);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createComment({ body, questionId, authorProfileId, votes }: Partial<TCommunityAnswer>) {
|
||||
try {
|
||||
const result = await submitComment({ body, questionId, authorProfileId, votes });
|
||||
|
||||
if (!result) {
|
||||
throw new AppError('Comment submission failed', ErrorCodes.COMMENT_SUBMISSION_FAILED, 404);
|
||||
}
|
||||
|
||||
return {
|
||||
id: result.id,
|
||||
body: result.body,
|
||||
votes: result.votes,
|
||||
createdAt: result.createdAt,
|
||||
authorId: result.authorProfileId
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to submit comment:', error);
|
||||
if (error instanceof AppError) throw error;
|
||||
throw new AppError('Failed to submit comment', ErrorCodes.COMMENT_SUBMISSION_FAILED, 500);
|
||||
}
|
||||
}
|
||||
|
||||
export async function upvote({ id, isQuestion }: { id: string | number; isQuestion: boolean }) {
|
||||
try {
|
||||
let result;
|
||||
|
||||
if (isQuestion) {
|
||||
result = await upvoteQuestion({ id: Number(id) });
|
||||
} else {
|
||||
result = await upvoteAnswer({ id: String(id) });
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to upvote:', error);
|
||||
if (error instanceof AppError) throw error;
|
||||
throw new AppError('Failed to upvote', ErrorCodes.UPVOTE_FAILED, 500);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteQuestion({ id }: Partial<TCommunityQuestion>) {
|
||||
try {
|
||||
// Delete all comments first, then delete the question
|
||||
await deleteCommentByQuestionId(id);
|
||||
const result = await deleteQuestionByQuestionId(id);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to delete question:', error);
|
||||
if (error instanceof AppError) throw error;
|
||||
throw new AppError('Failed to delete question', ErrorCodes.DELETE_QUESTION_FAILED, 500);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteComment({ id }: Partial<TCommunityAnswer>) {
|
||||
try {
|
||||
const result = await deleteCommentById(id);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to delete comment:', error);
|
||||
if (error instanceof AppError) throw error;
|
||||
throw new AppError('Failed to delete comment', ErrorCodes.DELETE_COMMENT_FAILED, 500);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCommentsByQuestionId({ questionId }: Partial<TCommunityAnswer>) {
|
||||
try {
|
||||
const result = await deleteCommentByQuestionId(questionId);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to delete comment:', error);
|
||||
if (error instanceof AppError) throw error;
|
||||
throw new AppError('Failed to delete comment', ErrorCodes.DELETE_COMMENT_FAILED, 500);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
updateOrganizationPlan
|
||||
} from '@cio/db/queries/organization';
|
||||
import {
|
||||
getCoursesById,
|
||||
getCoursesBySiteName,
|
||||
getCoursesBySiteNameForSetup,
|
||||
getExercisesBySiteName,
|
||||
@@ -128,6 +129,23 @@ export async function getCoursesByOrgSiteName(siteName: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets courses by organization orgId
|
||||
* @param orgId - The organization orgId
|
||||
* @returns Array of courses
|
||||
*/
|
||||
export async function getCoursesByOrgId(orgId: string) {
|
||||
try {
|
||||
return getCoursesById(orgId);
|
||||
} catch (error) {
|
||||
throw new AppError(
|
||||
error instanceof Error ? error.message : 'Failed to fetch courses',
|
||||
ErrorCodes.COURSES_FETCH_FAILED,
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets setup data for an organization
|
||||
* @param siteName - The organization site name
|
||||
|
||||
Reference in New Issue
Block a user