Merge pull request #135 from learnhouse/fix/post-migration-bugs

Post Migration Bug Fixes
This commit is contained in:
Badr B 2023-12-27 13:45:04 +01:00 committed by GitHub
commit a7e2bda41e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 580 additions and 336 deletions

View file

@ -11,6 +11,8 @@ botocore
python-jose python-jose
passlib passlib
fastapi-jwt-auth fastapi-jwt-auth
pytest
httpx
faker faker
requests requests
pyyaml pyyaml

View file

@ -49,9 +49,9 @@ class CourseRead(CourseBase):
class FullCourseRead(CourseBase): class FullCourseRead(CourseBase):
id: int id: int
course_uuid: str course_uuid: Optional[str]
creation_date: str creation_date: Optional[str]
update_date: str update_date: Optional[str]
# Chapters, Activities # Chapters, Activities
chapters: List[ChapterRead] chapters: List[ChapterRead]
authors: List[UserRead] authors: List[UserRead]
@ -60,9 +60,9 @@ class FullCourseRead(CourseBase):
class FullCourseReadWithTrail(CourseBase): class FullCourseReadWithTrail(CourseBase):
id: int id: int
course_uuid: str course_uuid: Optional[str]
creation_date: str creation_date: Optional[str]
update_date: str update_date: Optional[str]
org_id: int = Field(default=None, foreign_key="organization.id") org_id: int = Field(default=None, foreign_key="organization.id")
authors: List[UserRead] authors: List[UserRead]
# Chapters, Activities # Chapters, Activities

View file

@ -47,10 +47,10 @@ class TrailRunRead(BaseModel):
org_id: int = Field(default=None, foreign_key="organization.id") org_id: int = Field(default=None, foreign_key="organization.id")
user_id: int = Field(default=None, foreign_key="user.id") user_id: int = Field(default=None, foreign_key="user.id")
# course object # course object
course: dict course: Optional[dict]
# timestamps # timestamps
creation_date: str creation_date: Optional[str]
update_date: str update_date: Optional[str]
# number of activities in course # number of activities in course
course_total_steps: int course_total_steps: int
steps: list[TrailStep] steps: list[TrailStep]

View file

@ -23,11 +23,11 @@ class TrailCreate(TrailBase):
# trick because Lists are not supported in SQLModel (runs: list[TrailRun] ) # trick because Lists are not supported in SQLModel (runs: list[TrailRun] )
class TrailRead(BaseModel): class TrailRead(BaseModel):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
trail_uuid: str trail_uuid: Optional[str]
org_id: int = Field(default=None, foreign_key="organization.id") org_id: int = Field(default=None, foreign_key="organization.id")
user_id: int = Field(default=None, foreign_key="user.id") user_id: int = Field(default=None, foreign_key="user.id")
creation_date: str creation_date: Optional[str]
update_date: str update_date: Optional[str]
runs: list[TrailRunRead] runs: list[TrailRunRead]
class Config: class Config:

View file

