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:
co-authored by
Rotimi Best
parent
9fb6c7cbaa
commit
bc9e3b41ae
@@ -1,26 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
import { Empty } from '@cio/ui/custom/empty';
|
||||
import * as Item from '@cio/ui/base/item';
|
||||
import { Search } from '@cio/ui/custom/search';
|
||||
import MessageSquareMoreIcon from '@lucide/svelte/icons/message-square-more';
|
||||
import MessageCirclePlusIcon from '@lucide/svelte/icons/message-circle-plus';
|
||||
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { courses } from '$features/course/utils/store';
|
||||
import { communityApi } from '../api/community.svelte';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||
import MessageSquareMoreIcon from '@lucide/svelte/icons/message-square-more';
|
||||
|
||||
import { CommunityListLoader } from '../components';
|
||||
import { Vote } from '$features/ui';
|
||||
import { Empty } from '@cio/ui/custom/empty';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import * as Item from '@cio/ui/base/item';
|
||||
|
||||
import { AskCommunityButton } from '../components';
|
||||
import { CommunityListLoader } from '../components';
|
||||
import { Vote } from '$features/ui';
|
||||
import type { CommunityQuestionData } from '../utils/types';
|
||||
|
||||
interface Props {
|
||||
isLMS?: boolean;
|
||||
@@ -28,70 +25,17 @@
|
||||
|
||||
let { isLMS = false }: Props = $props();
|
||||
|
||||
let isLoading = $state(false);
|
||||
let discussions = $state([]);
|
||||
let searchValue = $state('');
|
||||
let allCourses: any[] = $state([]);
|
||||
let selectedId = $state('');
|
||||
|
||||
function fetchCommunityQuestions(orgId?: string, profileId?: string) {
|
||||
if (!orgId || !profileId) return;
|
||||
|
||||
untrack(async () => {
|
||||
isLoading = true;
|
||||
|
||||
if ($courses.length) {
|
||||
allCourses = [...$courses];
|
||||
} else {
|
||||
const courseResult = (await fetchCourses(profileId, orgId)) || { allCourses: [] };
|
||||
allCourses = courseResult.allCourses;
|
||||
}
|
||||
|
||||
const courseIds = allCourses.map((course) => course.id);
|
||||
const courseIdsFilter = `(${courseIds.join(',')})`;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('community_question')
|
||||
.select(
|
||||
`
|
||||
organization_id,
|
||||
course_id,
|
||||
title,
|
||||
votes,
|
||||
created_at,
|
||||
slug,
|
||||
comments:community_answer(count),
|
||||
author:profile(
|
||||
fullname
|
||||
),
|
||||
course!inner (
|
||||
title
|
||||
)
|
||||
`
|
||||
)
|
||||
.filter('course_id', 'in', courseIdsFilter)
|
||||
.order('created_at', { ascending: false });
|
||||
console.log('data', data);
|
||||
console.log('error', error);
|
||||
|
||||
isLoading = false;
|
||||
|
||||
if (error) {
|
||||
console.error('Error loading community', error);
|
||||
return goto(isLMS ? '/lms' : $currentOrgPath);
|
||||
}
|
||||
|
||||
discussions =
|
||||
data?.map((discussion) => ({
|
||||
title: discussion.title,
|
||||
courseId: discussion.course_id,
|
||||
courseTitle: discussion.course?.title,
|
||||
slug: discussion.slug,
|
||||
author: discussion?.author?.fullname,
|
||||
comments: discussion.comments?.[0]?.count || 0,
|
||||
votes: discussion.votes,
|
||||
createdAt: calDateDiff(discussion.created_at)
|
||||
})) || [];
|
||||
// Fetch courses - API class handles this
|
||||
await communityApi.fetchCoursesForOrg(profileId, orgId);
|
||||
// Call API - API class handles loading, errors, navigation
|
||||
await communityApi.fetchCommunityQuestions({ orgId, isLMS });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,11 +43,11 @@
|
||||
fetchCommunityQuestions($currentOrg.id, $profile.id);
|
||||
});
|
||||
|
||||
let filteredDiscussions = $derived(
|
||||
discussions.filter(
|
||||
(discussion) =>
|
||||
discussion.title.toLowerCase().includes(searchValue.toLowerCase()) &&
|
||||
(!selectedId || discussion.courseId === selectedId)
|
||||
let filteredQuestions = $derived(
|
||||
communityApi.questions.filter(
|
||||
(question: CommunityQuestionData[number]) =>
|
||||
question.title.toLowerCase().includes(searchValue.toLowerCase()) &&
|
||||
(!selectedId || question.courseId === selectedId)
|
||||
)
|
||||
);
|
||||
</script>
|
||||
@@ -114,46 +58,50 @@
|
||||
<Select.Root type="single" bind:value={selectedId}>
|
||||
<Select.Trigger class="w-full bg-gray-100 dark:bg-neutral-800">
|
||||
<p class="truncate">
|
||||
{selectedId ? allCourses.find((course) => course.id === selectedId)?.title : $t('community.all')}
|
||||
{selectedId
|
||||
? communityApi.courses.find((course) => course.id === selectedId)?.title || $t('community.all')
|
||||
: $t('community.all')}
|
||||
</p>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="">{$t('community.all')}</Select.Item>
|
||||
{#each allCourses as course}
|
||||
<Select.Item value={course.id}>{course.title}</Select.Item>
|
||||
{#each communityApi.courses as course}
|
||||
{#if course.id}
|
||||
<Select.Item value={course.id}>{course.title}</Select.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Page.BodyHeader>
|
||||
|
||||
{#if isLoading}
|
||||
{#if communityApi.isLoading}
|
||||
<CommunityListLoader />
|
||||
<CommunityListLoader />
|
||||
<CommunityListLoader />
|
||||
<CommunityListLoader />
|
||||
{:else}
|
||||
{#each filteredDiscussions as discussion}
|
||||
{#each filteredQuestions as question}
|
||||
<Item.Root variant="outline">
|
||||
{#snippet child({ props })}
|
||||
<a href="{isLMS ? '/lms' : $currentOrgPath}/community/{discussion.slug}" {...props}>
|
||||
<Vote value={discussion.votes} />
|
||||
<a href="{isLMS ? '/lms' : $currentOrgPath}/community/{question.slug}" {...props}>
|
||||
<Vote value={question.votes} />
|
||||
<Item.Content class="gap-y-0.5">
|
||||
<Item.Title class="mt-0">
|
||||
{discussion.title}
|
||||
{question.title}
|
||||
</Item.Title>
|
||||
<Item.Description>
|
||||
{discussion.author} asked {discussion.createdAt}
|
||||
{question.authorFullname} asked {calDateDiff(question.createdAt)}
|
||||
</Item.Description>
|
||||
<a class="m-0" href="/courses/{discussion.courseId}" onclick={(e) => e.stopPropagation()}>
|
||||
<a class="m-0" href="/courses/{question.courseId}" onclick={(e) => e.stopPropagation()}>
|
||||
<span class="text-muted-foreground p-0 text-xs">
|
||||
#{discussion.courseTitle}
|
||||
#{question.courseTitle}
|
||||
</span>
|
||||
</a>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<div class="flex items-center">
|
||||
<MessageCirclePlusIcon size={16} />
|
||||
<span class="ml-1">{discussion.comments}</span>
|
||||
<span class="ml-1">{question.comments}</span>
|
||||
</div>
|
||||
</Item.Actions>
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user