mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
Merge pull request #135 from learnhouse/fix/post-migration-bugs
Post Migration Bug Fixes
This commit is contained in:
commit
a7e2bda41e
44 changed files with 580 additions and 336 deletions
|
|
@ -11,6 +11,8 @@ botocore
|
|||
python-jose
|
||||
passlib
|
||||
fastapi-jwt-auth
|
||||
pytest
|
||||
httpx
|
||||
faker
|
||||
requests
|
||||
pyyaml
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ class CourseRead(CourseBase):
|
|||
|
||||
class FullCourseRead(CourseBase):
|
||||
id: int
|
||||
course_uuid: str
|
||||
creation_date: str
|
||||
update_date: str
|
||||
course_uuid: Optional[str]
|
||||
creation_date: Optional[str]
|
||||
update_date: Optional[str]
|
||||
# Chapters, Activities
|
||||
chapters: List[ChapterRead]
|
||||
authors: List[UserRead]
|
||||
|
|
@ -60,9 +60,9 @@ class FullCourseRead(CourseBase):
|
|||
|
||||
class FullCourseReadWithTrail(CourseBase):
|
||||
id: int
|
||||
course_uuid: str
|
||||
creation_date: str
|
||||
update_date: str
|
||||
course_uuid: Optional[str]
|
||||
creation_date: Optional[str]
|
||||
update_date: Optional[str]
|
||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||
authors: List[UserRead]
|
||||
# Chapters, Activities
|
||||
|
|
|
|||
|
|
@ -47,10 +47,10 @@ class TrailRunRead(BaseModel):
|
|||
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||
user_id: int = Field(default=None, foreign_key="user.id")
|
||||
# course object
|
||||
course: dict
|
||||
course: Optional[dict]
|
||||
# timestamps
|
||||
creation_date: str
|
||||
update_date: str
|
||||
creation_date: Optional[str]
|
||||
update_date: Optional[str]
|
||||
# number of activities in course
|
||||
course_total_steps: int
|
||||
steps: list[TrailStep]
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ class TrailCreate(TrailBase):
|
|||
# trick because Lists are not supported in SQLModel (runs: list[TrailRun] )
|
||||
class TrailRead(BaseModel):
|
||||
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")
|
||||
user_id: int = Field(default=None, foreign_key="user.id")
|
||||
creation_date: str
|
||||
update_date: str
|
||||
creation_date: Optional[str]
|
||||
update_date: Optional[str]
|
||||
runs: list[TrailRunRead]
|
||||
|
||||
class Config:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
from src.db.roles import RoleRead
|
||||
from src.db.organizations import OrganizationRead
|
||||
|
||||
|
||||
class UserBase(SQLModel):
|
||||
username: str
|
||||
|
|
@ -33,14 +37,27 @@ class UserRead(UserBase):
|
|||
id: int
|
||||
user_uuid: str
|
||||
|
||||
|
||||
class PublicUser(UserRead):
|
||||
pass
|
||||
|
||||
|
||||
class UserRoleWithOrg(BaseModel):
|
||||
role: RoleRead
|
||||
org: OrganizationRead
|
||||
|
||||
|
||||
class UserSession(BaseModel):
|
||||
user: UserRead
|
||||
roles: list[UserRoleWithOrg]
|
||||
|
||||
|
||||
class AnonymousUser(SQLModel):
|
||||
id: int = 0
|
||||
user_uuid: str = "user_anonymous"
|
||||
username: str = "anonymous"
|
||||
|
||||
|
||||
class User(UserBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
password: str = ""
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from src.db.users import (
|
|||
User,
|
||||
UserCreate,
|
||||
UserRead,
|
||||
UserSession,
|
||||
UserUpdate,
|
||||
UserUpdatePassword,
|
||||
)
|
||||
|
|
@ -17,6 +18,7 @@ from src.services.users.users import (
|
|||
create_user,
|
||||
create_user_without_org,
|
||||
delete_user_by_id,
|
||||
get_user_session,
|
||||
read_user_by_id,
|
||||
read_user_by_uuid,
|
||||
update_user,
|
||||
|
|
@ -35,6 +37,18 @@ async def api_get_current_user(current_user: User = Depends(get_current_user)):
|
|||
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}")
|
||||
async def api_get_authorization_status(
|
||||
request: Request,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ async def authorization_verify_if_element_is_public(
|
|||
# Verifies if the element is public
|
||||
if element_nature == ("courses" or "collections") and action == "read":
|
||||
if element_nature == "courses":
|
||||
print("looking for course")
|
||||
statement = select(Course).where(
|
||||
Course.public is True, Course.course_uuid == element_uuid
|
||||
)
|
||||
|
|
@ -28,10 +29,7 @@ async def authorization_verify_if_element_is_public(
|
|||
if course:
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights (public content) : You don't have the right to perform this action",
|
||||
)
|
||||
return False
|
||||
|
||||
if element_nature == "collections":
|
||||
statement = select(Collection).where(
|
||||
|
|
@ -42,15 +40,9 @@ async def authorization_verify_if_element_is_public(
|
|||
if collection:
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights (public content) : You don't have the right to perform this action",
|
||||
)
|
||||
return False
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights (public content) : You don't have the right to perform this action",
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
# Tested and working
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from sqlmodel import Session, select
|
|||
from src.db.chapters import Chapter
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.db.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate
|
||||
|
|
@ -212,18 +213,31 @@ async def get_activities(
|
|||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
course_id: str,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
):
|
||||
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(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
course_id,
|
||||
course_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from sqlmodel import Session, select
|
|||
from src.db.users import AnonymousUser
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.db.course_chapters import CourseChapter
|
||||
|
|
@ -207,6 +208,10 @@ async def get_course_chapters(
|
|||
page: int = 1,
|
||||
limit: int = 10,
|
||||
) -> List[ChapterRead]:
|
||||
|
||||
statement = select(Course).where(Course.id == course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
statement = (
|
||||
select(Chapter)
|
||||
.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]
|
||||
|
||||
# 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
|
||||
for chapter in chapters:
|
||||
|
|
@ -532,18 +537,31 @@ async def reorder_chapters_and_activities(
|
|||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
course_id: str,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
):
|
||||
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(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
course_id,
|
||||
course_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from sqlmodel import Session, select
|
|||
from src.db.users import AnonymousUser
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.db.collections import (
|
||||
|
|
@ -245,20 +246,34 @@ async def get_collections(
|
|||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
course_id: str,
|
||||
collection_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
):
|
||||
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(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
course_id,
|
||||
collection_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
||||
|
|
|
|||
|
|
@ -96,12 +96,17 @@ async def get_course_meta(
|
|||
chapters = await get_course_chapters(request, course.id, db_session, current_user)
|
||||
|
||||
# Trail
|
||||
trail = None
|
||||
|
||||
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)
|
||||
|
||||
|
||||
return FullCourseReadWithTrail(
|
||||
**course.dict(),
|
||||
chapters=chapters,
|
||||
|
|
@ -359,7 +364,6 @@ async def get_courses_orgslug(
|
|||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
||||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
|
|
@ -369,13 +373,16 @@ async def rbac_check(
|
|||
):
|
||||
if action == "read":
|
||||
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
|
||||
)
|
||||
print('res',res)
|
||||
return res
|
||||
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
|
||||
)
|
||||
return res
|
||||
else:
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ async def update_install_instance(
|
|||
|
||||
|
||||
# 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
|
||||
statement = select(Role).where(Role.role_type == RoleTypeEnum.TYPE_GLOBAL)
|
||||
roles = db_session.exec(statement).all()
|
||||
|
|
@ -279,7 +279,7 @@ async def install_default_elements(request: Request, data: dict, db_session: Ses
|
|||
|
||||
# Organization creation
|
||||
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)
|
||||
|
||||
|
|
@ -296,7 +296,7 @@ async def install_create_organization(
|
|||
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from src.db.courses import Course
|
|||
from src.db.trail_runs import TrailRun, TrailRunRead
|
||||
from src.db.trail_steps import TrailStep
|
||||
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(
|
||||
|
|
@ -17,7 +17,7 @@ async def create_user_trail(
|
|||
trail_object: TrailCreate,
|
||||
db_session: Session,
|
||||
) -> 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()
|
||||
|
||||
if trail:
|
||||
|
|
@ -103,7 +103,7 @@ async def check_trail_presence(
|
|||
user: PublicUser,
|
||||
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()
|
||||
|
||||
if not trail:
|
||||
|
|
@ -122,9 +122,15 @@ async def check_trail_presence(
|
|||
|
||||
|
||||
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:
|
||||
|
||||
if isinstance(user, AnonymousUser):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Anonymous users cannot access this endpoint",
|
||||
)
|
||||
|
||||
trail = await check_trail_presence(
|
||||
org_id=org_id,
|
||||
user_id=user.id,
|
||||
|
|
|
|||
|
|
@ -3,17 +3,20 @@ from typing import Literal
|
|||
from uuid import uuid4
|
||||
from fastapi import HTTPException, Request, status
|
||||
from sqlmodel import Session, select
|
||||
from src.db.roles import Role, RoleRead
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.db.organizations import Organization
|
||||
from src.db.organizations import Organization, OrganizationRead
|
||||
from src.db.users import (
|
||||
AnonymousUser,
|
||||
PublicUser,
|
||||
User,
|
||||
UserCreate,
|
||||
UserRead,
|
||||
UserRoleWithOrg,
|
||||
UserSession,
|
||||
UserUpdate,
|
||||
UserUpdatePassword,
|
||||
)
|
||||
|
|
@ -279,6 +282,57 @@ async def read_user_by_uuid(
|
|||
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(
|
||||
request: Request,
|
||||
db_session: Session,
|
||||
|
|
|
|||
50
apps/api/src/tests/test_main.py
Normal file
50
apps/api/src/tests/test_main.py
Normal 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
|
||||
0
apps/api/src/tests/test_rbac.py
Normal file
0
apps/api/src/tests/test_rbac.py
Normal file
0
apps/api/src/tests/utils/__init__.py
Normal file
0
apps/api/src/tests/utils/__init__.py
Normal file
57
apps/api/src/tests/utils/init_data_for_tests.py
Normal file
57
apps/api/src/tests/utils/init_data_for_tests.py
Normal 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
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { default as React, } from "react";
|
||||
import AuthProvider from "@components/Security/AuthProviderDepreceated";
|
||||
import EditorWrapper from "@components/Objects/Editor/EditorWrapper";
|
||||
import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
|
||||
import { cookies } from "next/headers";
|
||||
|
|
@ -7,6 +6,7 @@ import { Metadata } from "next";
|
|||
import { getActivityWithAuthHeader } from "@services/courses/activities";
|
||||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
|
||||
import { getOrganizationContextInfo, getOrganizationContextInfoWithId } from "@services/organizations/orgs";
|
||||
import SessionProvider from "@components/Contexts/SessionContext";
|
||||
|
||||
type MetadataProps = {
|
||||
params: { orgslug: string, courseid: string, activityid: string };
|
||||
|
|
@ -39,9 +39,9 @@ const EditActivity = async (params: any) => {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<AuthProvider>
|
||||
<SessionProvider>
|
||||
<EditorWrapper org={org} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>
|
||||
</AuthProvider>
|
||||
</SessionProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { deleteOrganizationFromBackend } from "@services/organizations/orgs";
|
|||
import useSWR, { mutate } from "swr";
|
||||
import { swrFetcher } from "@services/utils/ts/requests";
|
||||
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
|
||||
import AuthProvider from "@components/Security/AuthProviderDepreceated";
|
||||
|
||||
const Organizations = () => {
|
||||
const { data: organizations, error } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, swrFetcher)
|
||||
|
|
@ -17,7 +16,6 @@ const Organizations = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<AuthProvider />
|
||||
<div className="font-bold text-lg">
|
||||
Your Organizations{" "}
|
||||
<Link href="/organizations/new">
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ const CollectionsPage = async (params: any) => {
|
|||
<div className="flex justify-between" >
|
||||
<TypeOfContentTitle title="Collections" type="col" />
|
||||
<AuthenticatedClientElement
|
||||
ressourceType="collection"
|
||||
ressourceType="collections"
|
||||
action="create"
|
||||
checkMethod='roles' orgId={org_id}>
|
||||
<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>
|
||||
</div>
|
||||
<AuthenticatedClientElement checkMethod='roles'
|
||||
ressourceType="collection"
|
||||
ressourceType="collections"
|
||||
action="create"
|
||||
orgId={org_id}>
|
||||
<Link href={getUriWithOrg(orgslug, "/collections/new")}>
|
||||
|
|
|
|||
|
|
@ -42,9 +42,9 @@ const CourseClient = (props: any) => {
|
|||
}
|
||||
|
||||
function isCourseStarted() {
|
||||
const runs = course.trail.runs;
|
||||
// checks if one of the obejcts in the array has the property "STATUS_IN_PROGRESS"
|
||||
return runs.some((run: any) => run.status === "STATUS_IN_PROGRESS");
|
||||
const runs = course.trail?.runs;
|
||||
if (!runs) return false;
|
||||
return runs.some((run: any) => run.status === "STATUS_IN_PROGRESS" && run.course_id === course.id);
|
||||
}
|
||||
|
||||
async function quitCourse() {
|
||||
|
|
@ -197,7 +197,6 @@ const CourseClient = (props: any) => {
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
{console.log(course)}
|
||||
|
||||
{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}>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ function Courses(props: CourseProps) {
|
|||
<TypeOfContentTitle title="Courses" type="cou" />
|
||||
<AuthenticatedClientElement checkMethod='roles'
|
||||
action='create'
|
||||
ressourceType='course'
|
||||
ressourceType='courses'
|
||||
orgId={props.org_id}>
|
||||
<Modal
|
||||
isDialogOpen={newCourseModal}
|
||||
|
|
@ -78,7 +78,7 @@ function Courses(props: CourseProps) {
|
|||
</div>
|
||||
<AuthenticatedClientElement
|
||||
action='create'
|
||||
ressourceType='course'
|
||||
ressourceType='courses'
|
||||
checkMethod='roles' orgId={props.org_id}>
|
||||
<Modal
|
||||
isDialogOpen={newCourseModal}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import "@styles/globals.css";
|
||||
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}) {
|
||||
return (
|
||||
<>
|
||||
<AuthProvider>
|
||||
<SessionProvider>
|
||||
<Menu orgslug={params?.orgslug}></Menu>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
</SessionProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ const OrgHomePage = async (params: any) => {
|
|||
</div>
|
||||
<AuthenticatedClientElement
|
||||
checkMethod='roles'
|
||||
ressourceType='collection'
|
||||
ressourceType='collections'
|
||||
action='create'
|
||||
orgId={org_id}>
|
||||
<Link href={getUriWithOrg(orgslug, "/collections/new")}>
|
||||
|
|
@ -110,7 +110,7 @@ const OrgHomePage = async (params: any) => {
|
|||
<TypeOfContentTitle title="Courses" type="cou" />
|
||||
</div>
|
||||
<AuthenticatedClientElement
|
||||
ressourceType='course'
|
||||
ressourceType='courses'
|
||||
action='create'
|
||||
checkMethod='roles'
|
||||
orgId={org_id}>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ function CoursesHome(params: CourseProps) {
|
|||
<div className='pt-3 flex font-bold text-4xl'>Courses</div>
|
||||
<AuthenticatedClientElement checkMethod='roles'
|
||||
action='create'
|
||||
ressourceType='course'
|
||||
ressourceType='courses'
|
||||
orgId={params.org_id}>
|
||||
<Modal
|
||||
isDialogOpen={newCourseModal}
|
||||
|
|
@ -80,7 +80,7 @@ function CoursesHome(params: CourseProps) {
|
|||
</div>
|
||||
<AuthenticatedClientElement
|
||||
action='create'
|
||||
ressourceType='course'
|
||||
ressourceType='courses'
|
||||
checkMethod='roles' orgId={params.org_id}>
|
||||
<Modal
|
||||
isDialogOpen={newCourseModal}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import SessionProvider from '@components/Contexts/SessionContext'
|
||||
import LeftMenu from '@components/Dashboard/UI/LeftMenu'
|
||||
import AuthProvider from '@components/Security/AuthProviderDepreceated'
|
||||
import React from 'react'
|
||||
|
||||
function DashboardLayout({ children, params }: { children: React.ReactNode, params: any }) {
|
||||
return (
|
||||
<>
|
||||
<AuthProvider>
|
||||
<SessionProvider>
|
||||
<div className='flex'>
|
||||
<LeftMenu/>
|
||||
<div className='flex w-full'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</AuthProvider>
|
||||
</SessionProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import Link from 'next/link';
|
|||
import { getUriWithOrg } from '@services/config/config';
|
||||
import { Info, Lock } from 'lucide-react';
|
||||
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs';
|
||||
import { useAuth } from '@components/Security/AuthContext';
|
||||
import { useSession } from '@components/Contexts/SessionContext';
|
||||
|
||||
export type SettingsParams = {
|
||||
subpage: string
|
||||
|
|
@ -15,17 +15,17 @@ export type SettingsParams = {
|
|||
}
|
||||
|
||||
function SettingsPage({ params }: { params: SettingsParams }) {
|
||||
const auth = useAuth() as any;
|
||||
const session = useSession() as any;
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
}
|
||||
, [auth])
|
||||
, [session])
|
||||
|
||||
return (
|
||||
<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)]'>
|
||||
<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='w-100 flex justify-between'>
|
||||
<div className='pt-3 flex font-bold text-4xl'>Account Settings</div>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,17 @@
|
|||
'use client';
|
||||
import { OrgProvider } from "@components/Contexts/OrgContext";
|
||||
import AuthProvider from "@components/Security/AuthContext";
|
||||
import { getAPIUrl } from "@services/config/config";
|
||||
import { swrFetcher } from "@services/utils/ts/requests";
|
||||
import SessionProvider from "@components/Contexts/SessionContext";
|
||||
import "@styles/globals.css";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function RootLayout({ children, params }: { children: React.ReactNode, params: any }) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AuthProvider orgslug={params.orgslug}>
|
||||
<OrgProvider orgslug={params.orgslug}>
|
||||
<SessionProvider>
|
||||
{children}
|
||||
</SessionProvider>
|
||||
</OrgProvider>
|
||||
</AuthProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, For
|
|||
import Image from 'next/image';
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import { getOrgLogoMediaDirectory } from '@services/media/media';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import { AlertTriangle, Check, User } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { signup } from '@services/auth/auth';
|
||||
import { getUriWithOrg } from '@services/config/config';
|
||||
|
|
@ -44,10 +44,6 @@ const validate = (values: any) => {
|
|||
errors.username = 'Username must be at least 4 characters';
|
||||
}
|
||||
|
||||
if (!values.full_name) {
|
||||
errors.full_name = 'Required';
|
||||
}
|
||||
|
||||
if (!values.bio) {
|
||||
errors.bio = 'Required';
|
||||
}
|
||||
|
|
@ -61,6 +57,7 @@ function SignUpClient(props: SignUpClientProps) {
|
|||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
const router = useRouter();
|
||||
const [error, setError] = React.useState('');
|
||||
const [message, setMessage] = React.useState('');
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
org_slug: props.org?.slug,
|
||||
|
|
@ -68,7 +65,8 @@ function SignUpClient(props: SignUpClientProps) {
|
|||
password: '',
|
||||
username: '',
|
||||
bio: '',
|
||||
full_name: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
},
|
||||
validate,
|
||||
onSubmit: async values => {
|
||||
|
|
@ -76,7 +74,8 @@ function SignUpClient(props: SignUpClientProps) {
|
|||
let res = await signup(values);
|
||||
let message = await res.json();
|
||||
if (res.status == 200) {
|
||||
router.push(`/`);
|
||||
//router.push(`/login`);
|
||||
setMessage('Your account was successfully created')
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
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>
|
||||
)}
|
||||
{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}>
|
||||
<FormField name="email">
|
||||
<FormLabelAndMessage label='Email' message={formik.errors.email} />
|
||||
|
|
@ -150,16 +159,6 @@ function SignUpClient(props: SignUpClientProps) {
|
|||
</Form.Control>
|
||||
</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 */}
|
||||
<FormField name="bio">
|
||||
<FormLabelAndMessage label='Bio' message={formik.errors.bio} />
|
||||
|
|
|
|||
65
apps/web/components/Contexts/SessionContext.tsx
Normal file
65
apps/web/components/Contexts/SessionContext.tsx
Normal 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
|
||||
|
|
@ -1,26 +1,37 @@
|
|||
'use client';
|
||||
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 LearnHouseDashboardLogo from '@public/dashLogo.png';
|
||||
import { logout } from '@services/auth/auth';
|
||||
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 Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { use, useEffect } from 'react'
|
||||
|
||||
function LeftMenu() {
|
||||
const org = useOrg() as any;
|
||||
const auth = useAuth() as any;
|
||||
const session = useSession() as any;
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const route = useRouter();
|
||||
|
||||
function waitForEverythingToLoad() {
|
||||
if (org && auth) {
|
||||
if (org && session) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function logOutUI() {
|
||||
const res = await logout();
|
||||
if (res) {
|
||||
route.push('/login');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (waitForEverythingToLoad()) {
|
||||
setLoading(false);
|
||||
|
|
@ -59,20 +70,23 @@ function LeftMenu() {
|
|||
</ToolTip>
|
||||
</div>
|
||||
<div className='flex flex-col mx-auto pb-7 space-y-2'>
|
||||
<ToolTip content={auth.user.username + "'s Settings"} slateBlack sideOffset={8} side='right' >
|
||||
|
||||
<div className="flex items-center flex-col space-y-2">
|
||||
<ToolTip content={session.user.username} slateBlack sideOffset={8} side='right' >
|
||||
<div className="mx-auto shadow-lg">
|
||||
<Avvvatars radius={3} border borderColor='white' borderSize={3} size={35} value={session.user.user_uuid} style="shape" />
|
||||
</div>
|
||||
</ToolTip>
|
||||
<div className='flex items-center flex-col space-y-1'>
|
||||
<ToolTip content={session.user.username + "'s Settings"} slateBlack sideOffset={8} side='right' >
|
||||
<Link href={'/dash/user/settings/general'} className='py-3'>
|
||||
<Settings className='mx-auto text-neutral-400 cursor-pointer' size={18} />
|
||||
</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">
|
||||
<Avvvatars radius={3} border borderColor='white' borderSize={3} size={35} value={auth.user.user_uuid} style="shape" />
|
||||
<ToolTip content={'Logout'} slateBlack sideOffset={8} side='right' >
|
||||
<LogOut onClick={() => logOutUI()} className='mx-auto text-neutral-400 cursor-pointer' size={14} />
|
||||
</ToolTip>
|
||||
</div>
|
||||
</ToolTip>
|
||||
<ToolTip content={'Learnhouse Version'} slateBlack sideOffset={8} side='right' >
|
||||
<div className='py-1 px-3 bg-white/10 opacity-40 rounded-full text-[10px] uppercase justify-center text-center'>alpha</div>
|
||||
</ToolTip>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
import { useAuth } from '@components/Security/AuthContext';
|
||||
import { updateProfile } from '@services/settings/profile';
|
||||
import React, { useEffect } from 'react'
|
||||
import { Formik, Form, Field, ErrorMessage } from 'formik';
|
||||
import { useSession } from '@components/Contexts/SessionContext';
|
||||
|
||||
function UserEditGeneral() {
|
||||
const auth = useAuth() as any;
|
||||
const session = useSession() as any;
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
}
|
||||
, [auth, auth.user])
|
||||
, [session, session.user])
|
||||
|
||||
return (
|
||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
|
||||
{auth.user && (
|
||||
{session.user && (
|
||||
<Formik
|
||||
enableReinitialize
|
||||
initialValues={{
|
||||
username: auth.user.username,
|
||||
first_name: auth.user.first_name,
|
||||
last_name: auth.user.last_name,
|
||||
email: auth.user.email,
|
||||
bio: auth.user.bio,
|
||||
username: session.user.username,
|
||||
first_name: session.user.first_name,
|
||||
last_name: session.user.last_name,
|
||||
email: session.user.email,
|
||||
bio: session.user.bio,
|
||||
}}
|
||||
onSubmit={(values, { setSubmitting }) => {
|
||||
setTimeout(() => {
|
||||
|
||||
setSubmitting(false);
|
||||
updateProfile(values,auth.user.id)
|
||||
updateProfile(values,session.user.id)
|
||||
}, 400);
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import { useAuth } from '@components/Security/AuthContext';
|
||||
import { useSession } from '@components/Contexts/SessionContext';
|
||||
import { updatePassword } from '@services/settings/password';
|
||||
import { Formik, Form, Field, ErrorMessage } from 'formik';
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
function UserEditPassword() {
|
||||
const auth = useAuth() as any;
|
||||
const session = useSession() as 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)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
}
|
||||
, [auth])
|
||||
, [session])
|
||||
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import React from "react";
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { AuthContext } from "../../Security/AuthProviderDepreceated";
|
||||
import learnhouseIcon from "public/learnhouse_icon.png";
|
||||
import { ToolbarButtons } from "./Toolbar/ToolbarButtons";
|
||||
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 { CourseProvider } from "@components/Contexts/CourseContext";
|
||||
import { OrgProvider } from "@components/Contexts/OrgContext";
|
||||
import { useSession } from "@components/Contexts/SessionContext";
|
||||
|
||||
|
||||
interface Editor {
|
||||
|
|
@ -51,7 +51,7 @@ interface Editor {
|
|||
}
|
||||
|
||||
function Editor(props: Editor) {
|
||||
const auth: any = React.useContext(AuthContext);
|
||||
const session = useSession() as any;
|
||||
// remove course_ from course_uuid
|
||||
const course_uuid = props.course.course_uuid.substring(7);
|
||||
|
||||
|
|
@ -164,8 +164,8 @@ function Editor(props: Editor) {
|
|||
</EditorDocSection>
|
||||
<EditorUsersSection>
|
||||
<EditorUserProfileWrapper>
|
||||
{!auth.isAuthenticated && <span>Loading</span>}
|
||||
{auth.isAuthenticated && <Avvvatars value={auth.userInfo.user_uuid} style="shape" />}
|
||||
{!session.isAuthenticated && <span>Loading</span>}
|
||||
{session.isAuthenticated && <Avvvatars value={session.user.user_uuid} style="shape" />}
|
||||
</EditorUserProfileWrapper>
|
||||
<DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey", opacity: '0.5' }} />
|
||||
<EditorLeftOptionsSection className="space-x-2 pl-2 pr-3">
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import * as Form from '@radix-ui/react-form'
|
|||
import React, { useState } from "react";
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { CheckCircleIcon } from "lucide-react";
|
||||
import { AuthContext } from "@components/Security/AuthProviderDepreceated";
|
||||
import { randomUUID } from "crypto";
|
||||
import { useSession } from "@components/Contexts/SessionContext";
|
||||
|
||||
export const FeedbackModal = (user: any) => {
|
||||
const auth: any = React.useContext(AuthContext);
|
||||
const session = useSession() as any;
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [view, setView] = useState<"feedbackForm" | "success">("feedbackForm")
|
||||
|
|
@ -19,7 +19,7 @@ export const FeedbackModal = (user: any) => {
|
|||
e.preventDefault();
|
||||
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 userFeedback = {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const CollectionAdminEditsArea = (props: any) => {
|
|||
return (
|
||||
<AuthenticatedClientElement
|
||||
action="delete"
|
||||
ressourceType="collection"
|
||||
ressourceType="collections"
|
||||
orgId={props.org_id} checkMethod='roles'>
|
||||
<div className="flex space-x-1 justify-center mx-auto z-20 ">
|
||||
<ConfirmationModal
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const AdminEditsArea = (props: { orgSlug: string, courseId: string, course: any,
|
|||
return (
|
||||
<AuthenticatedClientElement
|
||||
action="update"
|
||||
ressourceType="course"
|
||||
ressourceType="courses"
|
||||
checkMethod='roles' orgId={props.course.org_id}>
|
||||
<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")}>
|
||||
|
|
|
|||
|
|
@ -24,10 +24,13 @@ function ActivityIndicators(props: Props) {
|
|||
|
||||
|
||||
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) {
|
||||
return run.steps.find((step: any) => step.activity_id == activity.id);
|
||||
}
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,49 +1,73 @@
|
|||
'use client';
|
||||
import React from "react";
|
||||
import { AuthContext } from "./AuthProviderDepreceated";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { getAPIUrl } from "@services/config/config";
|
||||
import { swrFetcher } from "@services/utils/ts/requests";
|
||||
import { useSession } from "@components/Contexts/SessionContext";
|
||||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
|
||||
interface AuthenticatedClientElementProps {
|
||||
children: React.ReactNode;
|
||||
checkMethod: 'authentication' | 'roles';
|
||||
orgId?: string;
|
||||
ressourceType?: 'collection' | 'course' | 'activity' | 'user' | 'organization';
|
||||
ressourceType?: 'collections' | 'courses' | 'activities' | 'users' | 'organizations';
|
||||
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) => {
|
||||
const auth: any = React.useContext(AuthContext);
|
||||
const { data: authorization_status, error: error } = useSWR(props.checkMethod == 'roles' && props.ressourceType ? `${getAPIUrl()}users/authorize/ressource/${generateRessourceId(props.ressourceType)}/action/${props.action}` : null, swrFetcher);
|
||||
console.log(authorization_status);
|
||||
const [isAllowed, setIsAllowed] = React.useState(false);
|
||||
const session = useSession() as any;
|
||||
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;
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no role matches the organization, resource type, and action, return false
|
||||
return false;
|
||||
}
|
||||
|
||||
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}
|
||||
</>
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
'use client';
|
||||
import React from "react";
|
||||
import React, { use, useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import Link from "next/link";
|
||||
import { AuthContext } from "./AuthProviderDepreceated";
|
||||
import Avvvatars from "avvvatars-react";
|
||||
import { GearIcon } from "@radix-ui/react-icons";
|
||||
import { Settings } from "lucide-react";
|
||||
import { useSession } from "@components/Contexts/SessionContext";
|
||||
|
||||
export const HeaderProfileBox = () => {
|
||||
const auth: any = React.useContext(AuthContext);
|
||||
const session = useSession() as any;
|
||||
|
||||
return (
|
||||
<ProfileArea>
|
||||
{!auth.isAuthenticated && (
|
||||
{!session.isAuthenticated && (
|
||||
<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">
|
||||
<li>
|
||||
|
|
@ -28,13 +28,13 @@ export const HeaderProfileBox = () => {
|
|||
</ul>
|
||||
</UnidentifiedArea>
|
||||
)}
|
||||
{auth.isAuthenticated && (
|
||||
{session.isAuthenticated && (
|
||||
<AccountArea className="space-x-0">
|
||||
<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="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>
|
||||
<Link className="text-gray-600" href={"/dash"}><Settings size={14} /></Link>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,26 @@ export async function loginAndGetToken(username: string, password: string): Prom
|
|||
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> {
|
||||
const origin = window.location.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));
|
||||
}
|
||||
|
||||
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> {
|
||||
const requestOptions: any = {
|
||||
method: "POST",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue