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 @@
import { default as React, } from "react";
"use client";
import { default as React, useEffect, useRef } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { getActivity } from "@services/courses/activities";
import AuthProvider from "@components/Security/AuthProvider"; import AuthProvider from "@components/Security/AuthProvider";
import EditorWrapper from "@components/Editor/EditorWrapper"; import EditorWrapper from "@components/Editor/EditorWrapper";
import useSWR, { mutate } from "swr"; import { getAPIUrl } from "@services/config/config";
import { getAPIUrl, getOrgFromUri } from "@services/config/config";
import { swrFetcher } from "@services/utils/ts/requests"; 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) { export async function generateMetadata(
const router = useRouter(); { 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 activityid = params.params.activityid;
const courseid = params.params.courseid; const courseid = params.params.courseid;
const orgslug = params.params.orgslug; 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 ( return (
<AuthProvider> <div>
{!courseInfo || !activity ? <div>Loading...</div> : <EditorWrapper orgslug={orgslug} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>} <AuthProvider>
</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 { getActivityWithAuthHeader } from "@services/courses/activities";
import { useRouter } from "next/navigation"; import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
import Link from "next/link"; import { cookies } from "next/headers";
import React, { useMemo } from "react"; import ActivityClient from "./activity";
import { getActivity } from "@services/courses/activities"; import { getOrganizationContextInfo } from "@services/organizations/orgs";
import { getAPIUrl, getBackendUrl, getUriWithOrg } from "@services/config/config"; import { Metadata } from "next";
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);
async function markActivityAsCompleteFront() { type MetadataProps = {
const trail = await markActivityAsComplete(orgslug, courseid, activityid); params: { orgslug: string, courseid: string, activityid: string };
mutate(`${getAPIUrl()}activities/activity_${activityid}`); searchParams: { [key: string]: string | string[] | undefined };
mutate(`${getAPIUrl()}courses/meta/course_${courseid}`); };
}
return ( export async function generateMetadata(
<> { params }: MetadataProps,
{error_course && <p>Failed to load</p>} ): Promise<Metadata> {
{!course || !activity ? ( const cookieStore = cookies();
<div>Loading...</div> const access_token_cookie: any = cookieStore.get('access_token_cookie');
) : (
<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>
{activity ? ( // Get Org context information
<CourseContent> const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
{activity.type == "dynamic" && <Canva content={activity.content} activity={activity} />} const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null )
{/* todo : use apis & streams instead of this */} const activity = await getActivityWithAuthHeader(params.activityid, { revalidate: 0, tags: ['activities'] }, access_token_cookie ? access_token_cookie.value : null)
{activity.type == "video" && <VideoActivity course={course} activity={activity} />}
{activity.type == "documentpdf" && <DocumentPdfActivity course={course} activity={activity} />} return {
title: activity.name + `${course_meta.course.name} Course`,
<ActivityMarkerWrapper className="py-10"> description: course_meta.course.mini_description,
};
{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>
)}
</>
);
} }
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` const course_meta = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null)
padding-right: 30px; const activity = await getActivityWithAuthHeader(activityid, { revalidate: 0, tags: ['activities'] }, access_token_cookie ? access_token_cookie.value : null)
justify-self: center; return (
img { <>
box-shadow: 0px 13px 33px -13px rgba(0, 0, 0, 0.42); <ActivityClient
border-radius: 7px; activityid={activityid}
width: 100px; courseid={courseid}
height: 57px; orgslug={orgslug}
} activity={activity}
`; course={course_meta}
const ActivityInfo = styled.div` /></>
h1 { )
margin-top: 0px; }
}
p { export default ActivityPage
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;

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 React from 'react'
import { EyeOpenIcon, Pencil2Icon } from "@radix-ui/react-icons"; import CourseClient from './course'
import { removeCourse, startCourse } from "@services/courses/activity"; import { cookies } from 'next/headers';
import Link from "next/link"; import { getCourseMetadataWithAuthHeader } from '@services/courses/courses';
import React from "react"; import { getOrganizationContextInfo } from '@services/organizations/orgs';
import styled from "styled-components"; import { Metadata } from 'next';
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";
const CourseIdPage = (params: any) => { type MetadataProps = {
const courseid = params.params.courseid; params: { orgslug: string, courseid: string };
const orgslug = params.params.orgslug; searchParams: { [key: string]: string | string[] | undefined };
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>
)}
</>
);
}; };
const CourseThumbnailWrapper = styled.div` export async function generateMetadata(
display: flex; { params }: MetadataProps,
padding-bottom: 20px; ): Promise<Metadata> {
img { const cookieStore = cookies();
width: 100%; const access_token_cookie: any = cookieStore.get('access_token_cookie');
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; // Get Org context information
font-weight: 700; 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)
h1 {
margin-top: 5px;
letter-spacing: -0.05em;
margin-bottom: 10px;
}
`;
const ChaptersWrapper = styled.div` return {
display: flex; title: course_meta.course.name + `${org.name}`,
width: 100%; description: course_meta.course.mini_description,
`; };
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 { const CoursePage = async (params: any) => {
cursor: pointer; const cookieStore = cookies();
background-color: #9d9d9d; 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` export default CoursePage
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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

174
front/package-lock.json generated
View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import { getAPIUrl } from "@services/config/config"; 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) { export async function createActivity(data: any, chapter_id: any, org_id: any) {
data.content = {}; data.content = {};
@ -40,14 +40,20 @@ export async function createExternalVideoActivity(data: any, activity: any, chap
// add coursechapter_id to data // add coursechapter_id to data
data.coursechapter_id = chapter_id; data.coursechapter_id = chapter_id;
data.activity_id = activity.id; data.activity_id = activity.id;
const result = await fetch(`${getAPIUrl()}activities/external_video?coursechapter_id=${chapter_id}`, RequestBody("POST", data, null)); const result = await fetch(`${getAPIUrl()}activities/external_video?coursechapter_id=${chapter_id}`, RequestBody("POST", data, null));
const res = await result.json(); const res = await result.json();
return res; return res;
} }
export async function getActivity(activity_id: any, next: any) { 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(); const res = await result.json();
return res; return res;
} }

View file

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

View file

@ -1,5 +1,5 @@
import { getAPIUrl } from "../config/config"; 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 This file includes only POST, PUT, DELETE requests
@ -19,7 +19,7 @@ export async function createCollection(collection: any) {
return res; return res;
} }
// Get collections // Get collections
// TODO : add per org filter // TODO : add per org filter
export async function getOrgCollections() { export async function getOrgCollections() {
const result: any = await fetch(`${getAPIUrl()}collections/page/1/limit/10`, { next: { revalidate: 10 } }); const result: any = await fetch(`${getAPIUrl()}collections/page/1/limit/10`, { next: { revalidate: 10 } });
@ -27,3 +27,8 @@ export async function getOrgCollections() {
return res; 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 { 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 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) { 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 result: any = await fetch(`${getAPIUrl()}courses/org_slug/${org_id}/page/1/limit/10`, RequestBody("GET", null, next));
const res = await errorHandling(result); 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; return res;
} }

View file

@ -18,6 +18,20 @@ export const RequestBody = (method: string, data: any, next: any) => {
return options; 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) => { export const RequestBodyForm = (method: string, data: any, next: any) => {
let HeadersConfig = new Headers({}); let HeadersConfig = new Headers({});
let options: any = { let options: any = {

View file

@ -1,13 +1,14 @@
from urllib.request import Request from urllib.request import Request
from fastapi import Depends, APIRouter, HTTPException, status, Request from fastapi import Depends, APIRouter, HTTPException, Response, status, Request
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from src.security.auth import * from src.security.auth import *
from src.services.users.users import * from src.services.users.users import *
router = APIRouter() router = APIRouter()
@router.post('/refresh')
@router.post("/refresh")
def refresh(Authorize: AuthJWT = Depends()): def refresh(Authorize: AuthJWT = Depends()):
""" """
The jwt_refresh_token_required() function insures a valid refresh The jwt_refresh_token_required() function insures a valid refresh
@ -18,11 +19,17 @@ def refresh(Authorize: AuthJWT = Depends()):
Authorize.jwt_refresh_token_required() Authorize.jwt_refresh_token_required()
current_user = Authorize.get_jwt_subject() 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} 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) user = await authenticate_user(request, form_data.username, form_data.password)
if not user: if not user:
raise HTTPException( 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) access_token = Authorize.create_access_token(subject=form_data.username)
refresh_token = Authorize.create_refresh_token(subject=form_data.username) refresh_token = Authorize.create_refresh_token(subject=form_data.username)
Authorize.set_refresh_cookies(refresh_token) Authorize.set_refresh_cookies(refresh_token)
Authorize.set_access_cookies(access_token) # set cookies using fastapi
return {"access_token": access_token , "refresh_token": refresh_token} 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()): def logout(Authorize: AuthJWT = Depends()):
""" """
Because the JWT are stored in an httponly cookie now, we cannot 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.jwt_required()
Authorize.unset_jwt_cookies() 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") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
#### JWT Auth #################################################### #### JWT Auth ####################################################
class Settings(BaseModel): class Settings(BaseModel):
authjwt_secret_key: str = "secret" authjwt_secret_key: str = "secret"
authjwt_token_location = {"cookies"} authjwt_token_location = {"cookies", "headers"}
authjwt_cookie_csrf_protect = False authjwt_cookie_csrf_protect = False
authjwt_access_token_expires = False # (pre-alpha only) # TODO: set to 1 hour authjwt_access_token_expires = False # (pre-alpha only) # TODO: set to 1 hour
authjwt_cookie_samesite = "none" authjwt_cookie_samesite = "lax"
authjwt_cookie_secure = True authjwt_cookie_secure = True
authjwt_cookie_domain = ".localhost"
@AuthJWT.load_config # type: ignore
@AuthJWT.load_config # type: ignore
def get_config(): def get_config():
return Settings() return Settings()
#### JWT Auth #################################################### #### JWT Auth ####################################################
#### Classes #################################################### #### Classes ####################################################
@ -41,10 +41,11 @@ class Token(BaseModel):
class TokenData(BaseModel): class TokenData(BaseModel):
username: str | None = None username: str | None = None
#### Classes #################################################### #### 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) user = await security_get_user(request, email)
if not user: if not user:
return False return False
@ -64,29 +65,28 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None):
return encoded_jwt return encoded_jwt
async def get_current_user(request: Request, Authorize: AuthJWT = Depends()): async def get_current_user(request: Request, Authorize: AuthJWT = Depends()):
credentials_exception = HTTPException( credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials", detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
try: try:
Authorize.jwt_optional() Authorize.jwt_optional()
username = Authorize.get_jwt_subject() or None username = Authorize.get_jwt_subject() or None
token_data = TokenData(username=username) # type: ignore token_data = TokenData(username=username) # type: ignore
except JWTError: except JWTError:
raise credentials_exception raise credentials_exception
if username: if username:
user = await security_get_user(request, email=token_data.username) # type: ignore # treated as an email user = await security_get_user(request, email=token_data.username) # type: ignore # treated as an email
if user is None: if user is None:
raise credentials_exception raise credentials_exception
return PublicUser(**user.dict()) return PublicUser(**user.dict())
else: else:
return AnonymousUser() return AnonymousUser()
async def non_public_endpoint(current_user: PublicUser ):
async def non_public_endpoint(current_user: PublicUser):
if isinstance(current_user, AnonymousUser): if isinstance(current_user, AnonymousUser):
raise HTTPException(status_code=401, detail="Not authenticated") raise HTTPException(status_code=401, detail="Not authenticated")