Merge pull request #89 from learnhouse/swve/eng-64-implement-api-authorization-from-server

Implement api authorization from server + Migrate some pages to Server Components
This commit is contained in:
Badr B 2023-05-22 21:06:41 +02:00 committed by GitHub
commit c9c261f12f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 800 additions and 627 deletions

View file

@ -0,0 +1,9 @@
import PageLoading from "@components/Pages/PageLoading";
export default function Loading() {
// Or a custom loading skeleton component
return (
<PageLoading></PageLoading>
)
}

View file

@ -1,34 +1,52 @@
"use client";
import { default as React, useEffect, useRef } from "react";
import { default as React, } from "react";
import { useRouter } from "next/navigation";
import { getActivity } from "@services/courses/activities";
import AuthProvider from "@components/Security/AuthProvider";
import EditorWrapper from "@components/Editor/EditorWrapper";
import useSWR, { mutate } from "swr";
import { getAPIUrl, getOrgFromUri } from "@services/config/config";
import { getAPIUrl } from "@services/config/config";
import { swrFetcher } from "@services/utils/ts/requests";
import { getOrganizationContextInfo } from "@services/organizations/orgs";
import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
import { cookies } from "next/headers";
import { Metadata } from "next";
import { getActivityWithAuthHeader } from "@services/courses/activities";
type MetadataProps = {
params: { orgslug: string, courseid: string, activityid: string };
searchParams: { [key: string]: string | string[] | undefined };
};
function EditActivity(params: any) {
const router = useRouter();
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
const cookieStore = cookies();
const access_token_cookie: any = cookieStore.get('access_token_cookie');
// Get Org context information
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null)
return {
title: `Edit - ${course_meta.course.name} Activity`,
description: course_meta.course.mini_description,
};
}
const EditActivity = async (params: any) => {
const cookieStore = cookies();
const access_token_cookie: any = cookieStore.get('access_token_cookie');
const activityid = params.params.activityid;
const courseid = params.params.courseid;
const orgslug = params.params.orgslug;
const { data: courseInfo, error: error_course } = useSWR(`${getAPIUrl()}courses/meta/course_${courseid}`, swrFetcher);
const { data: activity, error: error_activity } = useSWR(`${getAPIUrl()}activities/activity_${activityid}`, swrFetcher);
const courseInfo = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null)
const activity = await getActivityWithAuthHeader(activityid, { revalidate: 0, tags: ['activities'] }, access_token_cookie ? access_token_cookie.value : null)
return (
<AuthProvider>
{!courseInfo || !activity ? <div>Loading...</div> : <EditorWrapper orgslug={orgslug} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>}
</AuthProvider>
<div>
<AuthProvider>
<EditorWrapper orgslug={orgslug} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>
</AuthProvider>
</div>
);
}

View file

@ -0,0 +1,220 @@
"use client";
import Link from "next/link";
import React, { useMemo } from "react";
import { getAPIUrl, getBackendUrl, getUriWithOrg } from "@services/config/config";
import Canva from "@components/Pages/Activities/DynamicCanva/DynamicCanva";
import styled from "styled-components";
import VideoActivity from "@components/Pages/Activities/Video/Video";
import useSWR, { mutate } from "swr";
import { Check } from "lucide-react";
import { markActivityAsComplete } from "@services/courses/activity";
import ToolTip from "@components/UI/Tooltip/Tooltip";
import DocumentPdfActivity from "@components/Pages/Activities/DocumentPdf/DocumentPdf";
interface ActivityClientProps {
activityid: string;
courseid: string;
orgslug: string;
activity: any;
course: any;
}
function ActivityClient(props: ActivityClientProps) {
const activityid = props.activityid;
const courseid = props.courseid;
const orgslug = props.orgslug;
const activity = props.activity;
const course = props.course;
async function markActivityAsCompleteFront() {
const trail = await markActivityAsComplete(orgslug, courseid, activityid);
mutate(`${getAPIUrl()}activities/activity_${activityid}`);
mutate(`${getAPIUrl()}courses/meta/course_${courseid}`);
}
return (
<>
<ActivityLayout>
<pre style={{ display: "none" }}>{JSON.stringify(activity, null, 2)}</pre>
<ActivityTopWrapper>
<ActivityThumbnail>
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}`}>
<img src={`${getBackendUrl()}content/uploads/img/${course.course.thumbnail}`} alt="" />
</Link>
</ActivityThumbnail>
<ActivityInfo>
<p>Course</p>
<h1>{course.course.name}</h1>
</ActivityInfo>
</ActivityTopWrapper>
<ChaptersWrapper>
{course.chapters.map((chapter: any) => {
return (
<>
<div style={{ display: "flex", flexDirection: "row" }} key={chapter.chapter_id}>
{chapter.activities.map((activity: any) => {
return (
<ToolTip sideOffset={-5} slateBlack content={activity.name} key={activity.id}>
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`}>
<ChapterIndicator
done={course.trail.activities_marked_complete && course.trail.activities_marked_complete.includes(activity.id) && course.trail.status == "ongoing"}
active={"activity_" + activityid === activity.id ? true : false} key={activity.id}
/>
</Link>
</ToolTip>
);
})}
</div>
&nbsp;&nbsp;&nbsp;&nbsp;
</>
);
})}
</ChaptersWrapper>
{activity ? (
<CourseContent>
{activity.type == "dynamic" && <Canva content={activity.content} activity={activity} />}
{/* todo : use apis & streams instead of this */}
{activity.type == "video" && <VideoActivity course={course} activity={activity} />}
{activity.type == "documentpdf" && <DocumentPdfActivity course={course} activity={activity} />}
<ActivityMarkerWrapper className="py-10">
{course.trail.activities_marked_complete &&
course.trail.activities_marked_complete.includes("activity_" + activityid) &&
course.trail.status == "ongoing" ? (
<button style={{ backgroundColor: "green" }}>
<i>
<Check size={20}></Check>
</i>{" "}
Already completed
</button>
) : (
<button onClick={markActivityAsCompleteFront}>
{" "}
<i>
<Check size={20}></Check>
</i>{" "}
Mark as complete
</button>
)}
</ActivityMarkerWrapper>
</CourseContent>
) : (<div></div>)}
{<div style={{ height: "100px" }}></div>}
</ActivityLayout>
</>
);
}
const ActivityLayout = styled.div``;
const ActivityThumbnail = styled.div`
padding-right: 30px;
justify-self: center;
img {
box-shadow: 0px 13px 33px -13px rgba(0, 0, 0, 0.42);
border-radius: 7px;
width: 100px;
height: 57px;
}
`;
const ActivityInfo = styled.div`
h1 {
margin-top: 0px;
}
p {
margin-top: 0;
margin-bottom: 0;
font-weight: 700;
}
`;
const ChaptersWrapper = styled.div`
display: flex;
// row
flex-direction: row;
width: 100%;
width: 1300px;
margin: 0 auto;
`;
const ChapterIndicator = styled.div < { active?: boolean, done?: boolean } > `
border-radius: 20px;
height: 5px;
background: #151515;
border-radius: 3px;
width: 35px;
background-color: ${props => props.done ? "green" : (props.active ? "#9d9d9d" : "black")};
margin: 10px;
margin-bottom: 0px;
margin-left: 0px;
&:hover {
cursor: pointer;
background-color: #9d9d9d;
}
`;
const ActivityTopWrapper = styled.div`
width: 1300px;
padding-top: 50px;
margin: 0 auto;
display: flex;
`;
const CourseContent = styled.div`
display: flex;
flex-direction: column;
background-color: white;
min-height: 600px;
`;
const ActivityMarkerWrapper = styled.div`
display: block;
width: 1300px;
justify-content: flex-end;
margin: 0 auto;
align-items: center;
button {
background-color: #151515;
border: none;
padding: 18px;
border-radius: 15px;
margin: 15px;
margin-left: 20px;
margin-top: 20px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
margin: auto;
color: white;
font-weight: 700;
font-family: "DM Sans";
font-size: 16px;
letter-spacing: -0.05em;
box-shadow: 0px 13px 33px -13px rgba(0, 0, 0, 0.42);
i {
margin-right: 5px;
// center the icon
display: flex;
align-items: center;
justify-content: center;
}
&:hover {
background-color: #000000;
}
}
`;
export default ActivityClient;