@ -1,6 +1,10 @@
from typing import Optional from typing import Optional
from pydantic import BaseModel
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from src.db.roles import RoleRead
from src.db.organizations import OrganizationRead
class UserBase(SQLModel): class UserBase(SQLModel):
username: str username: str
@ -33,14 +37,27 @@ class UserRead(UserBase):
id: int id: int
user_uuid: str user_uuid: str
class PublicUser(UserRead): class PublicUser(UserRead):
pass pass
class UserRoleWithOrg(BaseModel):
role: RoleRead
org: OrganizationRead
class UserSession(BaseModel):
user: UserRead
roles: list[UserRoleWithOrg]
class AnonymousUser(SQLModel): class AnonymousUser(SQLModel):
id: int = 0 id: int = 0
user_uuid: str = "user_anonymous" user_uuid: str = "user_anonymous"
username: str = "anonymous" username: str = "anonymous"
class User(UserBase, table=True): class User(UserBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
password: str = "" password: str = ""

View file

@ -9,6 +9,7 @@ from src.db.users import (
User, User,
UserCreate, UserCreate,
UserRead, UserRead,
UserSession,
UserUpdate, UserUpdate,
UserUpdatePassword, UserUpdatePassword,
) )
@ -17,6 +18,7 @@ from src.services.users.users import (
create_user, create_user,
create_user_without_org, create_user_without_org,
delete_user_by_id, delete_user_by_id,
get_user_session,
read_user_by_id, read_user_by_id,
read_user_by_uuid, read_user_by_uuid,
update_user, update_user,
@ -35,6 +37,18 @@ async def api_get_current_user(current_user: User = Depends(get_current_user)):
return current_user.dict() return current_user.dict()
@router.get("/session")
async def api_get_current_user_session(
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> UserSession:
"""
Get current user
"""
return await get_user_session(request, db_session, current_user)
@router.get("/authorize/ressource/{ressource_uuid}/action/{action}") @router.get("/authorize/ressource/{ressource_uuid}/action/{action}")
async def api_get_authorization_status( async def api_get_authorization_status(
request: Request, request: Request,

View file

@ -21,6 +21,7 @@ async def authorization_verify_if_element_is_public(
# Verifies if the element is public # Verifies if the element is public
if element_nature == ("courses" or "collections") and action == "read": if element_nature == ("courses" or "collections") and action == "read":
if element_nature == "courses": if element_nature == "courses":
print("looking for course")
statement = select(Course).where( statement = select(Course).where(
Course.public is True, Course.course_uuid == element_uuid Course.public is True, Course.course_uuid == element_uuid
) )
@ -28,10 +29,7 @@ async def authorization_verify_if_element_is_public(
if course: if course:
return True return True
else: else:
raise HTTPException( return False
status_code=status.HTTP_403_FORBIDDEN,
detail="User rights (public content) : You don't have the right to perform this action",
)
if element_nature == "collections": if element_nature == "collections":
statement = select(Collection).where( statement = select(Collection).where(
@ -42,15 +40,9 @@ async def authorization_verify_if_element_is_public(
if collection: if collection:
return True return True
else: else:
raise HTTPException( return False
status_code=status.HTTP_403_FORBIDDEN,
detail="User rights (public content) : You don't have the right to perform this action",
)
else: else:
raise HTTPException( return False
status_code=status.HTTP_403_FORBIDDEN,
detail="User rights (public content) : You don't have the right to perform this action",
)
# Tested and working # Tested and working

View file

@ -3,6 +3,7 @@ from sqlmodel import Session, select
from src.db.chapters import Chapter from src.db.chapters import Chapter
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship, authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_element_is_public,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
) )
from src.db.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate from src.db.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate
@ -212,20 +213,33 @@ async def get_activities(
async def rbac_check( async def rbac_check(
request: Request, request: Request,
course_id: str, course_uuid: str,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"], action: Literal["create", "read", "update", "delete"],
db_session: Session, db_session: Session,
): ):
await authorization_verify_if_user_is_anon(current_user.id) if action == "read":
if current_user.id == 0: # Anonymous user
res = await authorization_verify_if_element_is_public(
request, course_uuid, action, db_session
)
print('res',res)
return res
else:
res = await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, course_uuid, db_session
)
return res
else:
await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_based_on_roles_and_authorship( await authorization_verify_based_on_roles_and_authorship(
request, request,
current_user.id, current_user.id,
action, action,
course_id, course_uuid,
db_session, db_session,
) )
## 🔒 RBAC Utils ## ## 🔒 RBAC Utils ##

View file

@ -5,6 +5,7 @@ from sqlmodel import Session, select
from src.db.users import AnonymousUser from src.db.users import AnonymousUser
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship, authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_element_is_public,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
) )
from src.db.course_chapters import CourseChapter from src.db.course_chapters import CourseChapter
@ -207,6 +208,10 @@ async def get_course_chapters(
page: int = 1, page: int = 1,
limit: int = 10, limit: int = 10,
) -> List[ChapterRead]: ) -> List[ChapterRead]:
statement = select(Course).where(Course.id == course_id)
course = db_session.exec(statement).first()
statement = ( statement = (
select(Chapter) select(Chapter)
.join(CourseChapter, Chapter.id == CourseChapter.chapter_id) .join(CourseChapter, Chapter.id == CourseChapter.chapter_id)
@ -220,7 +225,7 @@ async def get_course_chapters(
chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters] chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters]
# RBAC check # RBAC check
await rbac_check(request, "chapter_x", current_user, "read", db_session) await rbac_check(request, course.course_uuid, current_user, "read", db_session)
# Get activities for each chapter # Get activities for each chapter
for chapter in chapters: for chapter in chapters:
@ -532,20 +537,33 @@ async def reorder_chapters_and_activities(
async def rbac_check( async def rbac_check(
request: Request, request: Request,
course_id: str, course_uuid: str,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"], action: Literal["create", "read", "update", "delete"],
db_session: Session, db_session: Session,
): ):
await authorization_verify_if_user_is_anon(current_user.id) if action == "read":
if current_user.id == 0: # Anonymous user
res = await authorization_verify_if_element_is_public(
request, course_uuid, action, db_session
)
print('res',res)
return res
else:
res = await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, course_uuid, db_session
)
return res
else:
await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_based_on_roles_and_authorship( await authorization_verify_based_on_roles_and_authorship(
request, request,
current_user.id, current_user.id,
action, action,
course_id, course_uuid,
db_session, db_session,
) )
## 🔒 RBAC Utils ## ## 🔒 RBAC Utils ##

View file

@ -5,6 +5,7 @@ from sqlmodel import Session, select
from src.db.users import AnonymousUser from src.db.users import AnonymousUser
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship, authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_element_is_public,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
) )
from src.db.collections import ( from src.db.collections import (
@ -245,20 +246,34 @@ async def get_collections(
async def rbac_check( async def rbac_check(
request: Request, request: Request,
course_id: str, collection_uuid: str,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"], action: Literal["create", "read", "update", "delete"],
db_session: Session, db_session: Session,
): ):
await authorization_verify_if_user_is_anon(current_user.id) if action == "read":
if current_user.id == 0: # Anonymous user
res = await authorization_verify_if_element_is_public(
request, collection_uuid, action, db_session
)
print('res',res)
return res
else:
res = await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, collection_uuid, db_session
)
return res
else:
await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_based_on_roles_and_authorship( await authorization_verify_based_on_roles_and_authorship(
request, request,
current_user.id, current_user.id,
action, action,
course_id, collection_uuid,
db_session, db_session,
) )
## 🔒 RBAC Utils ## ## 🔒 RBAC Utils ##

View file

@ -96,11 +96,16 @@ async def get_course_meta(
chapters = await get_course_chapters(request, course.id, db_session, current_user) chapters = await get_course_chapters(request, course.id, db_session, current_user)
# Trail # Trail
trail = await get_user_trail_with_orgid( trail = None
request, current_user, course.org_id, db_session
) if isinstance(current_user, AnonymousUser):
trail = None
else:
trail = await get_user_trail_with_orgid(
request, current_user, course.org_id, db_session
)
trail = TrailRead.from_orm(trail)
trail = TrailRead.from_orm(trail)
return FullCourseReadWithTrail( return FullCourseReadWithTrail(
**course.dict(), **course.dict(),
@ -359,7 +364,6 @@ async def get_courses_orgslug(
## 🔒 RBAC Utils ## ## 🔒 RBAC Utils ##
async def rbac_check( async def rbac_check(
request: Request, request: Request,
course_uuid: str, course_uuid: str,
@ -369,13 +373,16 @@ async def rbac_check(
): ):
if action == "read": if action == "read":
if current_user.id == 0: # Anonymous user if current_user.id == 0: # Anonymous user
await authorization_verify_if_element_is_public( res = await authorization_verify_if_element_is_public(
request, course_uuid, action, db_session request, course_uuid, action, db_session
) )
print('res',res)
return res
else: else:
await authorization_verify_based_on_roles_and_authorship( res = await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, course_uuid, db_session request, current_user.id, action, course_uuid, db_session
) )
return res
else: else:
await authorization_verify_if_user_is_anon(current_user.id) await authorization_verify_if_user_is_anon(current_user.id)

View file

@ -93,7 +93,7 @@ async def update_install_instance(
# Install Default roles # Install Default roles
async def install_default_elements(request: Request, data: dict, db_session: Session): async def install_default_elements( data: dict, db_session: Session):
# remove all default roles # remove all default roles
statement = select(Role).where(Role.role_type == RoleTypeEnum.TYPE_GLOBAL) statement = select(Role).where(Role.role_type == RoleTypeEnum.TYPE_GLOBAL)
roles = db_session.exec(statement).all() roles = db_session.exec(statement).all()
@ -279,7 +279,7 @@ async def install_default_elements(request: Request, data: dict, db_session: Ses
# Organization creation # Organization creation
async def install_create_organization( async def install_create_organization(
request: Request, org_object: OrganizationCreate, db_session: Session org_object: OrganizationCreate, db_session: Session
): ):
org = Organization.from_orm(org_object) org = Organization.from_orm(org_object)
@ -296,7 +296,7 @@ async def install_create_organization(
async def install_create_organization_user( async def install_create_organization_user(
request: Request, user_object: UserCreate, org_slug: str, db_session: Session user_object: UserCreate, org_slug: str, db_session: Session
): ):
user = User.from_orm(user_object) user = User.from_orm(user_object)

View file

@ -8,7 +8,7 @@ from src.db.courses import Course
from src.db.trail_runs import TrailRun, TrailRunRead from src.db.trail_runs import TrailRun, TrailRunRead
from src.db.trail_steps import TrailStep from src.db.trail_steps import TrailStep
from src.db.trails import Trail, TrailCreate, TrailRead from src.db.trails import Trail, TrailCreate, TrailRead
from src.db.users import PublicUser from src.db.users import AnonymousUser, PublicUser
async def create_user_trail( async def create_user_trail(
@ -17,7 +17,7 @@ async def create_user_trail(
trail_object: TrailCreate, trail_object: TrailCreate,
db_session: Session, db_session: Session,
) -> Trail: ) -> Trail:
statement = select(Trail).where(Trail.org_id == trail_object.org_id) statement = select(Trail).where(Trail.org_id == trail_object.org_id, Trail.user_id == user.id)
trail = db_session.exec(statement).first() trail = db_session.exec(statement).first()
if trail: if trail:
@ -103,7 +103,7 @@ async def check_trail_presence(
user: PublicUser, user: PublicUser,
db_session: Session, db_session: Session,
): ):
statement = select(Trail).where(Trail.org_id == org_id, Trail.user_id == user.id) statement = select(Trail).where(Trail.org_id == org_id, Trail.user_id == user_id)
trail = db_session.exec(statement).first() trail = db_session.exec(statement).first()
if not trail: if not trail:
@ -122,9 +122,15 @@ async def check_trail_presence(
async def get_user_trail_with_orgid( async def get_user_trail_with_orgid(
request: Request, user: PublicUser, org_id: int, db_session: Session request: Request, user: PublicUser | AnonymousUser, org_id: int, db_session: Session
) -> TrailRead: ) -> TrailRead:
if isinstance(user, AnonymousUser):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Anonymous users cannot access this endpoint",
)
trail = await check_trail_presence( trail = await check_trail_presence(
org_id=org_id, org_id=org_id,
user_id=user.id, user_id=user.id,

View file

@ -3,17 +3,20 @@ from typing import Literal
from uuid import uuid4 from uuid import uuid4
from fastapi import HTTPException, Request, status from fastapi import HTTPException, Request, status
from sqlmodel import Session, select from sqlmodel import Session, select
from src.db.roles import Role, RoleRead
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship, authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
) )
from src.db.organizations import Organization from src.db.organizations import Organization, OrganizationRead
from src.db.users import ( from src.db.users import (
AnonymousUser, AnonymousUser,
PublicUser, PublicUser,
User, User,
UserCreate, UserCreate,
UserRead, UserRead,
UserRoleWithOrg,
UserSession,
UserUpdate, UserUpdate,
UserUpdatePassword, UserUpdatePassword,
) )
@ -279,6 +282,57 @@ async def read_user_by_uuid(
return user return user
async def get_user_session(
request: Request,
db_session: Session,
current_user: PublicUser | AnonymousUser,
) -> UserSession:
# Get user
statement = select(User).where(User.user_uuid == current_user.user_uuid)
user = db_session.exec(statement).first()
if not user:
raise HTTPException(
status_code=400,
detail="User does not exist",
)
user = UserRead.from_orm(user)
# Get roles and orgs
statement = (
select(UserOrganization)
.where(UserOrganization.user_id == user.id)
.join(Organization)
)
user_organizations = db_session.exec(statement).all()
roles = []
for user_organization in user_organizations:
role_statement = select(Role).where(Role.id == user_organization.role_id)
role = db_session.exec(role_statement).first()
org_statement = select(Organization).where(
Organization.id == user_organization.org_id
)
org = db_session.exec(org_statement).first()
roles.append(
UserRoleWithOrg(
role=RoleRead.from_orm(role),
org=OrganizationRead.from_orm(org),
)
)
user_session = UserSession(
user=user,
roles=roles,
)
return user_session
async def authorize_user_action( async def authorize_user_action(
request: Request, request: Request,
db_session: Session, db_session: Session,

View file

@ -0,0 +1,50 @@
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.pool import StaticPool
from sqlmodel import SQLModel, Session
from src.tests.utils.init_data_for_tests import create_initial_data_for_tests
from src.core.events.database import get_db_session
import pytest
import asyncio
from app import app
client = TestClient(app)
# TODO : fix this later https://stackoverflow.com/questions/10253826/path-issue-with-pytest-importerror-no-module-named
@pytest.fixture(name="session", scope="session")
def session_fixture():
engine = create_engine(
"sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool
)
SQLModel.metadata.create_all(engine)
with Session(engine) as session:
yield session
@pytest.fixture(name="client")
def client_fixture(session: Session):
def get_session_override():
return session
app.dependency_overrides[get_db_session] = get_session_override
client = TestClient(app)
yield client
app.dependency_overrides.clear()
@pytest.fixture(scope="session", autouse=True)
def execute_before_all_tests(session: Session):
# This function will run once before all tests.
asyncio.run(create_initial_data_for_tests(session))
def test_create_default_elements(client: TestClient, session: Session):
response = client.get(
"/api/v1/orgs/slug/wayne",
)
assert response.status_code == 200

View file

View file

View file

@ -0,0 +1,57 @@
from sqlmodel import Session, select
from src.db.user_organizations import UserOrganization
from src.db.organizations import OrganizationCreate
from src.db.users import User, UserCreate
from src.services.install.install import (
install_create_organization,
install_create_organization_user,
install_default_elements,
)
async def create_initial_data_for_tests(db_session: Session):
# Install default elements
await install_default_elements({}, db_session)
# Initiate test Organization
test_org = OrganizationCreate(
name="Wayne Enterprises",
description=None,
slug="wayne",
email="hello@wayne.dev",
logo_image=None,
)
# Create test organization
await install_create_organization(test_org, db_session)
users = [
UserCreate(
username="batman",
first_name="Bruce",
last_name="Wayne",
email="bruce@wayne.com",
password="imbatman",
),
UserCreate(
username="robin",
first_name="Richard John",
last_name="Grayson",
email="robin@wayne.com",
password="secret",
),
]
# Create 2 users in that Organization
for user in users:
await install_create_organization_user(user, "wayne", db_session)
# Make robin a normal user
statement = select(UserOrganization).join(User).where(User.username == "robin")
user_org = db_session.exec(statement).first()
user_org.role_id = 3 # type: ignore
db_session.add(user_org)
db_session.commit()
return True

View file

@ -1,5 +1,4 @@
import { default as React, } from "react"; import { default as React, } from "react";
import AuthProvider from "@components/Security/AuthProviderDepreceated";
import EditorWrapper from "@components/Objects/Editor/EditorWrapper"; import EditorWrapper from "@components/Objects/Editor/EditorWrapper";
import { getCourseMetadataWithAuthHeader } from "@services/courses/courses"; import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
@ -7,6 +6,7 @@ import { Metadata } from "next";
import { getActivityWithAuthHeader } from "@services/courses/activities"; import { getActivityWithAuthHeader } from "@services/courses/activities";
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth"; import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
import { getOrganizationContextInfo, getOrganizationContextInfoWithId } from "@services/organizations/orgs"; import { getOrganizationContextInfo, getOrganizationContextInfoWithId } from "@services/organizations/orgs";
import SessionProvider from "@components/Contexts/SessionContext";
type MetadataProps = { type MetadataProps = {
params: { orgslug: string, courseid: string, activityid: string }; params: { orgslug: string, courseid: string, activityid: string };
@ -35,13 +35,13 @@ const EditActivity = async (params: any) => {
const courseInfo = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) const courseInfo = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
const activity = await getActivityWithAuthHeader(activityuuid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null) const activity = await getActivityWithAuthHeader(activityuuid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null)
const org = await getOrganizationContextInfoWithId(courseInfo.org_id, { revalidate: 1800, tags: ['organizations'] }); const org = await getOrganizationContextInfoWithId(courseInfo.org_id, { revalidate: 1800, tags: ['organizations'] });
console.log('courseInfo', courseInfo ) console.log('courseInfo', courseInfo)
return ( return (
<div> <div>
<AuthProvider> <SessionProvider>
<EditorWrapper org={org} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper> <EditorWrapper org={org} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>
</AuthProvider> </SessionProvider>
</div> </div>
); );
} }

View file

@ -5,7 +5,6 @@ import { deleteOrganizationFromBackend } from "@services/organizations/orgs";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
import { swrFetcher } from "@services/utils/ts/requests"; import { swrFetcher } from "@services/utils/ts/requests";
import { getAPIUrl, getUriWithOrg } from "@services/config/config"; import { getAPIUrl, getUriWithOrg } from "@services/config/config";
import AuthProvider from "@components/Security/AuthProviderDepreceated";
const Organizations = () => { const Organizations = () => {
const { data: organizations, error } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, swrFetcher) const { data: organizations, error } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, swrFetcher)
@ -17,7 +16,6 @@ const Organizations = () => {
return ( return (
<> <>
<AuthProvider />
<div className="font-bold text-lg"> <div className="font-bold text-lg">
Your Organizations{" "} Your Organizations{" "}
<Link href="/organizations/new"> <Link href="/organizations/new">

View file

@ -57,7 +57,7 @@ const CollectionsPage = async (params: any) => {
<div className="flex justify-between" > <div className="flex justify-between" >
<TypeOfContentTitle title="Collections" type="col" /> <TypeOfContentTitle title="Collections" type="col" />
<AuthenticatedClientElement <AuthenticatedClientElement
ressourceType="collection" ressourceType="collections"
action="create" action="create"
checkMethod='roles' orgId={org_id}> checkMethod='roles' orgId={org_id}>
<Link className="flex justify-center" href={getUriWithOrg(orgslug, "/collections/new")}> <Link className="flex justify-center" href={getUriWithOrg(orgslug, "/collections/new")}>
@ -85,7 +85,7 @@ const CollectionsPage = async (params: any) => {
<p className="text-lg text-gray-400">Create a collection to group courses together</p> <p className="text-lg text-gray-400">Create a collection to group courses together</p>
</div> </div>
<AuthenticatedClientElement checkMethod='roles' <AuthenticatedClientElement checkMethod='roles'
ressourceType="collection" ressourceType="collections"
action="create" action="create"
orgId={org_id}> orgId={org_id}>
<Link href={getUriWithOrg(orgslug, "/collections/new")}> <Link href={getUriWithOrg(orgslug, "/collections/new")}>

View file

@ -42,9 +42,9 @@ const CourseClient = (props: any) => {
} }
function isCourseStarted() { function isCourseStarted() {
const runs = course.trail.runs; const runs = course.trail?.runs;
// checks if one of the obejcts in the array has the property "STATUS_IN_PROGRESS" if (!runs) return false;
return runs.some((run: any) => run.status === "STATUS_IN_PROGRESS"); return runs.some((run: any) => run.status === "STATUS_IN_PROGRESS" && run.course_id === course.id);
} }
async function quitCourse() { async function quitCourse() {
@ -193,11 +193,10 @@ const CourseClient = (props: any) => {
</div> </div>
<div className="-space-y-2 "> <div className="-space-y-2 ">
<div className="text-[12px] text-neutral-400 font-semibold">Author</div> <div className="text-[12px] text-neutral-400 font-semibold">Author</div>
<div className="text-xl font-bold text-neutral-800">{course.authors[0].first_name} {course.authors[0].last_name} { (course.authors[0].first_name && course.authors[0].last_name) ? course.authors[0].first_name + ' ' + course.authors[0].last_name : course.authors[0].username }</div> <div className="text-xl font-bold text-neutral-800">{course.authors[0].first_name} {course.authors[0].last_name} {(course.authors[0].first_name && course.authors[0].last_name) ? course.authors[0].first_name + ' ' + course.authors[0].last_name : course.authors[0].username}</div>
</div> </div>
</div> </div>
} }
{console.log(course)}
{isCourseStarted() ? ( {isCourseStarted() ? (
<button className="py-2 px-5 mx-auto rounded-xl text-white font-bold h-12 w-[200px] drop-shadow-md bg-red-600 hover:bg-red-700 hover:cursor-pointer" onClick={quitCourse}> <button className="py-2 px-5 mx-auto rounded-xl text-white font-bold h-12 w-[200px] drop-shadow-md bg-red-600 hover:bg-red-700 hover:cursor-pointer" onClick={quitCourse}>

View file

@ -34,7 +34,7 @@ function Courses(props: CourseProps) {
<TypeOfContentTitle title="Courses" type="cou" /> <TypeOfContentTitle title="Courses" type="cou" />
<AuthenticatedClientElement checkMethod='roles' <AuthenticatedClientElement checkMethod='roles'
action='create' action='create'
ressourceType='course' ressourceType='courses'
orgId={props.org_id}> orgId={props.org_id}>
<Modal <Modal
isDialogOpen={newCourseModal} isDialogOpen={newCourseModal}
@ -78,7 +78,7 @@ function Courses(props: CourseProps) {
</div> </div>
<AuthenticatedClientElement <AuthenticatedClientElement
action='create' action='create'
ressourceType='course' ressourceType='courses'
checkMethod='roles' orgId={props.org_id}> checkMethod='roles' orgId={props.org_id}>
<Modal <Modal
isDialogOpen={newCourseModal} isDialogOpen={newCourseModal}

View file

@ -1,14 +1,14 @@
import "@styles/globals.css"; import "@styles/globals.css";
import { Menu } from "@components/Objects/Menu/Menu"; import { Menu } from "@components/Objects/Menu/Menu";
import AuthProvider from "@components/Security/AuthProviderDepreceated"; import SessionProvider from "@components/Contexts/SessionContext";
export default function RootLayout({ children, params }: { children: React.ReactNode , params :any}) { export default function RootLayout({ children, params }: { children: React.ReactNode , params :any}) {
return ( return (
<> <>
<AuthProvider> <SessionProvider>
<Menu orgslug={params?.orgslug}></Menu> <Menu orgslug={params?.orgslug}></Menu>
{children} {children}
</AuthProvider> </SessionProvider>
</> </>
); );
} }

View file

@ -69,7 +69,7 @@ const OrgHomePage = async (params: any) => {
</div> </div>
<AuthenticatedClientElement <AuthenticatedClientElement
checkMethod='roles' checkMethod='roles'
ressourceType='collection' ressourceType='collections'
action='create' action='create'
orgId={org_id}> orgId={org_id}>
<Link href={getUriWithOrg(orgslug, "/collections/new")}> <Link href={getUriWithOrg(orgslug, "/collections/new")}>
@ -110,7 +110,7 @@ const OrgHomePage = async (params: any) => {
<TypeOfContentTitle title="Courses" type="cou" /> <TypeOfContentTitle title="Courses" type="cou" />
</div> </div>
<AuthenticatedClientElement <AuthenticatedClientElement
ressourceType='course' ressourceType='courses'
action='create' action='create'
checkMethod='roles' checkMethod='roles'
orgId={org_id}> orgId={org_id}>

View file

@ -37,7 +37,7 @@ function CoursesHome(params: CourseProps) {
<div className='pt-3 flex font-bold text-4xl'>Courses</div> <div className='pt-3 flex font-bold text-4xl'>Courses</div>
<AuthenticatedClientElement checkMethod='roles' <AuthenticatedClientElement checkMethod='roles'
action='create' action='create'
ressourceType='course' ressourceType='courses'
orgId={params.org_id}> orgId={params.org_id}>
<Modal <Modal
isDialogOpen={newCourseModal} isDialogOpen={newCourseModal}
@ -80,7 +80,7 @@ function CoursesHome(params: CourseProps) {
</div> </div>
<AuthenticatedClientElement <AuthenticatedClientElement
action='create' action='create'
ressourceType='course' ressourceType='courses'
checkMethod='roles' orgId={params.org_id}> checkMethod='roles' orgId={params.org_id}>
<Modal <Modal
isDialogOpen={newCourseModal} isDialogOpen={newCourseModal}

View file

@ -1,18 +1,18 @@
import SessionProvider from '@components/Contexts/SessionContext'
import LeftMenu from '@components/Dashboard/UI/LeftMenu' import LeftMenu from '@components/Dashboard/UI/LeftMenu'
import AuthProvider from '@components/Security/AuthProviderDepreceated'
import React from 'react' import React from 'react'
function DashboardLayout({ children, params }: { children: React.ReactNode, params: any }) { function DashboardLayout({ children, params }: { children: React.ReactNode, params: any }) {
return ( return (
<> <>
<AuthProvider> <SessionProvider>
<div className='flex'> <div className='flex'>
<LeftMenu/> <LeftMenu/>
<div className='flex w-full'> <div className='flex w-full'>
{children} {children}
</div> </div>
</div> </div>
</AuthProvider> </SessionProvider>
</> </>
) )
} }

View file

@ -7,7 +7,7 @@ import Link from 'next/link';
import { getUriWithOrg } from '@services/config/config'; import { getUriWithOrg } from '@services/config/config';
import { Info, Lock } from 'lucide-react'; import { Info, Lock } from 'lucide-react';
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'; import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs';
import { useAuth } from '@components/Security/AuthContext'; import { useSession } from '@components/Contexts/SessionContext';
export type SettingsParams = { export type SettingsParams = {
subpage: string subpage: string
@ -15,17 +15,17 @@ export type SettingsParams = {
} }
function SettingsPage({ params }: { params: SettingsParams }) { function SettingsPage({ params }: { params: SettingsParams }) {
const auth = useAuth() as any; const session = useSession() as any;
useEffect(() => { useEffect(() => {
} }
, [auth]) , [session])
return ( return (
<div className='h-full w-full bg-[#f8f8f8]'> <div className='h-full w-full bg-[#f8f8f8]'>
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'> <div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'>
<BreadCrumbs type='user' last_breadcrumb={auth?.user?.username} ></BreadCrumbs> <BreadCrumbs type='user' last_breadcrumb={session?.user?.username} ></BreadCrumbs>
<div className='my-2 tracking-tighter'> <div className='my-2 tracking-tighter'>
<div className='w-100 flex justify-between'> <div className='w-100 flex justify-between'>
<div className='pt-3 flex font-bold text-4xl'>Account Settings</div> <div className='pt-3 flex font-bold text-4xl'>Account Settings</div>

View file

@ -1,20 +1,17 @@
'use client'; 'use client';
import { OrgProvider } from "@components/Contexts/OrgContext"; import { OrgProvider } from "@components/Contexts/OrgContext";
import AuthProvider from "@components/Security/AuthContext"; import SessionProvider from "@components/Contexts/SessionContext";
import { getAPIUrl } from "@services/config/config";
import { swrFetcher } from "@services/utils/ts/requests";
import "@styles/globals.css"; import "@styles/globals.css";
import useSWR from "swr";
export default function RootLayout({ children, params }: { children: React.ReactNode, params: any }) { export default function RootLayout({ children, params }: { children: React.ReactNode, params: any }) {
return ( return (
<div> <div>
<AuthProvider orgslug={params.orgslug}> <OrgProvider orgslug={params.orgslug}>
<OrgProvider orgslug={params.orgslug}> <SessionProvider>
{children} {children}
</OrgProvider> </SessionProvider>
</AuthProvider> </OrgProvider>
</div> </div>
); );
} }

View file

@ -7,7 +7,7 @@ import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, For
import Image from 'next/image'; import Image from 'next/image';
import * as Form from '@radix-ui/react-form'; import * as Form from '@radix-ui/react-form';
import { getOrgLogoMediaDirectory } from '@services/media/media'; import { getOrgLogoMediaDirectory } from '@services/media/media';
import { AlertTriangle } from 'lucide-react'; import { AlertTriangle, Check, User } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { signup } from '@services/auth/auth'; import { signup } from '@services/auth/auth';
import { getUriWithOrg } from '@services/config/config'; import { getUriWithOrg } from '@services/config/config';
@ -44,10 +44,6 @@ const validate = (values: any) => {
errors.username = 'Username must be at least 4 characters'; errors.username = 'Username must be at least 4 characters';
} }
if (!values.full_name) {
errors.full_name = 'Required';
}
if (!values.bio) { if (!values.bio) {
errors.bio = 'Required'; errors.bio = 'Required';
} }
@ -61,6 +57,7 @@ function SignUpClient(props: SignUpClientProps) {
const [isSubmitting, setIsSubmitting] = React.useState(false); const [isSubmitting, setIsSubmitting] = React.useState(false);
const router = useRouter(); const router = useRouter();
const [error, setError] = React.useState(''); const [error, setError] = React.useState('');
const [message, setMessage] = React.useState('');
const formik = useFormik({ const formik = useFormik({
initialValues: { initialValues: {
org_slug: props.org?.slug, org_slug: props.org?.slug,
@ -68,7 +65,8 @@ function SignUpClient(props: SignUpClientProps) {
password: '', password: '',
username: '', username: '',
bio: '', bio: '',
full_name: '', first_name: '',
last_name: '',
}, },
validate, validate,
onSubmit: async values => { onSubmit: async values => {
@ -76,7 +74,8 @@ function SignUpClient(props: SignUpClientProps) {
let res = await signup(values); let res = await signup(values);
let message = await res.json(); let message = await res.json();
if (res.status == 200) { if (res.status == 200) {
router.push(`/`); //router.push(`/login`);
setMessage('Your account was successfully created')
setIsSubmitting(false); setIsSubmitting(false);
} }
else if (res.status == 401 || res.status == 400 || res.status == 404 || res.status == 409) { else if (res.status == 401 || res.status == 400 || res.status == 404 || res.status == 409) {
@ -126,6 +125,16 @@ function SignUpClient(props: SignUpClientProps) {
<div className="font-bold text-sm">{error}</div> <div className="font-bold text-sm">{error}</div>
</div> </div>
)} )}
{message && (
<div className="flex flex-col space-y-4 justify-center bg-green-200 rounded-md text-green-950 space-x-2 items-center p-4 transition-all shadow-sm">
<div className='flex space-x-2'>
<Check size={18} />
<div className="font-bold text-sm">{message}</div>
</div>
<hr className='border-green-900/20 800 w-40 border' />
<Link className='flex space-x-2 items-center' href={'/login'}><User size={14} /> <div>Login </div></Link>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}> <FormLayout onSubmit={formik.handleSubmit}>
<FormField name="email"> <FormField name="email">
<FormLabelAndMessage label='Email' message={formik.errors.email} /> <FormLabelAndMessage label='Email' message={formik.errors.email} />
@ -150,16 +159,6 @@ function SignUpClient(props: SignUpClientProps) {
</Form.Control> </Form.Control>
</FormField> </FormField>
{/* for full name */}
<FormField name="full_name">
<FormLabelAndMessage label='Full Name' message={formik.errors.full_name} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.full_name} type="text" required />
</Form.Control>
</FormField>
{/* for bio */} {/* for bio */}
<FormField name="bio"> <FormField name="bio">
<FormLabelAndMessage label='Bio' message={formik.errors.bio} /> <FormLabelAndMessage label='Bio' message={formik.errors.bio} />

View file

@ -0,0 +1,65 @@
'use client';
import { getNewAccessTokenUsingRefreshToken, getUserSession } from '@services/auth/auth';
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import React, { useContext, createContext, useEffect } from 'react'
import { useOrg } from './OrgContext';
export const SessionContext = createContext({}) as any;
const PATHS_THAT_REQUIRE_AUTH = ['/dash'];
type Session = {
access_token: string;
user: any;
roles: any;
isLoading: boolean;
isAuthenticated: boolean;
}
function SessionProvider({ children }: { children: React.ReactNode }) {
const [session, setSession] = React.useState<Session>({ access_token: "", user: {}, roles: {}, isLoading: true, isAuthenticated: false });
const org = useOrg() as any;
const pathname = usePathname()
const router = useRouter()
async function getNewAccessTokenUsingRefreshTokenUI() {
let data = await getNewAccessTokenUsingRefreshToken();
return data.access_token;
}
async function checkSession() {
// Get new access token using refresh token
const access_token = await getNewAccessTokenUsingRefreshTokenUI();
if (access_token) {
// Get user session info
const user_session = await getUserSession(access_token);
// Set session
setSession({ access_token: access_token, user: user_session.user, roles: user_session.roles, isLoading: false, isAuthenticated: true });
}
}
useEffect(() => {
// Check session
checkSession();
}, [])
return (
<SessionContext.Provider value={session}>
{children}
</SessionContext.Provider>
)
}
export function useSession() {
return useContext(SessionContext);
}
export default SessionProvider

View file

@ -1,26 +1,37 @@
'use client'; 'use client';
import { useOrg } from '@components/Contexts/OrgContext'; import { useOrg } from '@components/Contexts/OrgContext';
import { useAuth } from '@components/Security/AuthContext'; import { useSession } from '@components/Contexts/SessionContext';
import ToolTip from '@components/StyledElements/Tooltip/Tooltip' import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
import LearnHouseDashboardLogo from '@public/dashLogo.png'; import LearnHouseDashboardLogo from '@public/dashLogo.png';
import { logout } from '@services/auth/auth';
import Avvvatars from 'avvvatars-react'; import Avvvatars from 'avvvatars-react';
import { ArrowLeft, Book, BookCopy, Home, School, Settings } from 'lucide-react' import { ArrowLeft, Book, BookCopy, Home, LogOut, School, Settings } from 'lucide-react'
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/navigation';
import React, { use, useEffect } from 'react' import React, { use, useEffect } from 'react'
function LeftMenu() { function LeftMenu() {
const org = useOrg() as any; const org = useOrg() as any;
const auth = useAuth() as any; const session = useSession() as any;
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
const route = useRouter();
function waitForEverythingToLoad() { function waitForEverythingToLoad() {
if (org && auth) { if (org && session) {
return true; return true;
} }
return false; return false;
} }
async function logOutUI() {
const res = await logout();
if (res) {
route.push('/login');
}
}
useEffect(() => { useEffect(() => {
if (waitForEverythingToLoad()) { if (waitForEverythingToLoad()) {
setLoading(false); setLoading(false);
@ -59,20 +70,23 @@ function LeftMenu() {
</ToolTip> </ToolTip>
</div> </div>
<div className='flex flex-col mx-auto pb-7 space-y-2'> <div className='flex flex-col mx-auto pb-7 space-y-2'>
<ToolTip content={auth.user.username + "'s Settings"} slateBlack sideOffset={8} side='right' >
<Link href={'/dash/user/settings/general'} className='py-3'> <div className="flex items-center flex-col space-y-2">
<Settings className='mx-auto text-neutral-400 cursor-pointer' size={18} /> <ToolTip content={session.user.username} slateBlack sideOffset={8} side='right' >
</Link>
</ToolTip>
<div className="flex items-center flex-col space-y-4">
<ToolTip content={auth.user.username} slateBlack sideOffset={8} side='right' >
<div className="mx-auto shadow-lg"> <div className="mx-auto shadow-lg">
<Avvvatars radius={3} border borderColor='white' borderSize={3} size={35} value={auth.user.user_uuid} style="shape" /> <Avvvatars radius={3} border borderColor='white' borderSize={3} size={35} value={session.user.user_uuid} style="shape" />
</div> </div>
</ToolTip> </ToolTip>
<ToolTip content={'Learnhouse Version'} slateBlack sideOffset={8} side='right' > <div className='flex items-center flex-col space-y-1'>
<div className='py-1 px-3 bg-white/10 opacity-40 rounded-full text-[10px] uppercase justify-center text-center'>alpha</div> <ToolTip content={session.user.username + "'s Settings"} slateBlack sideOffset={8} side='right' >
</ToolTip> <Link href={'/dash/user/settings/general'} className='py-3'>
<Settings className='mx-auto text-neutral-400 cursor-pointer' size={18} />
</Link>
</ToolTip>
<ToolTip content={'Logout'} slateBlack sideOffset={8} side='right' >
<LogOut onClick={() => logOutUI()} className='mx-auto text-neutral-400 cursor-pointer' size={14} />
</ToolTip>
</div>
</div> </div>
</div> </div>

View file

@ -1,33 +1,33 @@
import { useAuth } from '@components/Security/AuthContext';
import { updateProfile } from '@services/settings/profile'; import { updateProfile } from '@services/settings/profile';
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { Formik, Form, Field, ErrorMessage } from 'formik'; import { Formik, Form, Field, ErrorMessage } from 'formik';
import { useSession } from '@components/Contexts/SessionContext';
function UserEditGeneral() { function UserEditGeneral() {
const auth = useAuth() as any; const session = useSession() as any;
useEffect(() => { useEffect(() => {
} }
, [auth, auth.user]) , [session, session.user])
return ( return (
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'> <div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
{auth.user && ( {session.user && (
<Formik <Formik
enableReinitialize enableReinitialize
initialValues={{ initialValues={{
username: auth.user.username, username: session.user.username,
first_name: auth.user.first_name, first_name: session.user.first_name,
last_name: auth.user.last_name, last_name: session.user.last_name,
email: auth.user.email, email: session.user.email,
bio: auth.user.bio, bio: session.user.bio,
}} }}
onSubmit={(values, { setSubmitting }) => { onSubmit={(values, { setSubmitting }) => {
setTimeout(() => { setTimeout(() => {
setSubmitting(false); setSubmitting(false);
updateProfile(values,auth.user.id) updateProfile(values,session.user.id)
}, 400); }, 400);
}} }}
> >

View file

@ -1,19 +1,19 @@
import { useAuth } from '@components/Security/AuthContext'; import { useSession } from '@components/Contexts/SessionContext';
import { updatePassword } from '@services/settings/password'; import { updatePassword } from '@services/settings/password';
import { Formik, Form, Field, ErrorMessage } from 'formik'; import { Formik, Form, Field, ErrorMessage } from 'formik';
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
function UserEditPassword() { function UserEditPassword() {
const auth = useAuth() as any; const session = useSession() as any;
const updatePasswordUI = async (values: any) => { const updatePasswordUI = async (values: any) => {
let user_id = auth.userInfo.user_object.user_id; let user_id = session.user.user_id;
await updatePassword(user_id, values) await updatePassword(user_id, values)
} }
useEffect(() => { useEffect(() => {
} }
, [auth]) , [session])
return ( return (

View file

@ -2,7 +2,6 @@
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";
import { AuthContext } from "../../Security/AuthProviderDepreceated";
import learnhouseIcon from "public/learnhouse_icon.png"; import learnhouseIcon from "public/learnhouse_icon.png";
import { ToolbarButtons } from "./Toolbar/ToolbarButtons"; import { ToolbarButtons } from "./Toolbar/ToolbarButtons";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
@ -38,6 +37,7 @@ import python from 'highlight.js/lib/languages/python'
import java from 'highlight.js/lib/languages/java' import java from 'highlight.js/lib/languages/java'
import { CourseProvider } from "@components/Contexts/CourseContext"; import { CourseProvider } from "@components/Contexts/CourseContext";
import { OrgProvider } from "@components/Contexts/OrgContext"; import { OrgProvider } from "@components/Contexts/OrgContext";
import { useSession } from "@components/Contexts/SessionContext";
interface Editor { interface Editor {
@ -51,7 +51,7 @@ interface Editor {
} }
function Editor(props: Editor) { function Editor(props: Editor) {
const auth: any = React.useContext(AuthContext); const session = useSession() as any;
// remove course_ from course_uuid // remove course_ from course_uuid
const course_uuid = props.course.course_uuid.substring(7); const course_uuid = props.course.course_uuid.substring(7);
@ -164,8 +164,8 @@ function Editor(props: Editor) {
</EditorDocSection> </EditorDocSection>
<EditorUsersSection> <EditorUsersSection>
<EditorUserProfileWrapper> <EditorUserProfileWrapper>
{!auth.isAuthenticated && <span>Loading</span>} {!session.isAuthenticated && <span>Loading</span>}
{auth.isAuthenticated && <Avvvatars value={auth.userInfo.user_uuid} style="shape" />} {session.isAuthenticated && <Avvvatars value={session.user.user_uuid} style="shape" />}
</EditorUserProfileWrapper> </EditorUserProfileWrapper>
<DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey", opacity: '0.5' }} /> <DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey", opacity: '0.5' }} />
<EditorLeftOptionsSection className="space-x-2 pl-2 pr-3"> <EditorLeftOptionsSection className="space-x-2 pl-2 pr-3">

View file

@ -4,11 +4,11 @@ import * as Form from '@radix-ui/react-form'
import React, { useState } from "react"; import React, { useState } from "react";
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { CheckCircleIcon } from "lucide-react"; import { CheckCircleIcon } from "lucide-react";
import { AuthContext } from "@components/Security/AuthProviderDepreceated";
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
import { useSession } from "@components/Contexts/SessionContext";
export const FeedbackModal = (user: any) => { export const FeedbackModal = (user: any) => {
const auth: any = React.useContext(AuthContext); const session = useSession() as any;
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [view, setView] = useState<"feedbackForm" | "success">("feedbackForm") const [view, setView] = useState<"feedbackForm" | "success">("feedbackForm")
@ -19,7 +19,7 @@ export const FeedbackModal = (user: any) => {
e.preventDefault(); e.preventDefault();
setIsSubmitting(true); setIsSubmitting(true);
const user = auth.userInfo.user_object ? auth.userInfo.user_object : null; const user = session.user ? session.user : null;
const eventId = Sentry.captureMessage(`Feedback from ${user ? user.email : 'Anonymous'} - ${feedbackMessage}`); const eventId = Sentry.captureMessage(`Feedback from ${user ? user.email : 'Anonymous'} - ${feedbackMessage}`);
const userFeedback = { const userFeedback = {

View file

@ -58,7 +58,7 @@ const CollectionAdminEditsArea = (props: any) => {
return ( return (
<AuthenticatedClientElement <AuthenticatedClientElement
action="delete" action="delete"
ressourceType="collection" ressourceType="collections"
orgId={props.org_id} checkMethod='roles'> orgId={props.org_id} checkMethod='roles'>
<div className="flex space-x-1 justify-center mx-auto z-20 "> <div className="flex space-x-1 justify-center mx-auto z-20 ">
<ConfirmationModal <ConfirmationModal

View file

@ -54,7 +54,7 @@ const AdminEditsArea = (props: { orgSlug: string, courseId: string, course: any,
return ( return (
<AuthenticatedClientElement <AuthenticatedClientElement
action="update" action="update"
ressourceType="course" ressourceType="courses"
checkMethod='roles' orgId={props.course.org_id}> checkMethod='roles' orgId={props.course.org_id}>
<div className="flex space-x-2 absolute z-20 bottom-14 right-[15px] transform"> <div className="flex space-x-2 absolute z-20 bottom-14 right-[15px] transform">
<Link href={getUriWithOrg(props.orgSlug, "/dash/courses/course/" + removeCoursePrefix(props.courseId) + "/general")}> <Link href={getUriWithOrg(props.orgSlug, "/dash/courses/course/" + removeCoursePrefix(props.courseId) + "/general")}>

View file

@ -24,10 +24,13 @@ function ActivityIndicators(props: Props) {
function isActivityDone(activity: any) { function isActivityDone(activity: any) {
let run = props.course.trail.runs.find((run: any) => run.course_id == props.course.id); let run = props.course.trail?.runs.find((run: any) => run.course_id == props.course.id);
if (run) { if (run) {
return run.steps.find((step: any) => step.activity_id == activity.id); return run.steps.find((step: any) => step.activity_id == activity.id);
} }
else {
return false
}
} }

View file

@ -1,58 +0,0 @@
import { getNewAccessTokenUsingRefreshToken, getUserInfo } from '@services/auth/auth';
import { getAPIUrl } from '@services/config/config';
import { swrFetcher } from '@services/utils/ts/requests';
import React, { useEffect } from 'react'
import useSWR from 'swr';
const AuthContext = React.createContext({})
type Auth = {
access_token: string;
isAuthenticated: boolean;
user: any;
}
function AuthProvider({ children, orgslug }: { children: React.ReactNode, orgslug: string }) {
const [auth, setAuth] = React.useState<Auth>({ access_token: "", isAuthenticated: false, user: {} });
async function checkRefreshToken() {
//deleteCookie("access_token_cookie");
let data = await getNewAccessTokenUsingRefreshToken();
if (data) {
return data.access_token;
}
}
async function checkAuth() {
try {
let access_token = await checkRefreshToken();
let userInfo = {};
if (access_token) {
userInfo = await getUserInfo(access_token);
setAuth({ access_token: access_token, isAuthenticated: true, user: userInfo });
} else {
setAuth({ access_token: access_token, isAuthenticated: false, user: userInfo });
}
} catch (error) {
console.log(error);
}
}
useEffect(() => {
checkAuth();
}, [])
return (
<AuthContext.Provider value={auth}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
return React.useContext(AuthContext);
}
export default AuthProvider

View file

@ -1,79 +0,0 @@
"use client";
import React, { useEffect } from "react";
import { getNewAccessTokenUsingRefreshToken, getUserInfo } from "../../services/auth/auth";
import { useRouter, usePathname } from "next/navigation";
export const AuthContext: any = React.createContext({});
const PRIVATE_ROUTES = ["/course/*/edit", "/settings*", "/trail"];
const NON_AUTHENTICATED_ROUTES = ["/login", "/register"];
export interface Auth {
access_token: string;
isAuthenticated: boolean;
userInfo: {};
isLoading: boolean;
}
const AuthProvider = ({ children }: any) => {
const router = useRouter();
const pathname = usePathname();
const [auth, setAuth] = React.useState<Auth>({ access_token: "", isAuthenticated: false, userInfo: {}, isLoading: true });
function deleteCookie(cookieName: string) {
document.cookie = cookieName + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
async function checkRefreshToken() {
//deleteCookie("access_token_cookie");
let data = await getNewAccessTokenUsingRefreshToken();
if (data) {
return data.access_token;
}
}
async function checkAuth() {
try {
let access_token = await checkRefreshToken();
let userInfo = {};
let isLoading = false;
if (access_token) {
userInfo = await getUserInfo(access_token);
setAuth({ access_token, isAuthenticated: true, userInfo, isLoading });
// Redirect to home if user is trying to access a NON_AUTHENTICATED_ROUTES route
if (NON_AUTHENTICATED_ROUTES.some((route) => new RegExp(`^${route.replace("*", ".*")}$`).test(pathname))) {
router.push("/");
}
} else {
setAuth({ access_token, isAuthenticated: false, userInfo, isLoading });
// Redirect to login if user is trying to access a private route
if (PRIVATE_ROUTES.some((route) => new RegExp(`^${route.replace("*", ".*")}$`).test(pathname))) {
router.push("/login");
}
}
} catch (error) {
console.log(error);
}
}
useEffect(() => {
checkAuth();
return () => {
auth.isLoading = false;
};
}, [pathname]);
return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
};
export default AuthProvider;

View file

@ -1,49 +1,73 @@
'use client'; 'use client';
import React from "react"; import React from "react";
import { AuthContext } from "./AuthProviderDepreceated";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
import { getAPIUrl } from "@services/config/config"; import { getAPIUrl } from "@services/config/config";
import { swrFetcher } from "@services/utils/ts/requests"; import { swrFetcher } from "@services/utils/ts/requests";
import { useSession } from "@components/Contexts/SessionContext";
import { useOrg } from "@components/Contexts/OrgContext";
interface AuthenticatedClientElementProps { interface AuthenticatedClientElementProps {
children: React.ReactNode; children: React.ReactNode;
checkMethod: 'authentication' | 'roles'; checkMethod: 'authentication' | 'roles';
orgId?: string; orgId?: string;
ressourceType?: 'collection' | 'course' | 'activity' | 'user' | 'organization'; ressourceType?: 'collections' | 'courses' | 'activities' | 'users' | 'organizations';
action?: 'create' | 'update' | 'delete' | 'read'; action?: 'create' | 'update' | 'delete' | 'read';
} }
function generateRessourceId(ressourceType: string) {
// for every type of ressource, we need to generate a ressource id, example for a collection: col_XXXXX
if (ressourceType == 'collection') {
return `collection_xxxx`
}
else if (ressourceType == 'course') {
return `course_xxxx`
}
else if (ressourceType == 'activity') {
return `activity_xxxx`
}
else if (ressourceType == 'user') {
return `user_xxxx`
}
else if (ressourceType == 'organization') {
return `org_xxxx`
}
else if (ressourceType === null) {
return `n/a`
}
}
export const AuthenticatedClientElement = (props: AuthenticatedClientElementProps) => { export const AuthenticatedClientElement = (props: AuthenticatedClientElementProps) => {
const auth: any = React.useContext(AuthContext); const [isAllowed, setIsAllowed] = React.useState(false);
const { data: authorization_status, error: error } = useSWR(props.checkMethod == 'roles' && props.ressourceType ? `${getAPIUrl()}users/authorize/ressource/${generateRessourceId(props.ressourceType)}/action/${props.action}` : null, swrFetcher); const session = useSession() as any;
console.log(authorization_status); const org = useOrg() as any;
if ((props.checkMethod == 'authentication' && auth.isAuthenticated) || (auth.isAuthenticated && props.checkMethod == 'roles' && authorization_status)) {
return <>{props.children}</>; function isUserAllowed(roles: any[], action: string, resourceType: string, org_uuid: string): boolean {
// Iterate over the user's roles
for (const role of roles) {
// Check if the role is for the right organization
if (role.org.org_uuid === org_uuid) {
// Check if the user has the role for the resource type
if (role.role.rights && role.role.rights[resourceType]) {
// Check if the user is allowed to execute the action
const actionKey = `action_${action}`;
if (role.role.rights[resourceType][actionKey] === true) {
return true;
}
}
}
}
// If no role matches the organization, resource type, and action, return false
return false;
} }
return <></>;
function check() {
if (props.checkMethod === 'authentication') {
setIsAllowed(session.isAuthenticated);
} else if (props.checkMethod === 'roles') {
return setIsAllowed(isUserAllowed(session.roles, props.action!, props.ressourceType!, org.org_uuid));
}
}
React.useEffect(() => {
if (session.isLoading) {
return;
}
check();
}, [session, org])
return (
<>
{isAllowed && props.children}
</>
)
} }

View file

@ -1,18 +1,18 @@
'use client'; 'use client';
import React from "react"; import React, { use, useEffect } from "react";
import styled from "styled-components"; import styled from "styled-components";
import Link from "next/link"; import Link from "next/link";
import { AuthContext } from "./AuthProviderDepreceated";
import Avvvatars from "avvvatars-react"; import Avvvatars from "avvvatars-react";
import { GearIcon } from "@radix-ui/react-icons"; import { GearIcon } from "@radix-ui/react-icons";
import { Settings } from "lucide-react"; import { Settings } from "lucide-react";
import { useSession } from "@components/Contexts/SessionContext";
export const HeaderProfileBox = () => { export const HeaderProfileBox = () => {
const auth: any = React.useContext(AuthContext); const session = useSession() as any;
return ( return (
<ProfileArea> <ProfileArea>
{!auth.isAuthenticated && ( {!session.isAuthenticated && (
<UnidentifiedArea className="flex text-sm text-gray-700 font-bold p-1.5 px-2 rounded-lg"> <UnidentifiedArea className="flex text-sm text-gray-700 font-bold p-1.5 px-2 rounded-lg">
<ul className="flex space-x-3 items-center"> <ul className="flex space-x-3 items-center">
<li> <li>
@ -28,13 +28,13 @@ export const HeaderProfileBox = () => {
</ul> </ul>
</UnidentifiedArea> </UnidentifiedArea>
)} )}
{auth.isAuthenticated && ( {session.isAuthenticated && (
<AccountArea className="space-x-0"> <AccountArea className="space-x-0">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="text-xs">{auth.userInfo.username} </div> <div className="text-xs">{session.user.username} </div>
<div className="py-4"> <div className="py-4">
<div className="shadow-sm rounded-xl"> <div className="shadow-sm rounded-xl">
<Avvvatars radius={3} size={30} value={auth.userInfo.user_uuid} style="shape" /> <Avvvatars radius={3} size={30} value={session.user.user_uuid} style="shape" />
</div> </div>
</div> </div>
<Link className="text-gray-600" href={"/dash"}><Settings size={14} /></Link> <Link className="text-gray-600" href={"/dash"}><Settings size={14} /></Link>

View file

@ -29,6 +29,26 @@ export async function loginAndGetToken(username: string, password: string): Prom
return response; return response;
} }
export async function logout(): Promise<any> {
// Request Config
// get origin
const HeadersConfig = new Headers({ "Content-Type": "application/x-www-form-urlencoded" });
const urlencoded = new URLSearchParams();
const requestOptions: any = {
method: "DELETE",
headers: HeadersConfig,
body: urlencoded,
redirect: "follow",
credentials: "include",
};
// fetch using await and async
const response = await fetch(`${getAPIUrl()}auth/logout`, requestOptions);
return response;
}
export async function getUserInfo(token: string): Promise<any> { export async function getUserInfo(token: string): Promise<any> {
const origin = window.location.origin; const origin = window.location.origin;
const HeadersConfig = new Headers({ Authorization: `Bearer ${token}`, Origin: origin }); const HeadersConfig = new Headers({ Authorization: `Bearer ${token}`, Origin: origin });
@ -45,6 +65,22 @@ export async function getUserInfo(token: string): Promise<any> {
.catch((error) => console.log("error", error)); .catch((error) => console.log("error", error));
} }
export async function getUserSession(token: string): Promise<any> {
const origin = window.location.origin;
const HeadersConfig = new Headers({ Authorization: `Bearer ${token}`, Origin: origin });
const requestOptions: any = {
method: "GET",
headers: HeadersConfig,
redirect: "follow",
credentials: "include",
};
return fetch(`${getAPIUrl()}users/session`, requestOptions)
.then((result) => result.json())
.catch((error) => console.log("error", error));
}
export async function getNewAccessTokenUsingRefreshToken(): Promise<any> { export async function getNewAccessTokenUsingRefreshToken(): Promise<any> {
const requestOptions: any = { const requestOptions: any = {
method: "POST", method: "POST",