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 <[email protected]>
This commit is contained in:
Adeyemi Adetunji
2025-12-22 19:57:27 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 9fb6c7cbaa
commit bc9e3b41ae
41 changed files with 2699 additions and 1598 deletions
+204
View File
@@ -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);
}
}
+18
View File
@@ -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