View file

@ -0,0 +1,9 @@
import PageLoading from "@components/Pages/PageLoading";
export default function Loading() {
// Or a custom loading skeleton component
return (
<PageLoading></PageLoading>
)
}

View file

@ -1,221 +1,52 @@
"use client";
import { useRouter } from "next/navigation";
import Link from "next/link";
import React, { useMemo } from "react";
import { getActivity } from "@services/courses/activities";
import { getAPIUrl, getBackendUrl, getUriWithOrg } from "@services/config/config";
import Canva from "@components/Pages/Activities/DynamicCanva/DynamicCanva";
import styled from "styled-components";
import { getCourse } from "@services/courses/courses";
import VideoActivity from "@components/Pages/Activities/Video/Video";
import useSWR, { mutate } from "swr";
import { Check } from "lucide-react";
import { swrFetcher } from "@services/utils/ts/requests";
import { markActivityAsComplete } from "@services/courses/activity";
import ToolTip from "@components/UI/Tooltip/Tooltip";
import DocumentPdfActivity from "@components/Pages/Activities/DocumentPdf/DocumentPdf";
function ActivityPage(params: any) {
const activityid = params.params.activityid;
const courseid = params.params.courseid;
const orgslug = params.params.orgslug;
const { data: course, error: error_course } = useSWR(`${getAPIUrl()}courses/meta/course_${courseid}`, swrFetcher);
const { data: activity, error: error_activity } = useSWR(`${getAPIUrl()}activities/activity_${activityid}`, swrFetcher);
import { getActivityWithAuthHeader } from "@services/courses/activities";
import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
import { cookies } from "next/headers";
import ActivityClient from "./activity";
import { getOrganizationContextInfo } from "@services/organizations/orgs";
import { Metadata } from "next";
async function markActivityAsCompleteFront() {
const trail = await markActivityAsComplete(orgslug, courseid, activityid);
mutate(`${getAPIUrl()}activities/activity_${activityid}`);
mutate(`${getAPIUrl()}courses/meta/course_${courseid}`);
}
type MetadataProps = {
params: { orgslug: string, courseid: string, activityid: string };
searchParams: { [key: string]: string | string[] | undefined };
};
return (
<>
{error_course && <p>Failed to load</p>}
{!course || !activity ? (
<div>Loading...</div>
) : (
<ActivityLayout>
<ActivityTopWrapper>
<ActivityThumbnail>
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}`}>
<img src={`${getBackendUrl()}content/uploads/img/${course.course.thumbnail}`} alt="" />
</Link>
</ActivityThumbnail>
<ActivityInfo>
<p>Course</p>
<h1>{course.course.name}</h1>
</ActivityInfo>
</ActivityTopWrapper>
<ChaptersWrapper>
{course.chapters.map((chapter: any) => {
return (
<>
<div style={{ display: "flex", flexDirection: "row" }} key={chapter.chapter_id}>
{chapter.activities.map((activity: any) => {
return (
<ToolTip sideOffset={-5} slateBlack content={activity.name} key={activity.id}>
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`}>
<ChapterIndicator
done={course.trail.activities_marked_complete && course.trail.activities_marked_complete.includes(activity.id) && course.trail.status == "ongoing"}
active={"activity_" + activityid === activity.id ? true : false} key={activity.id}
/>
</Link>
</ToolTip>
);
})}
</div>
&nbsp;&nbsp;&nbsp;&nbsp;
</>
);
})}
</ChaptersWrapper>
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
const cookieStore = cookies();
const access_token_cookie: any = cookieStore.get('access_token_cookie');
{activity ? (
<CourseContent>
{activity.type == "dynamic" && <Canva content={activity.content} activity={activity} />}
{/* todo : use apis & streams instead of this */}
{activity.type == "video" && <VideoActivity course={course} activity={activity} />}
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null )
const activity = await getActivityWithAuthHeader(params.activityid, { revalidate: 0, tags: ['activities'] }, access_token_cookie ? access_token_cookie.value : null)
{activity.type == "documentpdf" && <DocumentPdfActivity course={course} activity={activity} />}
<ActivityMarkerWrapper className="py-10">
{course.trail.activities_marked_complete &&
course.trail.activities_marked_complete.includes("activity_" + activityid) &&
course.trail.status == "ongoing" ? (
<button style={{ backgroundColor: "green" }}>
<i>
<Check size={20}></Check>
</i>{" "}
Already completed
</button>
) : (
<button onClick={markActivityAsCompleteFront}>
{" "}
<i>
<Check size={20}></Check>
</i>{" "}
Mark as complete
</button>
)}
</ActivityMarkerWrapper>
</CourseContent>
) : (<div></div>)}
{error_activity && <p>Failed to load {error_activity.message}</p>}
</ActivityLayout>
)}
</>
);
return {
title: activity.name + `${course_meta.course.name} Course`,
description: course_meta.course.mini_description,
};
}
const ActivityLayout = styled.div``;
const ActivityPage = async (params: any) => {
const cookieStore = cookies();
const access_token_cookie: any = cookieStore.get('access_token_cookie');
const activityid = params.params.activityid;
const courseid = params.params.courseid;
const orgslug = params.params.orgslug;
const ActivityThumbnail = styled.div`
padding-right: 30px;
justify-self: center;
img {
box-shadow: 0px 13px 33px -13px rgba(0, 0, 0, 0.42);
border-radius: 7px;
width: 100px;
height: 57px;
}
`;
const ActivityInfo = styled.div`
h1 {
margin-top: 0px;
}
const course_meta = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null)
const activity = await getActivityWithAuthHeader(activityid, { revalidate: 0, tags: ['activities'] }, access_token_cookie ? access_token_cookie.value : null)
return (
<>
<ActivityClient
activityid={activityid}
courseid={courseid}
orgslug={orgslug}
activity={activity}
course={course_meta}
/></>
)
}
p {
margin-top: 0;
margin-bottom: 0;
font-weight: 700;
}
`;
const ChaptersWrapper = styled.div`
display: flex;
// row
flex-direction: row;
width: 100%;
width: 1300px;
margin: 0 auto;
`;
const ChapterIndicator = styled.div < { active?: boolean, done?: boolean } > `
border-radius: 20px;
height: 5px;
background: #151515;
border-radius: 3px;
width: 35px;
background-color: ${props => props.done ? "green" : (props.active ? "#9d9d9d" : "black")};
margin: 10px;
margin-bottom: 0px;
margin-left: 0px;
&:hover {
cursor: pointer;
background-color: #9d9d9d;
}
`;
const ActivityTopWrapper = styled.div`
width: 1300px;
padding-top: 50px;
margin: 0 auto;
display: flex;
`;
const CourseContent = styled.div`
display: flex;
flex-direction: column;
background-color: white;
min-height: 600px;
`;
const ActivityMarkerWrapper = styled.div`
display: block;
width: 1300px;
justify-content: flex-end;
margin: 0 auto;
align-items: center;
button {
background-color: #151515;
border: none;
padding: 18px;
border-radius: 15px;
margin: 15px;
margin-left: 20px;
margin-top: 20px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
margin: auto;
color: white;
font-weight: 700;
font-family: "DM Sans";
font-size: 16px;
letter-spacing: -0.05em;
box-shadow: 0px 13px 33px -13px rgba(0, 0, 0, 0.42);
i {
margin-right: 5px;
// center the icon
display: flex;
align-items: center;
justify-content: center;
}
&:hover {
background-color: #000000;
}
}
`;
export default ActivityPage;
export default ActivityPage

View file

@ -0,0 +1,232 @@
"use client";
import { EyeOpenIcon, Pencil2Icon } from "@radix-ui/react-icons";
import { removeCourse, startCourse } from "@services/courses/activity";
import Link from "next/link";
import React from "react";
import styled from "styled-components";
import { getAPIUrl, getBackendUrl, getUriWithOrg } from "@services/config/config";
import useSWR, { mutate } from "swr";
import ToolTip from "@components/UI/Tooltip/Tooltip";
import PageLoading from "@components/Pages/PageLoading";
import { revalidateTags } from "@services/utils/ts/requests";
const CourseClient = (props: any) => {
const courseid = props.courseid;
const orgslug = props.orgslug;
const course = props.course;
async function startCourseUI() {
// Create activity
await startCourse("course_" + courseid, orgslug);
revalidateTags(['courses']);
}
async function quitCourse() {
// Close activity
let activity = await removeCourse("course_" + courseid, orgslug);
console.log(activity);
// Mutate course
revalidateTags(['courses']);
}
console.log(course);
return (
<>
{!course ? (
<PageLoading></PageLoading>
) : (
<CoursePageLayout className="pt-6">
<p className="text-lg font-bold">Course</p>
<h1 className="text-3xl font-bold flex items-center space-x-5">
{course.course.name}{" "}
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}/edit`} rel="noopener noreferrer">
<Pencil2Icon />
</Link>{" "}
</h1>
<CourseThumbnailWrapper>
<img src={`${getBackendUrl()}content/uploads/img/${course.course.thumbnail}`} alt="" />
</CourseThumbnailWrapper>
<CourseMetaWrapper>
<CourseMetaLeft>
<h2 className="py-3 font-bold">Description</h2>
<BoxWrapper>
<p className="py-3">{course.course.description}</p>
</BoxWrapper>
<h2 className="py-3 font-bold">What you will learn</h2>
<BoxWrapper>
<p className="py-3">{course.learnings == ![] ? "no data" : course.learnings}</p>
</BoxWrapper>
<h2 className="py-3 font-bold">Course Lessons</h2>
<BoxWrapper>
{course.chapters.map((chapter: any) => {
return (
<div
key={chapter}
className="py-3"
>
<h3 className="text-lg">{chapter.name}</h3>
<div
className="py-3"
>{chapter.activities.map((activity: any) => {
return (
<>
<p className="flex text-md">
{activity.name}
<Link className="pl-3" href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`} rel="noopener noreferrer">
<EyeOpenIcon />
</Link>{" "}
</p>
</>
);
})}</div>
</div>
);
})}
</BoxWrapper>
</CourseMetaLeft>
<CourseMetaRight>
{course.trail.status == "ongoing" ? (
<button style={{ backgroundColor: "red" }} onClick={quitCourse}>
Quit Course
</button>
) : (
<button onClick={startCourseUI}>Start Course</button>
)}
</CourseMetaRight>
</CourseMetaWrapper>
</CoursePageLayout>
)}
</>
);
};
const CourseThumbnailWrapper = styled.div`
display: flex;
padding-bottom: 20px;
img {
width: 100%;
height: 300px;
object-fit: cover;
object-position: center;
background: url(), #d9d9d9;
border: 1px solid rgba(255, 255, 255, 0.19);
box-shadow: 0px 13px 33px -13px rgba(0, 0, 0, 0.42);
border-radius: 7px;
}
`;
const CoursePageLayout = styled.div`
width: 1300px;
margin: 0 auto;
p {
margin-bottom: 0px;
letter-spacing: -0.05em;
color: #727272;
font-weight: 700;
}
h1 {
margin-top: 5px;
letter-spacing: -0.05em;
margin-bottom: 10px;
}
`;
const ChaptersWrapper = styled.div`
display: flex;
width: 100%;
`;
const CourseIndicator = styled.div< { active?: boolean, done?: boolean } >`
border-radius: 20px;
height: 5px;
background: #151515;
border-radius: 3px;
background-color: ${props => props.done ? "green" : "black"};
width: 40px;
margin: 10px;
margin-bottom: 20px;
margin-left: 0px;
&:hover {
cursor: pointer;
background-color: #9d9d9d;
}
`;
const ChapterSeparator = styled.div`
display: flex;
flex-direction: row;
padding-right: 7px;
`;
const BoxWrapper = styled.div`
background: #ffffff;
box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.03);
border-radius: 7px;
padding: 20px;
padding-top: 7px;
padding-left: 30px;
p {
font-family: "DM Sans";
font-style: normal;
font-weight: 500;
line-height: 16px;
letter-spacing: -0.02em;
color: #9d9d9d;
}
`;
const CourseMetaWrapper = styled.div`
display: flex;
justify-content: space-between;
`;
const CourseMetaLeft = styled.div`
width: 80%;
`;
const CourseMetaRight = styled.div`
background: #ffffff;
box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.03);
border-radius: 7px;
padding: 20px;
width: 30%;
display: flex;
height: 100%;
justify-content: center;
margin-left: 50px;
margin-top: 20px;
button {
width: 100%;
height: 50px;
background: #151515;
border-radius: 15px;
border: none;
color: white;
font-weight: 700;
font-family: "DM Sans";
font-size: 16px;
letter-spacing: -0.05em;
transition: all 0.2s ease;
box-shadow: 0px 13px 33px -13px rgba(0, 0, 0, 0.42);
&:hover {
cursor: pointer;
background: #000000;
}
}
`;
export default CourseClient;

View file

@ -0,0 +1,9 @@
import PageLoading from "@components/Pages/PageLoading";
export default function Loading() {
// Or a custom loading skeleton component
return (
<PageLoading></PageLoading>
)
}

View file

@ -1,261 +1,45 @@
"use client";
import { EyeOpenIcon, Pencil2Icon } from "@radix-ui/react-icons";
import { removeCourse, startCourse } from "@services/courses/activity";
import Link from "next/link";
import React from "react";
import styled from "styled-components";
import { getAPIUrl, getBackendUrl, getUriWithOrg } from "@services/config/config";
import useSWR, { mutate } from "swr";
import { swrFetcher } from "@services/utils/ts/requests";
import { useRouter } from "next/navigation";
import ToolTip from "@components/UI/Tooltip/Tooltip";
import PageLoading from "@components/Pages/PageLoading";
import React from 'react'
import CourseClient from './course'
import { cookies } from 'next/headers';
import { getCourseMetadataWithAuthHeader } from '@services/courses/courses';
import { getOrganizationContextInfo } from '@services/organizations/orgs';
import { Metadata } from 'next';
const CourseIdPage = (params: any) => {
const courseid = params.params.courseid;
const orgslug = params.params.orgslug;
const router = useRouter();
const { data: course, error: error } = useSWR(`${getAPIUrl()}courses/meta/course_${courseid}`,
(url: string, body: any) => swrFetcher(url, body, router)
);
async function startCourseUI() {
// Create activity
await startCourse("course_" + courseid, orgslug);
// Mutate course
mutate(`${getAPIUrl()}courses/meta/course_${courseid}`);
}
async function quitCourse() {
// Close activity
let activity = await removeCourse("course_" + courseid, orgslug);
console.log(activity);
// Mutate course
mutate(`${getAPIUrl()}courses/meta/course_${courseid}`);
}
return (
<>
{error && <p>Failed to load</p>}
{!course ? (
<PageLoading></PageLoading>
) : (
<CoursePageLayout>
<br></br>
<p className="text-lg font-bold">Course</p>
<h1 className="text-3xl font-bold flex items-center space-x-5">
{course.course.name}{" "}
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}/edit`} rel="noopener noreferrer">
<Pencil2Icon />
</Link>{" "}
</h1>
<ChaptersWrapper>
{course.chapters.map((chapter: any) => {
return (
<ChapterSeparator key={chapter}>
{chapter.activities.map((activity: any) => {
return (
<>
<ToolTip sideOffset={-18} slateBlack content={activity.name}>
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`}>
<CourseIndicator
done={course.trail.activities_marked_complete && course.trail.activities_marked_complete.includes(activity.id) && course.trail.status == "ongoing"}
/>
</Link>
</ToolTip>
</>
);
})}
</ChapterSeparator>
);
})}
</ChaptersWrapper>
<CourseThumbnailWrapper>
<img src={`${getBackendUrl()}content/uploads/img/${course.course.thumbnail}`} alt="" />
</CourseThumbnailWrapper>
<CourseMetaWrapper>
<CourseMetaLeft>
<h2 className="py-3 font-bold">Description</h2>
<BoxWrapper>
<p className="py-3">{course.course.description}</p>
</BoxWrapper>
<h2 className="py-3 font-bold">What you will learn</h2>
<BoxWrapper>
<p className="py-3">{course.course.learnings == ![] ? "no data" : course.course.learnings}</p>
</BoxWrapper>
<h2 className="py-3 font-bold">Course Lessons</h2>
<BoxWrapper>
{course.chapters.map((chapter: any) => {
return (
<div
key={chapter}
className="py-3"
>
<h3 className="text-lg">{chapter.name}</h3>
<div
className="py-3"
>{chapter.activities.map((activity: any) => {
return (
<>
<p className="flex text-md">
{activity.name}
<Link className="pl-3" href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`} rel="noopener noreferrer">
<EyeOpenIcon />
</Link>{" "}
</p>
</>
);
})}</div>
</div>
);
})}
</BoxWrapper>
</CourseMetaLeft>
<CourseMetaRight>
{course.trail.status == "ongoing" ? (
<button style={{ backgroundColor: "red" }} onClick={quitCourse}>
Quit Course
</button>
) : (
<button onClick={startCourseUI}>Start Course</button>
)}
</CourseMetaRight>
</CourseMetaWrapper>
</CoursePageLayout>
)}
</>
);
type MetadataProps = {
params: { orgslug: string, courseid: string };
searchParams: { [key: string]: string | string[] | undefined };
};
const CourseThumbnailWrapper = styled.div`
display: flex;
padding-bottom: 20px;
img {
width: 100%;
height: 300px;
object-fit: cover;
object-position: center;
background: url(), #d9d9d9;
border: 1px solid rgba(255, 255, 255, 0.19);
box-shadow: 0px 13px 33px -13px rgba(0, 0, 0, 0.42);
border-radius: 7px;
}
`;
const CoursePageLayout = styled.div`
width: 1300px;
margin: 0 auto;
p {
margin-bottom: 0px;
letter-spacing: -0.05em;
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
const cookieStore = cookies();
const access_token_cookie: any = cookieStore.get('access_token_cookie');
color: #727272;
font-weight: 700;
}
h1 {
margin-top: 5px;
letter-spacing: -0.05em;
margin-bottom: 10px;
}
`;
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null)
const ChaptersWrapper = styled.div`
display: flex;
width: 100%;
`;
const CourseIndicator = styled.div< { active?: boolean, done?: boolean } >`
border-radius: 20px;
height: 5px;
background: #151515;
border-radius: 3px;
return {
title: course_meta.course.name + `${org.name}`,
description: course_meta.course.mini_description,
};
}
background-color: ${props => props.done ? "green" : "black"};
width: 40px;
margin: 10px;
margin-bottom: 20px;
margin-left: 0px;
&:hover {
cursor: pointer;
background-color: #9d9d9d;
}
`;
const CoursePage = async (params: any) => {
const cookieStore = cookies();
const access_token_cookie: any = cookieStore.get('access_token_cookie');
const courseid = params.params.courseid
const orgslug = params.params.orgslug;
const course_meta = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null)
return (
<div>
<CourseClient courseid={courseid} orgslug={orgslug} course={course_meta} />
</div>
)
}
const ChapterSeparator = styled.div`
display: flex;
flex-direction: row;
padding-right: 7px;
`;
const BoxWrapper = styled.div`
background: #ffffff;
box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.03);
border-radius: 7px;
padding: 20px;
padding-top: 7px;
padding-left: 30px;
p {
font-family: "DM Sans";
font-style: normal;
font-weight: 500;
line-height: 16px;
letter-spacing: -0.02em;
color: #9d9d9d;
}
`;
const CourseMetaWrapper = styled.div`
display: flex;
justify-content: space-between;
`;
const CourseMetaLeft = styled.div`
width: 80%;
`;
const CourseMetaRight = styled.div`
background: #ffffff;
box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.03);
border-radius: 7px;
padding: 20px;
width: 30%;
display: flex;
height: 100%;
justify-content: center;
margin-left: 50px;
margin-top: 20px;
button {
width: 100%;
height: 50px;
background: #151515;
border-radius: 15px;
border: none;
color: white;
font-weight: 700;
font-family: "DM Sans";
font-size: 16px;
letter-spacing: -0.05em;
transition: all 0.2s ease;
box-shadow: 0px 13px 33px -13px rgba(0, 0, 0, 0.42);
&:hover {
cursor: pointer;
background: #000000;
}
}
`;
export default CourseIdPage;
export default CoursePage

View file

@ -17,7 +17,7 @@ export async function generateMetadata(
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
return {
title: org.name + " — Courses",
title: "Courses — " + org.name,
description: org.description,
};
}

View file

@ -1,17 +1,16 @@
export const dynamic = 'force-dynamic';
import { Metadata, ResolvingMetadata } from 'next';
import { Menu } from "@components/UI/Elements/Menu";
import { getBackendUrl, getUriWithOrg } from "@services/config/config";
import { getOrgCourses } from "@services/courses/courses";
import { getCourse, getOrgCourses, getOrgCoursesWithAuthHeader } from "@services/courses/courses";
import CoursesLogo from "public/svg/courses.svg";
import CollectionsLogo from "public/svg/collections.svg";
import Link from "next/link";
import Image from "next/image";
import { log } from "console";
import AuthProvider from "@components/Security/AuthProvider";
import { getOrgCollections } from "@services/courses/collections";
import { getOrgCollections, getOrgCollectionsWithAuthHeader } from "@services/courses/collections";
import { getOrganizationContextInfo } from '@services/organizations/orgs';
import { cookies } from 'next/headers';
type MetadataProps = {
params: { orgslug: string };
searchParams: { [key: string]: string | string[] | undefined };
@ -21,18 +20,23 @@ export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
return {
title: org.name + " — Home",
title: `Home — ${org.name}`,
description: org.description,
};
}
const OrgHomePage = async (params: any) => {
const orgslug = params.params.orgslug;
const courses = await getOrgCourses(orgslug, { revalidate: 360 , tags: ['courses'] });
const collections = await getOrgCollections();
const cookieStore = cookies();
const access_token_cookie: any = cookieStore.get('access_token_cookie');
const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null);
const collections = await getOrgCollectionsWithAuthHeader(access_token_cookie ? access_token_cookie.value : null);
// function to remove "course_" from the course_id
function removeCoursePrefix(course_id: string) {

View file

@ -1,3 +1,4 @@
'use client';
import React from "react";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";

View file

@ -1,3 +1,4 @@
'use client';
import { default as React, } from "react";
import * as Y from "yjs";
import { WebrtcProvider } from "y-webrtc";

View file

@ -1,3 +1,4 @@
'use client';
import React from 'react';
import * as Dialog from '@radix-ui/react-dialog';
import { styled, keyframes } from '@stitches/react';
@ -44,7 +45,7 @@ const Modal = (params: ModalParams) => (
{params.addDefCloseButton ? (
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
<Dialog.Close asChild>
<ButtonBlack type="submit" css={{ marginTop: 10 }}>Close</ButtonBlack>
<ButtonBlack type="submit" css={{ marginTop: 10 }}>Close</ButtonBlack>
</Dialog.Close>
</Flex>
) : null}

View file

@ -1,3 +1,4 @@
'use client';
import React from 'react'
import { Toaster } from 'react-hot-toast';

View file

@ -1,3 +1,4 @@
'use client';
import React from 'react';
import * as Tooltip from '@radix-ui/react-tooltip';
import { styled, keyframes } from '@stitches/react';
@ -6,14 +7,14 @@ import { PlusIcon } from '@radix-ui/react-icons';
type TooltipProps = {
sideOffset?: number;
content: React.ReactNode;
children: React.ReactNode;
slateBlack?: boolean;
sideOffset?: number;
content: React.ReactNode;
children: React.ReactNode;
slateBlack?: boolean;
};
const ToolTip = (props: TooltipProps) => {
return (
<Tooltip.Provider delayDuration={200}>
<Tooltip.Root>
@ -58,10 +59,10 @@ const closeAndFade = keyframes({
const TooltipContent = styled(Tooltip.Content, {
variants : {
variants: {
slateBlack: {
true: {
backgroundColor:" #5a5a5a",
backgroundColor: " #5a5a5a",
color: 'white',
},
},
@ -71,7 +72,7 @@ const TooltipContent = styled(Tooltip.Content, {
padding: '5px 10px',
fontSize: 12,
lineHeight: 1,
color:"black",
color: "black",
backgroundColor: 'rgba(217, 217, 217, 0.50)',
zIndex: 4,
boxShadow: 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
@ -97,7 +98,7 @@ const TooltipContent = styled(Tooltip.Content, {
const TooltipArrow = styled(Tooltip.Arrow, {
fill: 'white',
});
const IconButton = styled('button', {

174
front/package-lock.json generated
View file

@ -26,7 +26,7 @@
"formik": "^2.2.9",
"framer-motion": "^7.3.6",
"lucide-react": "^0.104.1",
"next": "^13.4.2",
"next": "^13.4.3",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
@ -2123,9 +2123,9 @@
}
},
"node_modules/@next/env": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.2.tgz",
"integrity": "sha512-Wqvo7lDeS0KGwtwg9TT9wKQ8raelmUxt+TQKWvG/xKfcmDXNOtCuaszcfCF8JzlBG1q0VhpI6CKaRMbVPMDWgw=="
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.3.tgz",
"integrity": "sha512-pa1ErjyFensznttAk3EIv77vFbfSYT6cLzVRK5jx4uiRuCQo+m2wCFAREaHKIy63dlgvOyMlzh6R8Inu8H3KrQ=="
},
"node_modules/@next/eslint-plugin-next": {
"version": "13.0.6",
@ -2137,9 +2137,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.2.tgz",
"integrity": "sha512-6BBlqGu3ewgJflv9iLCwO1v1hqlecaIH2AotpKfVUEzUxuuDNJQZ2a4KLb4MBl8T9/vca1YuWhSqtbF6ZuUJJw==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.3.tgz",
"integrity": "sha512-yx18udH/ZmR4Bw4M6lIIPE3JxsAZwo04iaucEfA2GMt1unXr2iodHUX/LAKNyi6xoLP2ghi0E+Xi1f4Qb8f1LQ==",
"cpu": [
"arm64"
],
@ -2152,9 +2152,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.2.tgz",
"integrity": "sha512-iZuYr7ZvGLPjPmfhhMl0ISm+z8EiyLBC1bLyFwGBxkWmPXqdJ60mzuTaDSr5WezDwv0fz32HB7JHmRC6JVHSZg==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.3.tgz",
"integrity": "sha512-Mi8xJWh2IOjryAM1mx18vwmal9eokJ2njY4nDh04scy37F0LEGJ/diL6JL6kTXi0UfUCGbMsOItf7vpReNiD2A==",
"cpu": [
"x64"
],
@ -2167,9 +2167,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.2.tgz",
"integrity": "sha512-2xVabFtIge6BJTcJrW8YuUnYTuQjh4jEuRuS2mscyNVOj6zUZkom3CQg+egKOoS+zh2rrro66ffSKIS+ztFJTg==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.3.tgz",
"integrity": "sha512-aBvtry4bxJ1xwKZ/LVPeBGBwWVwxa4bTnNkRRw6YffJnn/f4Tv4EGDPaVeYHZGQVA56wsGbtA6nZMuWs/EIk4Q==",
"cpu": [
"arm64"
],
@ -2182,9 +2182,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.2.tgz",
"integrity": "sha512-wKRCQ27xCUJx5d6IivfjYGq8oVngqIhlhSAJntgXLt7Uo9sRT/3EppMHqUZRfyuNBTbykEre1s5166z+pvRB5A==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.3.tgz",
"integrity": "sha512-krT+2G3kEsEUvZoYte3/2IscscDraYPc2B+fDJFipPktJmrv088Pei/RjrhWm5TMIy5URYjZUoDZdh5k940Dyw==",
"cpu": [
"arm64"
],
@ -2197,9 +2197,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.2.tgz",
"integrity": "sha512-NpCa+UVhhuNeaFVUP1Bftm0uqtvLWq2JTm7+Ta48+2Uqj2mNXrDIvyn1DY/ZEfmW/1yvGBRaUAv9zkMkMRixQA==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.3.tgz",
"integrity": "sha512-AMdFX6EKJjC0G/CM6hJvkY8wUjCcbdj3Qg7uAQJ7PVejRWaVt0sDTMavbRfgMchx8h8KsAudUCtdFkG9hlEClw==",
"cpu": [
"x64"
],
@ -2212,9 +2212,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.2.tgz",
"integrity": "sha512-ZWVC72x0lW4aj44e3khvBrj2oSYj1bD0jESmyah3zG/3DplEy/FOtYkMzbMjHTdDSheso7zH8GIlW6CDQnKhmQ==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.3.tgz",
"integrity": "sha512-jySgSXE48shaLtcQbiFO9ajE9mqz7pcAVLnVLvRIlUHyQYR/WyZdK8ehLs65Mz6j9cLrJM+YdmdJPyV4WDaz2g==",
"cpu": [
"x64"
],
@ -2227,9 +2227,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.2.tgz",
"integrity": "sha512-pLT+OWYpzJig5K4VKhLttlIfBcVZfr2+Xbjra0Tjs83NQSkFS+y7xx+YhCwvpEmXYLIvaggj2ONPyjbiigOvHQ==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.3.tgz",
"integrity": "sha512-5DxHo8uYcaADiE9pHrg8o28VMt/1kR8voDehmfs9AqS0qSClxAAl+CchjdboUvbCjdNWL1MISCvEfKY2InJ3JA==",
"cpu": [
"arm64"
],
@ -2242,9 +2242,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.2.tgz",
"integrity": "sha512-dhpiksQCyGca4WY0fJyzK3FxMDFoqMb0Cn+uDB+9GYjpU2K5//UGPQlCwiK4JHxuhg8oLMag5Nf3/IPSJNG8jw==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.3.tgz",
"integrity": "sha512-LaqkF3d+GXRA5X6zrUjQUrXm2MN/3E2arXBtn5C7avBCNYfm9G3Xc646AmmmpN3DJZVaMYliMyCIQCMDEzk80w==",
"cpu": [
"ia32"
],
@ -2257,9 +2257,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.2.tgz",
"integrity": "sha512-O7bort1Vld00cu8g0jHZq3cbSTUNMohOEvYqsqE10+yfohhdPHzvzO+ziJRz4Dyyr/fYKREwS7gR4JC0soSOMw==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.3.tgz",
"integrity": "sha512-jglUk/x7ZWeOJWlVoKyIAkHLTI+qEkOriOOV+3hr1GyiywzcqfI7TpFSiwC7kk1scOiH7NTFKp8mA3XPNO9bDw==",
"cpu": [
"x64"
],
@ -6371,11 +6371,11 @@
"dev": true
},
"node_modules/next": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/next/-/next-13.4.2.tgz",
"integrity": "sha512-aNFqLs3a3nTGvLWlO9SUhCuMUHVPSFQC0+tDNGAsDXqx+WJDFSbvc233gOJ5H19SBc7nw36A9LwQepOJ2u/8Kg==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/next/-/next-13.4.3.tgz",
"integrity": "sha512-FV3pBrAAnAIfOclTvncw9dDohyeuEEXPe5KNcva91anT/rdycWbgtu3IjUj4n5yHnWK8YEPo0vrUecHmnmUNbA==",
"dependencies": {
"@next/env": "13.4.2",
"@next/env": "13.4.3",
"@swc/helpers": "0.5.1",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001406",
@ -6390,15 +6390,15 @@
"node": ">=16.8.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "13.4.2",
"@next/swc-darwin-x64": "13.4.2",
"@next/swc-linux-arm64-gnu": "13.4.2",
"@next/swc-linux-arm64-musl": "13.4.2",
"@next/swc-linux-x64-gnu": "13.4.2",
"@next/swc-linux-x64-musl": "13.4.2",
"@next/swc-win32-arm64-msvc": "13.4.2",
"@next/swc-win32-ia32-msvc": "13.4.2",
"@next/swc-win32-x64-msvc": "13.4.2"
"@next/swc-darwin-arm64": "13.4.3",
"@next/swc-darwin-x64": "13.4.3",
"@next/swc-linux-arm64-gnu": "13.4.3",
"@next/swc-linux-arm64-musl": "13.4.3",
"@next/swc-linux-x64-gnu": "13.4.3",
"@next/swc-linux-x64-musl": "13.4.3",
"@next/swc-win32-arm64-msvc": "13.4.3",
"@next/swc-win32-ia32-msvc": "13.4.3",
"@next/swc-win32-x64-msvc": "13.4.3"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@ -10043,9 +10043,9 @@
}
},
"@next/env": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.2.tgz",
"integrity": "sha512-Wqvo7lDeS0KGwtwg9TT9wKQ8raelmUxt+TQKWvG/xKfcmDXNOtCuaszcfCF8JzlBG1q0VhpI6CKaRMbVPMDWgw=="
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.3.tgz",
"integrity": "sha512-pa1ErjyFensznttAk3EIv77vFbfSYT6cLzVRK5jx4uiRuCQo+m2wCFAREaHKIy63dlgvOyMlzh6R8Inu8H3KrQ=="
},
"@next/eslint-plugin-next": {
"version": "13.0.6",
@ -10057,57 +10057,57 @@
}
},
"@next/swc-darwin-arm64": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.2.tgz",
"integrity": "sha512-6BBlqGu3ewgJflv9iLCwO1v1hqlecaIH2AotpKfVUEzUxuuDNJQZ2a4KLb4MBl8T9/vca1YuWhSqtbF6ZuUJJw==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.3.tgz",
"integrity": "sha512-yx18udH/ZmR4Bw4M6lIIPE3JxsAZwo04iaucEfA2GMt1unXr2iodHUX/LAKNyi6xoLP2ghi0E+Xi1f4Qb8f1LQ==",
"optional": true
},
"@next/swc-darwin-x64": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.2.tgz",
"integrity": "sha512-iZuYr7ZvGLPjPmfhhMl0ISm+z8EiyLBC1bLyFwGBxkWmPXqdJ60mzuTaDSr5WezDwv0fz32HB7JHmRC6JVHSZg==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.3.tgz",
"integrity": "sha512-Mi8xJWh2IOjryAM1mx18vwmal9eokJ2njY4nDh04scy37F0LEGJ/diL6JL6kTXi0UfUCGbMsOItf7vpReNiD2A==",
"optional": true
},
"@next/swc-linux-arm64-gnu": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.2.tgz",
"integrity": "sha512-2xVabFtIge6BJTcJrW8YuUnYTuQjh4jEuRuS2mscyNVOj6zUZkom3CQg+egKOoS+zh2rrro66ffSKIS+ztFJTg==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.3.tgz",
"integrity": "sha512-aBvtry4bxJ1xwKZ/LVPeBGBwWVwxa4bTnNkRRw6YffJnn/f4Tv4EGDPaVeYHZGQVA56wsGbtA6nZMuWs/EIk4Q==",
"optional": true
},
"@next/swc-linux-arm64-musl": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.2.tgz",
"integrity": "sha512-wKRCQ27xCUJx5d6IivfjYGq8oVngqIhlhSAJntgXLt7Uo9sRT/3EppMHqUZRfyuNBTbykEre1s5166z+pvRB5A==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.3.tgz",
"integrity": "sha512-krT+2G3kEsEUvZoYte3/2IscscDraYPc2B+fDJFipPktJmrv088Pei/RjrhWm5TMIy5URYjZUoDZdh5k940Dyw==",
"optional": true
},
"@next/swc-linux-x64-gnu": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.2.tgz",
"integrity": "sha512-NpCa+UVhhuNeaFVUP1Bftm0uqtvLWq2JTm7+Ta48+2Uqj2mNXrDIvyn1DY/ZEfmW/1yvGBRaUAv9zkMkMRixQA==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.3.tgz",
"integrity": "sha512-AMdFX6EKJjC0G/CM6hJvkY8wUjCcbdj3Qg7uAQJ7PVejRWaVt0sDTMavbRfgMchx8h8KsAudUCtdFkG9hlEClw==",
"optional": true
},
"@next/swc-linux-x64-musl": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.2.tgz",
"integrity": "sha512-ZWVC72x0lW4aj44e3khvBrj2oSYj1bD0jESmyah3zG/3DplEy/FOtYkMzbMjHTdDSheso7zH8GIlW6CDQnKhmQ==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.3.tgz",
"integrity": "sha512-jySgSXE48shaLtcQbiFO9ajE9mqz7pcAVLnVLvRIlUHyQYR/WyZdK8ehLs65Mz6j9cLrJM+YdmdJPyV4WDaz2g==",
"optional": true
},
"@next/swc-win32-arm64-msvc": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.2.tgz",
"integrity": "sha512-pLT+OWYpzJig5K4VKhLttlIfBcVZfr2+Xbjra0Tjs83NQSkFS+y7xx+YhCwvpEmXYLIvaggj2ONPyjbiigOvHQ==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.3.tgz",
"integrity": "sha512-5DxHo8uYcaADiE9pHrg8o28VMt/1kR8voDehmfs9AqS0qSClxAAl+CchjdboUvbCjdNWL1MISCvEfKY2InJ3JA==",
"optional": true
},
"@next/swc-win32-ia32-msvc": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.2.tgz",
"integrity": "sha512-dhpiksQCyGca4WY0fJyzK3FxMDFoqMb0Cn+uDB+9GYjpU2K5//UGPQlCwiK4JHxuhg8oLMag5Nf3/IPSJNG8jw==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.3.tgz",
"integrity": "sha512-LaqkF3d+GXRA5X6zrUjQUrXm2MN/3E2arXBtn5C7avBCNYfm9G3Xc646AmmmpN3DJZVaMYliMyCIQCMDEzk80w==",
"optional": true
},
"@next/swc-win32-x64-msvc": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.2.tgz",
"integrity": "sha512-O7bort1Vld00cu8g0jHZq3cbSTUNMohOEvYqsqE10+yfohhdPHzvzO+ziJRz4Dyyr/fYKREwS7gR4JC0soSOMw==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.3.tgz",
"integrity": "sha512-jglUk/x7ZWeOJWlVoKyIAkHLTI+qEkOriOOV+3hr1GyiywzcqfI7TpFSiwC7kk1scOiH7NTFKp8mA3XPNO9bDw==",
"optional": true
},
"@nicolo-ribaudo/chokidar-2": {
@ -13076,20 +13076,20 @@
"dev": true
},
"next": {
"version": "13.4.2",
"resolved": "https://registry.npmjs.org/next/-/next-13.4.2.tgz",
"integrity": "sha512-aNFqLs3a3nTGvLWlO9SUhCuMUHVPSFQC0+tDNGAsDXqx+WJDFSbvc233gOJ5H19SBc7nw36A9LwQepOJ2u/8Kg==",
"version": "13.4.3",
"resolved": "https://registry.npmjs.org/next/-/next-13.4.3.tgz",
"integrity": "sha512-FV3pBrAAnAIfOclTvncw9dDohyeuEEXPe5KNcva91anT/rdycWbgtu3IjUj4n5yHnWK8YEPo0vrUecHmnmUNbA==",
"requires": {
"@next/env": "13.4.2",
"@next/swc-darwin-arm64": "13.4.2",
"@next/swc-darwin-x64": "13.4.2",
"@next/swc-linux-arm64-gnu": "13.4.2",
"@next/swc-linux-arm64-musl": "13.4.2",
"@next/swc-linux-x64-gnu": "13.4.2",
"@next/swc-linux-x64-musl": "13.4.2",
"@next/swc-win32-arm64-msvc": "13.4.2",
"@next/swc-win32-ia32-msvc": "13.4.2",
"@next/swc-win32-x64-msvc": "13.4.2",
"@next/env": "13.4.3",
"@next/swc-darwin-arm64": "13.4.3",
"@next/swc-darwin-x64": "13.4.3",
"@next/swc-linux-arm64-gnu": "13.4.3",
"@next/swc-linux-arm64-musl": "13.4.3",
"@next/swc-linux-x64-gnu": "13.4.3",
"@next/swc-linux-x64-musl": "13.4.3",
"@next/swc-win32-arm64-msvc": "13.4.3",
"@next/swc-win32-ia32-msvc": "13.4.3",
"@next/swc-win32-x64-msvc": "13.4.3",
"@swc/helpers": "0.5.1",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001406",

View file

@ -27,7 +27,7 @@
"formik": "^2.2.9",
"framer-motion": "^7.3.6",
"lucide-react": "^0.104.1",
"next": "^13.4.2",
"next": "^13.4.3",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",

View file

@ -12,8 +12,7 @@ export async function loginAndGetToken(username: string, password: string): Prom
// Request Config
// get origin
const origin = window.location.origin;
const HeadersConfig = new Headers({ "Content-Type": "application/x-www-form-urlencoded" , Origin: origin });
const HeadersConfig = new Headers({ "Content-Type": "application/x-www-form-urlencoded" });
const urlencoded = new URLSearchParams({ username: username, password: password });
const requestOptions: any = {

View file

@ -1,5 +1,5 @@
import { getAPIUrl } from "@services/config/config";
import { RequestBody, RequestBodyForm } from "@services/utils/ts/requests";
import { RequestBody, RequestBodyForm, RequestBodyWithAuthHeader } from "@services/utils/ts/requests";
export async function createActivity(data: any, chapter_id: any, org_id: any) {
data.content = {};
@ -40,14 +40,20 @@ export async function createExternalVideoActivity(data: any, activity: any, chap
// add coursechapter_id to data
data.coursechapter_id = chapter_id;
data.activity_id = activity.id;
const result = await fetch(`${getAPIUrl()}activities/external_video?coursechapter_id=${chapter_id}`, RequestBody("POST", data, null));
const res = await result.json();
return res;
}
export async function getActivity(activity_id: any, next: any) {
const result = await fetch(`${getAPIUrl()}activities/${activity_id}`, RequestBody("GET", null,next));
const result = await fetch(`${getAPIUrl()}activities/${activity_id}`, RequestBody("GET", null, next));
const res = await result.json();
return res;
}
export async function getActivityWithAuthHeader(activity_id: any, next: any, access_token: string) {
const result = await fetch(`${getAPIUrl()}activities/activity_${activity_id}`, RequestBodyWithAuthHeader("GET", null, next, access_token));
const res = await result.json();
return res;
}

View file

@ -1,5 +1,5 @@
import { getAPIUrl } from "@services/config/config";
import { RequestBody, errorHandling } from "@services/utils/ts/requests";
import { RequestBody, RequestBodyWithAuthHeader, errorHandling } from "@services/utils/ts/requests";
/*
This file includes only POST, PUT, DELETE requests
@ -8,11 +8,13 @@ import { RequestBody, errorHandling } from "@services/utils/ts/requests";
//TODO : depreciate this function
export async function getCourseChaptersMetadata(course_id: any, next: any) {
const result = await fetch(`${getAPIUrl()}chapters/meta/course_${course_id}`, RequestBody("GET", null,next));
const result = await fetch(`${getAPIUrl()}chapters/meta/course_${course_id}`, RequestBody("GET", null, next));
const res = await errorHandling(result);
return res;
}
export async function updateChaptersMetadata(course_id: any, data: any) {
const result: any = await fetch(`${getAPIUrl()}chapters/meta/course_${course_id}`, RequestBody("PUT", data, null));
const res = await errorHandling(result);

View file

@ -1,5 +1,5 @@
import { getAPIUrl } from "../config/config";
import { RequestBody, errorHandling } from "@services/utils/ts/requests";
import { RequestBody, RequestBodyWithAuthHeader, errorHandling } from "@services/utils/ts/requests";
/*
This file includes only POST, PUT, DELETE requests
@ -19,7 +19,7 @@ export async function createCollection(collection: any) {
return res;
}
// Get collections
// Get collections
// TODO : add per org filter
export async function getOrgCollections() {
const result: any = await fetch(`${getAPIUrl()}collections/page/1/limit/10`, { next: { revalidate: 10 } });
@ -27,3 +27,8 @@ export async function getOrgCollections() {
return res;
}
export async function getOrgCollectionsWithAuthHeader(access_token: string) {
const result: any = await fetch(`${getAPIUrl()}collections/page/1/limit/10`, RequestBodyWithAuthHeader("GET", null, { revalidate: 10 }, access_token));
const res = await errorHandling(result);
return res;
}

View file

@ -1,5 +1,5 @@
import { getAPIUrl } from "@services/config/config";
import { RequestBody, RequestBodyForm, errorHandling } from "@services/utils/ts/requests";
import { RequestBody, RequestBodyForm, RequestBodyWithAuthHeader, errorHandling } from "@services/utils/ts/requests";
/*
This file includes only POST, PUT, DELETE requests
@ -9,7 +9,19 @@ import { RequestBody, RequestBodyForm, errorHandling } from "@services/utils/ts/
export async function getOrgCourses(org_id: number, next: any) {
const result: any = await fetch(`${getAPIUrl()}courses/org_slug/${org_id}/page/1/limit/10`, RequestBody("GET", null, next));
const res = await errorHandling(result);
return res;
}
export async function getOrgCoursesWithAuthHeader(org_id: number, next: any, access_token: string) {
const result: any = await fetch(`${getAPIUrl()}courses/org_slug/${org_id}/page/1/limit/10`, RequestBodyWithAuthHeader("GET", null, next, access_token));
const res = await errorHandling(result);
return res;
}
export async function getCourseMetadataWithAuthHeader(course_id: any, next: any, access_token: string) {
const result = await fetch(`${getAPIUrl()}courses/meta/course_${course_id}`, RequestBodyWithAuthHeader("GET", null, next, access_token));
const res = await errorHandling(result);
return res;
}

View file

@ -18,6 +18,20 @@ export const RequestBody = (method: string, data: any, next: any) => {
return options;
};
export const RequestBodyWithAuthHeader = (method: string, data: any, next: any, token: string) => {
let HeadersConfig = new Headers(token ? { "Content-Type": "application/json", Authorization: `Bearer ${token}` } : { "Content-Type": "application/json" });
let options: any = {
method: method,
headers: HeadersConfig,
redirect: "follow",
credentials: "include",
body: data,
// Next.js
next: next,
};
return options;
};
export const RequestBodyForm = (method: string, data: any, next: any) => {
let HeadersConfig = new Headers({});
let options: any = {

View file

@ -1,13 +1,14 @@
from urllib.request import Request
from fastapi import Depends, APIRouter, HTTPException, status, Request
from fastapi.security import OAuth2PasswordRequestForm
from fastapi import Depends, APIRouter, HTTPException, Response, status, Request
from fastapi.security import OAuth2PasswordRequestForm
from src.security.auth import *
from src.services.users.users import *
router = APIRouter()
@router.post('/refresh')
@router.post("/refresh")
def refresh(Authorize: AuthJWT = Depends()):
"""
The jwt_refresh_token_required() function insures a valid refresh
@ -18,11 +19,17 @@ def refresh(Authorize: AuthJWT = Depends()):
Authorize.jwt_refresh_token_required()
current_user = Authorize.get_jwt_subject()
new_access_token = Authorize.create_access_token(subject=current_user) # type: ignore
new_access_token = Authorize.create_access_token(subject=current_user) # type: ignore
return {"access_token": new_access_token}
@router.post('/login')
async def login(request: Request,Authorize: AuthJWT = Depends(), form_data: OAuth2PasswordRequestForm = Depends()):
@router.post("/login")
async def login(
request: Request,
response: Response,
Authorize: AuthJWT = Depends(),
form_data: OAuth2PasswordRequestForm = Depends(),
):
user = await authenticate_user(request, form_data.username, form_data.password)
if not user:
raise HTTPException(
@ -34,10 +41,17 @@ async def login(request: Request,Authorize: AuthJWT = Depends(), form_data: OAut
access_token = Authorize.create_access_token(subject=form_data.username)
refresh_token = Authorize.create_refresh_token(subject=form_data.username)
Authorize.set_refresh_cookies(refresh_token)
Authorize.set_access_cookies(access_token)
return {"access_token": access_token , "refresh_token": refresh_token}
# set cookies using fastapi
response.set_cookie(key="access_token_cookie", value=access_token , httponly=False)
@router.delete('/logout')
result = {
"user": user,
"tokens": {"access_token": access_token, "refresh_token": refresh_token},
}
return result
@router.delete("/logout")
def logout(Authorize: AuthJWT = Depends()):
"""
Because the JWT are stored in an httponly cookie now, we cannot
@ -47,4 +61,4 @@ def logout(Authorize: AuthJWT = Depends()):
Authorize.jwt_required()
Authorize.unset_jwt_cookies()
return {"msg":"Successfully logout"}
return {"msg": "Successfully logout"}

View file

@ -10,26 +10,26 @@ from fastapi_jwt_auth import AuthJWT
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
#### JWT Auth ####################################################
class Settings(BaseModel):
authjwt_secret_key: str = "secret"
authjwt_token_location = {"cookies"}
authjwt_token_location = {"cookies", "headers"}
authjwt_cookie_csrf_protect = False
authjwt_access_token_expires = False # (pre-alpha only) # TODO: set to 1 hour
authjwt_cookie_samesite = "none"
authjwt_access_token_expires = False # (pre-alpha only) # TODO: set to 1 hour
authjwt_cookie_samesite = "lax"
authjwt_cookie_secure = True
@AuthJWT.load_config # type: ignore
authjwt_cookie_domain = ".localhost"
@AuthJWT.load_config # type: ignore
def get_config():
return Settings()
#### JWT Auth ####################################################
#### Classes ####################################################
@ -41,10 +41,11 @@ class Token(BaseModel):
class TokenData(BaseModel):
username: str | None = None
#### Classes ####################################################
async def authenticate_user(request: Request,email: str, password: str):
async def authenticate_user(request: Request, email: str, password: str):
user = await security_get_user(request, email)
if not user:
return False
@ -64,29 +65,28 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None):
return encoded_jwt
async def get_current_user(request: Request, Authorize: AuthJWT = Depends()):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
Authorize.jwt_optional()
username = Authorize.get_jwt_subject() or None
token_data = TokenData(username=username) # type: ignore
except JWTError:
raise credentials_exception
if username:
user = await security_get_user(request, email=token_data.username) # type: ignore # treated as an email
if username:
user = await security_get_user(request, email=token_data.username) # type: ignore # treated as an email
if user is None:
raise credentials_exception
return PublicUser(**user.dict())
else:
return AnonymousUser()
async def non_public_endpoint(current_user: PublicUser ):
async def non_public_endpoint(current_user: PublicUser):
if isinstance(current_user, AnonymousUser):
raise HTTPException(status_code=401, detail="Not authenticated")
raise HTTPException(status_code=401, detail="Not authenticated")