Merge pull request #133 from learnhouse/feat/psql-migration-and-frontend-improvements

PostgreSQL migration and frontend improvements
This commit is contained in:
Badr B 2023-12-16 12:34:53 +01:00 committed by GitHub
commit 0e2e66d0e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
172 changed files with 7488 additions and 5008 deletions

View file

@ -46,7 +46,8 @@ class HostingConfig(BaseModel):
class DatabaseConfig(BaseModel): class DatabaseConfig(BaseModel):
mongodb_connection_string: Optional[str] sql_connection_string: Optional[str]
mongo_connection_string: Optional[str]
class LearnHouseConfig(BaseModel): class LearnHouseConfig(BaseModel):
@ -105,9 +106,7 @@ def get_learnhouse_config() -> LearnHouseConfig:
env_allowed_origins = env_allowed_origins.split(",") env_allowed_origins = env_allowed_origins.split(",")
env_allowed_regexp = os.environ.get("LEARNHOUSE_ALLOWED_REGEXP") env_allowed_regexp = os.environ.get("LEARNHOUSE_ALLOWED_REGEXP")
env_self_hosted = os.environ.get("LEARNHOUSE_SELF_HOSTED") env_self_hosted = os.environ.get("LEARNHOUSE_SELF_HOSTED")
env_mongodb_connection_string = os.environ.get( env_sql_connection_string = os.environ.get("LEARNHOUSE_SQL_CONNECTION_STRING")
"LEARNHOUSE_MONGODB_CONNECTION_STRING"
)
# Sentry Config # Sentry Config
env_sentry_dsn = os.environ.get("LEARNHOUSE_SENTRY_DSN") env_sentry_dsn = os.environ.get("LEARNHOUSE_SENTRY_DSN")
@ -166,9 +165,13 @@ def get_learnhouse_config() -> LearnHouseConfig:
) )
# Database config # Database config
mongodb_connection_string = env_mongodb_connection_string or yaml_config.get( sql_connection_string = env_sql_connection_string or yaml_config.get(
"database_config", {} "database_config", {}
).get("mongodb_connection_string") ).get("sql_connection_string")
mongo_connection_string = yaml_config.get("database_config", {}).get(
"mongo_connection_string"
)
# Sentry config # Sentry config
# check if the sentry config is provided in the YAML file # check if the sentry config is provided in the YAML file
@ -210,7 +213,8 @@ def get_learnhouse_config() -> LearnHouseConfig:
content_delivery=content_delivery, content_delivery=content_delivery,
) )
database_config = DatabaseConfig( database_config = DatabaseConfig(
mongodb_connection_string=mongodb_connection_string sql_connection_string=sql_connection_string,
mongo_connection_string=mongo_connection_string,
) )
# Create LearnHouseConfig object # Create LearnHouseConfig object

View file

@ -13,8 +13,8 @@ hosting_config:
domain: learnhouse.app domain: learnhouse.app
ssl: true ssl: true
allowed_origins: allowed_origins:
- http://localhost:3000 - http://localhost:3000
- http://localhost:3001 - http://localhost:3001
cookies_config: cookies_config:
domain: ".localhost" domain: ".localhost"
allowed_regexp: '\b((?:https?://)[^\s/$.?#].[^\s]*)\b' allowed_regexp: '\b((?:https?://)[^\s/$.?#].[^\s]*)\b'
@ -25,4 +25,5 @@ hosting_config:
endpoint_url: "" endpoint_url: ""
database_config: database_config:
mongodb_connection_string: mongodb://learnhouse:learnhouse@mongo:27017/ sql_connection_string: postgresql://learnhouse:learnhouse@db:5432/learnhouse
mongo_connection_string: mongodb://learnhouse:learnhouse@mongo:27017/

View file

@ -1,8 +1,10 @@
fastapi==0.101.1 fastapi==0.104.1
pydantic>=1.8.0,<2.0.0 pydantic>=1.8.0,<2.0.0
sqlmodel==0.0.10
uvicorn==0.23.2 uvicorn==0.23.2
pymongo==4.3.3 pymongo==4.3.3
motor==3.1.1 motor==3.1.1
psycopg2
python-multipart python-multipart
boto3 boto3
botocore botocore

View file

@ -1,21 +1,34 @@
import logging import logging
from config.config import get_learnhouse_config
from fastapi import FastAPI from fastapi import FastAPI
from sqlmodel import SQLModel, Session, create_engine
import motor.motor_asyncio import motor.motor_asyncio
learnhouse_config = get_learnhouse_config()
engine = create_engine(
learnhouse_config.database_config.sql_connection_string, echo=False # type: ignore
)
SQLModel.metadata.create_all(engine)
async def connect_to_db(app: FastAPI): async def connect_to_db(app: FastAPI):
logging.info("Connecting to database...") app.db_engine = engine # type: ignore
try: logging.info("LearnHouse database has been started.")
app.mongodb_client = motor.motor_asyncio.AsyncIOMotorClient( # type: ignore SQLModel.metadata.create_all(engine)
app.learnhouse_config.database_config.mongodb_connection_string) # type: ignore
app.db = app.mongodb_client["learnhouse"] # type: ignore # MongoDB for migration purposes
logging.info("Connected to database!") # mongodb
except Exception as e: app.mongodb_client = motor.motor_asyncio.AsyncIOMotorClient( # type: ignore
logging.error("Failed to connect to database!") app.learnhouse_config.database_config.mongo_connection_string # type: ignore
logging.error(e) ) # type: ignore
app.db = app.mongodb_client["learnhouse"] # type: ignore
def get_db_session():
with Session(engine) as session:
yield session
async def close_database(app: FastAPI): async def close_database(app: FastAPI):
app.mongodb_client.close() # type: ignore
logging.info("LearnHouse has been shut down.") logging.info("LearnHouse has been shut down.")
return app return app

View file

@ -0,0 +1,72 @@
from typing import Optional
from sqlalchemy import JSON, BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel
from enum import Enum
class ActivityTypeEnum(str, Enum):
TYPE_VIDEO = "TYPE_VIDEO"
TYPE_DOCUMENT = "TYPE_DOCUMENT"
TYPE_DYNAMIC = "TYPE_DYNAMIC"
TYPE_ASSESSMENT = "TYPE_ASSESSMENT"
TYPE_CUSTOM = "TYPE_CUSTOM"
class ActivitySubTypeEnum(str, Enum):
# Dynamic
SUBTYPE_DYNAMIC_PAGE = "SUBTYPE_DYNAMIC_PAGE"
# Video
SUBTYPE_VIDEO_YOUTUBE = "SUBTYPE_VIDEO_YOUTUBE"
SUBTYPE_VIDEO_HOSTED = "SUBTYPE_VIDEO_HOSTED"
# Document
SUBTYPE_DOCUMENT_PDF = "SUBTYPE_DOCUMENT_PDF"
SUBTYPE_DOCUMENT_DOC = "SUBTYPE_DOCUMENT_DOC"
# Assessment
SUBTYPE_ASSESSMENT_QUIZ = "SUBTYPE_ASSESSMENT_QUIZ"
# Custom
SUBTYPE_CUSTOM = "SUBTYPE_CUSTOM"
class ActivityBase(SQLModel):
name: str
activity_type: ActivityTypeEnum = ActivityTypeEnum.TYPE_CUSTOM
activity_sub_type: ActivitySubTypeEnum = ActivitySubTypeEnum.SUBTYPE_CUSTOM
content: dict = Field(default={}, sa_column=Column(JSON))
published_version: int
version: int
class Activity(ActivityBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(default=None, foreign_key="organization.id")
course_id: int = Field(
default=None,
sa_column=Column(
BigInteger, ForeignKey("course.id", ondelete="CASCADE")
),
)
activity_uuid: str = ""
creation_date: str = ""
update_date: str = ""
class ActivityCreate(ActivityBase):
chapter_id: int
pass
class ActivityUpdate(ActivityBase):
name: Optional[str]
activity_type: Optional[ActivityTypeEnum]
activity_sub_type: Optional[ActivitySubTypeEnum]
content: dict = Field(default={}, sa_column=Column(JSON))
published_version: Optional[int]
version: Optional[int]
class ActivityRead(ActivityBase):
id: int
activity_uuid: str
creation_date: str
update_date: str
pass

46
apps/api/src/db/blocks.py Normal file
View file

@ -0,0 +1,46 @@
from typing import Optional
from sqlalchemy import JSON, Column, ForeignKey
from sqlmodel import Field, SQLModel
from enum import Enum
class BlockTypeEnum(str, Enum):
BLOCK_QUIZ = "BLOCK_QUIZ"
BLOCK_VIDEO = "BLOCK_VIDEO"
BLOCK_DOCUMENT_PDF = "BLOCK_DOCUMENT_PDF"
BLOCK_IMAGE = "BLOCK_IMAGE"
BLOCK_CUSTOM = "BLOCK_CUSTOM"
class BlockBase(SQLModel):
id: Optional[int] = Field(default=None, primary_key=True)
block_type: BlockTypeEnum = BlockTypeEnum.BLOCK_CUSTOM
content: dict = Field(default={}, sa_column=Column(JSON))
class Block(BlockBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
content: dict = Field(default={}, sa_column=Column(JSON))
org_id: int = Field(default=None, foreign_key="organization.id")
course_id: int = Field(sa_column= Column("course_id", ForeignKey("course.id", ondelete="CASCADE")))
chapter_id: int = Field(sa_column= Column("chapter_id", ForeignKey("chapter.id", ondelete="CASCADE")))
activity_id: int = Field(sa_column= Column("activity_id", ForeignKey("activity.id", ondelete="CASCADE")))
block_uuid: str
creation_date: str
update_date: str
class BlockCreate(BlockBase):
pass
class BlockRead(BlockBase):
id: int
org_id: int = Field(default=None, foreign_key="organization.id")
course_id: int = Field(default=None, foreign_key="course.id")
chapter_id: int = Field(default=None, foreign_key="chapter.id")
activity_id: int = Field(default=None, foreign_key="activity.id")
block_uuid: str
creation_date: str
update_date: str
pass

View file

@ -0,0 +1,13 @@
from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel
class ChapterActivity(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
order: int
chapter_id: int = Field(sa_column=Column(BigInteger, ForeignKey("chapter.id", ondelete="CASCADE")))
activity_id: int = Field(sa_column=Column(BigInteger, ForeignKey("activity.id", ondelete="CASCADE")))
course_id : int = Field(sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE")))
org_id : int = Field(default=None, foreign_key="organization.id")
creation_date: str
update_date: str

View file

@ -0,0 +1,68 @@
from typing import Any, List, Optional
from pydantic import BaseModel
from sqlalchemy import Column, ForeignKey
from sqlmodel import Field, SQLModel
from src.db.activities import ActivityRead
class ChapterBase(SQLModel):
name: str
description: Optional[str] = ""
thumbnail_image: Optional[str] = ""
org_id: int = Field(default=None, foreign_key="organization.id")
course_id: int = Field(
sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE"))
)
class Chapter(ChapterBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
course_id: int = Field(
sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE"))
)
chapter_uuid: str = ""
creation_date: str = ""
update_date: str = ""
class ChapterCreate(ChapterBase):
# referenced order here will be ignored and just used for validation
# used order will be the next available.
pass
class ChapterUpdate(ChapterBase):
name: Optional[str]
description: Optional[str]
thumbnail_image: Optional[str]
course_id: Optional[int]
org_id: Optional[int]
class ChapterRead(ChapterBase):
id: int
activities: List[ActivityRead]
chapter_uuid: str
creation_date: str
update_date: str
pass
class ActivityOrder(BaseModel):
activity_id: int
class ChapterOrder(BaseModel):
chapter_id: int
activities_order_by_ids: List[ActivityOrder]
class ChapterUpdateOrder(BaseModel):
chapter_order_by_ids: List[ChapterOrder]
class DepreceatedChaptersRead(BaseModel):
chapterOrder: Any
chapters: Any
activities: Any
pass

View file

@ -0,0 +1,39 @@
from typing import Optional
from sqlmodel import Field, SQLModel
class CollectionBase(SQLModel):
name: str
public: bool
description: Optional[str] = ""
class Collection(CollectionBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(default=None, foreign_key="organization.id")
collection_uuid: str = ""
creation_date: str = ""
update_date: str = ""
class CollectionCreate(CollectionBase):
courses: list[int]
org_id: int = Field(default=None, foreign_key="organization.id")
pass
class CollectionUpdate(CollectionBase):
courses: Optional[list]
name: Optional[str]
public: Optional[bool]
description: Optional[str]
class CollectionRead(CollectionBase):
id: int
courses: list
collection_uuid: str
creation_date: str
update_date: str
pass

View file

@ -0,0 +1,12 @@
from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel
class CollectionCourse(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
collection_id: int = Field(sa_column=Column(BigInteger, ForeignKey("collection.id", ondelete="CASCADE")))
course_id: int = Field(sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE")))
org_id: int = Field(default=None, foreign_key="organization.id")
creation_date: str
update_date: str

View file

@ -0,0 +1,17 @@
from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel
class CourseChapter(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
order: int
course_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE"))
)
chapter_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("chapter.id", ondelete="CASCADE"))
)
org_id: int = Field(default=None, foreign_key="organization.id")
creation_date: str
update_date: str

View file

@ -0,0 +1,72 @@
from typing import List, Optional
from sqlmodel import Field, SQLModel
from src.db.users import UserRead
from src.db.trails import TrailRead
from src.db.chapters import ChapterRead
class CourseBase(SQLModel):
name: str
description: Optional[str]
about: Optional[str]
learnings: Optional[str]
tags: Optional[str]
thumbnail_image: Optional[str]
public: bool
class Course(CourseBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(default=None, foreign_key="organization.id")
course_uuid: str = ""
creation_date: str = ""
update_date: str = ""
class CourseCreate(CourseBase):
org_id: int = Field(default=None, foreign_key="organization.id")
pass
class CourseUpdate(CourseBase):
name: str
description: Optional[str]
about: Optional[str]
learnings: Optional[str]
tags: Optional[str]
public: Optional[bool]
class CourseRead(CourseBase):
id: int
org_id: int = Field(default=None, foreign_key="organization.id")
authors: List[UserRead]
course_uuid: str
creation_date: str
update_date: str
pass
class FullCourseRead(CourseBase):
id: int
course_uuid: str
creation_date: str
update_date: str
# Chapters, Activities
chapters: List[ChapterRead]
authors: List[UserRead]
pass
class FullCourseReadWithTrail(CourseBase):
id: int
course_uuid: str
creation_date: str
update_date: str
org_id: int = Field(default=None, foreign_key="organization.id")
authors: List[UserRead]
# Chapters, Activities
chapters: List[ChapterRead]
# Trail
trail: TrailRead | None
pass

View file

@ -0,0 +1,31 @@
from typing import Optional
from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel
class InstallBase(SQLModel):
step: int = Field(default=0)
data: dict = Field(default={}, sa_column=Column(JSON))
class Install(InstallBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
install_uuid: str = Field(default=None)
creation_date: str = ""
update_date: str = ""
class InstallCreate(InstallBase):
pass
class InstallUpdate(InstallBase):
pass
class InstallRead(InstallBase):
id: Optional[int] = Field(default=None, primary_key=True)
install_uuid: str = Field(default=None)
creation_date: str
update_date: str
pass

View file

@ -0,0 +1,21 @@
from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel
from enum import Enum
class HeaderTypeEnum(str, Enum):
LOGO_MENU_SETTINGS = "LOGO_MENU_SETTINGS"
MENU_LOGO_SETTINGS = "MENU_LOGO_SETTINGS"
class OrganizationSettings(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("organization.id", ondelete="CASCADE"))
)
logo_image: Optional[str] = ""
header_type: HeaderTypeEnum = HeaderTypeEnum.LOGO_MENU_SETTINGS
color: str = ""
creation_date: str
update_date: str

View file

@ -0,0 +1,30 @@
from typing import Optional
from sqlmodel import Field, SQLModel
class OrganizationBase(SQLModel):
name: str
description: Optional[str]
slug: str
email: str
logo_image: Optional[str]
class Organization(OrganizationBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
org_uuid: str = ""
creation_date: str = ""
update_date: str = ""
class OrganizationUpdate(OrganizationBase):
pass
class OrganizationCreate(OrganizationBase):
pass
class OrganizationRead(OrganizationBase):
id: int
org_uuid: str
creation_date: str
update_date: str

View file

@ -0,0 +1,18 @@
from enum import Enum
from typing import Optional
from sqlmodel import Field, SQLModel
class ResourceAuthorshipEnum(str, Enum):
CREATOR = "CREATOR"
MAINTAINER = "MAINTAINER"
REPORTER = "REPORTER"
class ResourceAuthor(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
resource_uuid: str
user_id: int = Field(default=None, foreign_key="user.id")
authorship: ResourceAuthorshipEnum = ResourceAuthorshipEnum.CREATOR
creation_date: str = ""
update_date: str = ""

72
apps/api/src/db/roles.py Normal file
View file

@ -0,0 +1,72 @@
from enum import Enum
from typing import Optional, Union
from pydantic import BaseModel
from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel
# Rights
class Permission(BaseModel):
action_create: bool
action_read: bool
action_update: bool
action_delete: bool
def __getitem__(self, item):
return getattr(self, item)
class Rights(BaseModel):
courses: Permission
users: Permission
collections: Permission
organizations: Permission
coursechapters: Permission
activities: Permission
def __getitem__(self, item):
return getattr(self, item)
# Database Models
class RoleTypeEnum(str, Enum):
TYPE_ORGANIZATION = "TYPE_ORGANIZATION" # Organization roles are associated with an organization, they are used to define the rights of a user in an organization
TYPE_ORGANIZATION_API_TOKEN = "TYPE_ORGANIZATION_API_TOKEN" # Organization API Token roles are associated with an organization, they are used to define the rights of an API Token in an organization
TYPE_GLOBAL = "TYPE_GLOBAL" # Global roles are not associated with an organization, they are used to define the default rights of a user
class RoleBase(SQLModel):
name: str
description: Optional[str]
rights: Optional[Union[Rights, dict]] = Field(default={}, sa_column=Column(JSON))
class Role(RoleBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(default=None, foreign_key="organization.id")
role_type: RoleTypeEnum = RoleTypeEnum.TYPE_GLOBAL
role_uuid: str = ""
creation_date: str = ""
update_date: str = ""
class RoleRead(RoleBase):
id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(default=None, foreign_key="organization.id")
role_type: RoleTypeEnum = RoleTypeEnum.TYPE_GLOBAL
role_uuid: str
creation_date: str
update_date: str
class RoleCreate(RoleBase):
org_id: Optional[int] = Field(default=None, foreign_key="organization.id")
class RoleUpdate(SQLModel):
role_id: int = Field(default=None, foreign_key="role.id")
name: Optional[str]
description: Optional[str]
rights: Optional[Union[Rights, dict]] = Field(default={}, sa_column=Column(JSON))

View file

@ -0,0 +1,57 @@
from typing import Optional
from pydantic import BaseModel
from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel
from enum import Enum
from src.db.trail_steps import TrailStep
class TrailRunEnum(str, Enum):
RUN_TYPE_COURSE = "RUN_TYPE_COURSE"
class StatusEnum(str, Enum):
STATUS_IN_PROGRESS = "STATUS_IN_PROGRESS"
STATUS_COMPLETED = "STATUS_COMPLETED"
STATUS_PAUSED = "STATUS_PAUSED"
STATUS_CANCELLED = "STATUS_CANCELLED"
class TrailRun(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
data: dict = Field(default={}, sa_column=Column(JSON))
status: StatusEnum = StatusEnum.STATUS_IN_PROGRESS
# foreign keys
trail_id: int = Field(default=None, foreign_key="trail.id")
course_id: int = Field(default=None, foreign_key="course.id")
org_id: int = Field(default=None, foreign_key="organization.id")
user_id: int = Field(default=None, foreign_key="user.id")
# timestamps
creation_date: str
update_date: str
class TrailRunCreate(TrailRun):
pass
# trick because Lists are not supported in SQLModel (runs: list[TrailStep] )
class TrailRunRead(BaseModel):
id: Optional[int] = Field(default=None, primary_key=True)
data: dict = Field(default={}, sa_column=Column(JSON))
status: StatusEnum = StatusEnum.STATUS_IN_PROGRESS
# foreign keys
trail_id: int = Field(default=None, foreign_key="trail.id")
course_id: int = Field(default=None, foreign_key="course.id")
org_id: int = Field(default=None, foreign_key="organization.id")
user_id: int = Field(default=None, foreign_key="user.id")
# course object
course: dict
# timestamps
creation_date: str
update_date: str
# number of activities in course
course_total_steps: int
steps: list[TrailStep]
pass

View file

@ -0,0 +1,34 @@
from enum import Enum
from typing import Optional
from sqlmodel import Field, SQLModel
from sqlalchemy import BigInteger, ForeignKey, JSON, Column
class TrailStepTypeEnum(str, Enum):
STEP_TYPE_READABLE_ACTIVITY = "STEP_TYPE_READABLE_ACTIVITY"
STEP_TYPE_ASSIGNMENT_ACTIVITY = "STEP_TYPE_ASSIGNMENT_ACTIVITY"
STEP_TYPE_CUSTOM_ACTIVITY = "STEP_TYPE_CUSTOM_ACTIVITY"
class TrailStep(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
complete: bool
teacher_verified: bool
grade: str
data: dict = Field(default={}, sa_column=Column(JSON))
# foreign keys
trailrun_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("trailrun.id", ondelete="CASCADE"))
)
trail_id: int = Field(default=None, foreign_key="trail.id")
activity_id: int = Field(default=None, foreign_key="activity.id")
course_id: int = Field(default=None, foreign_key="course.id")
org_id: int = Field(default=None, foreign_key="organization.id")
user_id: int = Field(default=None, foreign_key="user.id")
# timestamps
creation_date: str
update_date: str
# note : prepare assignments support
# an assignment object will be linked to a trail step object in the future

34
apps/api/src/db/trails.py Normal file
View file

@ -0,0 +1,34 @@
from typing import Optional
from pydantic import BaseModel
from sqlmodel import Field, SQLModel
from src.db.trail_runs import TrailRunRead
class TrailBase(SQLModel):
org_id: int = Field(default=None, foreign_key="organization.id")
user_id: int = Field(default=None, foreign_key="user.id")
class Trail(TrailBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
trail_uuid: str = ""
creation_date: str = ""
update_date: str = ""
class TrailCreate(TrailBase):
pass
# 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
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
runs: list[TrailRunRead]
class Config:
orm_mode = True

View file

@ -0,0 +1,14 @@
from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel
class UserOrganization(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
user_id: int = Field(default=None, foreign_key="user.id")
org_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("organization.id", ondelete="CASCADE"))
)
role_id: int = Field(default=None, foreign_key="role.id")
creation_date: str
update_date: str

50
apps/api/src/db/users.py Normal file
View file

@ -0,0 +1,50 @@
from typing import Optional
from sqlmodel import Field, SQLModel
class UserBase(SQLModel):
username: str
first_name: str
last_name: str
email: str
avatar_image: Optional[str] = ""
bio: Optional[str] = ""
class UserCreate(UserBase):
password: str
class UserUpdate(UserBase):
username: str
first_name: Optional[str]
last_name: Optional[str]
email: str
avatar_image: Optional[str] = ""
bio: Optional[str] = ""
class UserUpdatePassword(SQLModel):
old_password: str
new_password: str
class UserRead(UserBase):
id: int
user_uuid: str
class PublicUser(UserRead):
pass
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 = ""
user_uuid: str = ""
email_verified: bool = False
creation_date: str = ""
update_date: str = ""

View file

@ -35,3 +35,4 @@ v1_router.include_router(
tags=["install"], tags=["install"],
dependencies=[Depends(isInstallModeEnabled)], dependencies=[Depends(isInstallModeEnabled)],
) )

View file

@ -1,15 +1,17 @@
from fastapi import Depends, APIRouter, HTTPException, Response, status, Request from fastapi import Depends, APIRouter, HTTPException, Response, status, Request
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from sqlmodel import Session
from src.db.users import UserRead
from src.core.events.database import get_db_session
from config.config import get_learnhouse_config from config.config import get_learnhouse_config
from src.security.auth import AuthJWT, authenticate_user from src.security.auth import AuthJWT, authenticate_user
from src.services.users.users import PublicUser
router = APIRouter() router = APIRouter()
@router.post("/refresh") @router.post("/refresh")
def refresh(response: Response,Authorize: AuthJWT = Depends()): def refresh(response: Response, Authorize: AuthJWT = Depends()):
""" """
The jwt_refresh_token_required() function insures a valid refresh The jwt_refresh_token_required() function insures a valid refresh
token is present in the request before running any code below that function. token is present in the request before running any code below that function.
@ -21,7 +23,12 @@ def refresh(response: Response,Authorize: AuthJWT = Depends()):
current_user = Authorize.get_jwt_subject() current_user = Authorize.get_jwt_subject()
new_access_token = Authorize.create_access_token(subject=current_user) # type: ignore new_access_token = Authorize.create_access_token(subject=current_user) # type: ignore
response.set_cookie(key="access_token_cookie", value=new_access_token, httponly=False, domain=get_learnhouse_config().hosting_config.cookie_config.domain) response.set_cookie(
key="access_token_cookie",
value=new_access_token,
httponly=False,
domain=get_learnhouse_config().hosting_config.cookie_config.domain,
)
return {"access_token": new_access_token} return {"access_token": new_access_token}
@ -31,8 +38,11 @@ async def login(
response: Response, response: Response,
Authorize: AuthJWT = Depends(), Authorize: AuthJWT = Depends(),
form_data: OAuth2PasswordRequestForm = Depends(), form_data: OAuth2PasswordRequestForm = Depends(),
db_session: Session = Depends(get_db_session),
): ):
user = await authenticate_user(request, form_data.username, form_data.password) user = await authenticate_user(
request, form_data.username, form_data.password, db_session
)
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@ -44,8 +54,14 @@ async def login(
refresh_token = Authorize.create_refresh_token(subject=form_data.username) refresh_token = Authorize.create_refresh_token(subject=form_data.username)
Authorize.set_refresh_cookies(refresh_token) Authorize.set_refresh_cookies(refresh_token)
# set cookies using fastapi # set cookies using fastapi
response.set_cookie(key="access_token_cookie", value=access_token, httponly=False, domain=get_learnhouse_config().hosting_config.cookie_config.domain) response.set_cookie(
user = PublicUser(**user.dict()) key="access_token_cookie",
value=access_token,
httponly=False,
domain=get_learnhouse_config().hosting_config.cookie_config.domain,
)
user = UserRead.from_orm(user)
result = { result = {
"user": user, "user": user,

View file

@ -1,9 +1,20 @@
from fastapi import APIRouter, Depends, UploadFile, Form, Request from fastapi import APIRouter, Depends, UploadFile, Form, Request
from src.db.blocks import BlockRead
from src.core.events.database import get_db_session
from src.security.auth import get_current_user from src.security.auth import get_current_user
from src.services.blocks.block_types.imageBlock.images import create_image_block, get_image_block from src.services.blocks.block_types.imageBlock.imageBlock import (
from src.services.blocks.block_types.videoBlock.videoBlock import create_video_block, get_video_block create_image_block,
from src.services.blocks.block_types.pdfBlock.pdfBlock import create_pdf_block, get_pdf_block get_image_block,
from src.services.blocks.block_types.quizBlock.quizBlock import create_quiz_block, get_quiz_block_answers, get_quiz_block_options, quizBlock )
from src.services.blocks.block_types.videoBlock.videoBlock import (
create_video_block,
get_video_block,
)
from src.services.blocks.block_types.pdfBlock.pdfBlock import (
create_pdf_block,
get_pdf_block,
)
from src.services.users.users import PublicUser from src.services.users.users import PublicUser
router = APIRouter() router = APIRouter()
@ -12,83 +23,93 @@ router = APIRouter()
# Image Block # Image Block
#################### ####################
@router.post("/image") @router.post("/image")
async def api_create_image_file_block(request: Request, file_object: UploadFile, activity_id: str = Form(), current_user: PublicUser = Depends(get_current_user)): async def api_create_image_file_block(
request: Request,
file_object: UploadFile,
activity_uuid: str = Form(),
db_session=Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> BlockRead:
""" """
Create new image file Create new image file
""" """
return await create_image_block(request, file_object, activity_id) return await create_image_block(request, file_object, activity_uuid, db_session)
@router.get("/image") @router.get("/image")
async def api_get_image_file_block(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_get_image_file_block(
request: Request,
block_uuid: str,
db_session=Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> BlockRead:
""" """
Get image file Get image file
""" """
return await get_image_block(request, file_id, current_user) return await get_image_block(request, block_uuid, current_user, db_session)
#################### ####################
# Video Block # Video Block
#################### ####################
@router.post("/video") @router.post("/video")
async def api_create_video_file_block(request: Request, file_object: UploadFile, activity_id: str = Form(), current_user: PublicUser = Depends(get_current_user)): async def api_create_video_file_block(
request: Request,
file_object: UploadFile,
activity_uuid: str = Form(),
db_session=Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> BlockRead:
""" """
Create new video file Create new video file
""" """
return await create_video_block(request, file_object, activity_id) return await create_video_block(request, file_object, activity_uuid, db_session)
@router.get("/video") @router.get("/video")
async def api_get_video_file_block(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_get_video_file_block(
request: Request,
block_uuid: str,
db_session=Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> BlockRead:
""" """
Get video file Get video file
""" """
return await get_video_block(request, file_id, current_user) return await get_video_block(request, block_uuid, current_user, db_session)
#################### ####################
# PDF Block # PDF Block
#################### ####################
@router.post("/pdf") @router.post("/pdf")
async def api_create_pdf_file_block(request: Request, file_object: UploadFile, activity_id: str = Form(), current_user: PublicUser = Depends(get_current_user)): async def api_create_pdf_file_block(
request: Request,
file_object: UploadFile,
activity_uuid: str = Form(),
db_session=Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> BlockRead:
""" """
Create new pdf file Create new pdf file
""" """
return await create_pdf_block(request, file_object, activity_id) return await create_pdf_block(request, file_object, activity_uuid, db_session)
@router.get("/pdf") @router.get("/pdf")
async def api_get_pdf_file_block(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_get_pdf_file_block(
request: Request,
block_uuid: str,
db_session=Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> BlockRead:
""" """
Get pdf file Get pdf file
""" """
return await get_pdf_block(request, file_id, current_user) return await get_pdf_block(request, block_uuid, current_user, db_session)
####################
# Quiz Block
####################
@router.post("/quiz/{activity_id}")
async def api_create_quiz_block(request: Request, quiz_block: quizBlock, activity_id: str, current_user: PublicUser = Depends(get_current_user)):
"""
Create new document file
"""
return await create_quiz_block(request, quiz_block, activity_id, current_user)
@router.get("/quiz/options")
async def api_get_quiz_options(request: Request, block_id: str, current_user: PublicUser = Depends(get_current_user)):
"""
Get quiz options
"""
return await get_quiz_block_options(request, block_id, current_user)
@router.get("/quiz/answers")
async def api_get_quiz_answers(request: Request, block_id: str, current_user: PublicUser = Depends(get_current_user)):
"""
Get quiz answers
"""
return await get_quiz_block_answers(request, block_id, current_user)

View file

@ -1,6 +1,9 @@
from typing import List
from fastapi import APIRouter, Depends, UploadFile, Form, Request from fastapi import APIRouter, Depends, UploadFile, Form, Request
from src.db.activities import ActivityCreate, ActivityRead, ActivityUpdate
from src.db.users import PublicUser
from src.core.events.database import get_db_session
from src.services.courses.activities.activities import ( from src.services.courses.activities.activities import (
Activity,
create_activity, create_activity,
get_activity, get_activity,
get_activities, get_activities,
@ -14,7 +17,6 @@ from src.services.courses.activities.video import (
create_external_video_activity, create_external_video_activity,
create_video_activity, create_video_activity,
) )
from src.services.users.schemas.users import PublicUser
router = APIRouter() router = APIRouter()
@ -22,17 +24,14 @@ router = APIRouter()
@router.post("/") @router.post("/")
async def api_create_activity( async def api_create_activity(
request: Request, request: Request,
activity_object: Activity, activity_object: ActivityCreate,
org_id: str,
coursechapter_id: str,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
): db_session=Depends(get_db_session),
) -> ActivityRead:
""" """
Create new activity Create new activity
""" """
return await create_activity( return await create_activity(request, activity_object, current_user, db_session)
request, activity_object, org_id, coursechapter_id, current_user
)
@router.get("/{activity_id}") @router.get("/{activity_id}")
@ -40,36 +39,43 @@ async def api_get_activity(
request: Request, request: Request,
activity_id: str, activity_id: str,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
): db_session=Depends(get_db_session),
) -> ActivityRead:
""" """
Get single activity by activity_id Get single activity by activity_id
""" """
return await get_activity(request, activity_id, current_user=current_user) return await get_activity(
request, activity_id, current_user=current_user, db_session=db_session
)
@router.get("/coursechapter/{coursechapter_id}") @router.get("/chapter/{chapter_id}")
async def api_get_activities( async def api_get_chapter_activities(
request: Request, request: Request,
coursechapter_id: str, chapter_id: int,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
): db_session=Depends(get_db_session),
) -> List[ActivityRead]:
""" """
Get CourseChapter activities Get Activities for a chapter
""" """
return await get_activities(request, coursechapter_id, current_user) return await get_activities(request, chapter_id, current_user, db_session)
@router.put("/{activity_id}") @router.put("/{activity_uuid}")
async def api_update_activity( async def api_update_activity(
request: Request, request: Request,
activity_object: Activity, activity_object: ActivityUpdate,
activity_id: str, activity_uuid: str,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
): db_session=Depends(get_db_session),
) -> ActivityRead:
""" """
Update activity by activity_id Update activity by activity_id
""" """
return await update_activity(request, activity_object, activity_id, current_user) return await update_activity(
request, activity_object, activity_uuid, current_user, db_session
)
@router.delete("/{activity_id}") @router.delete("/{activity_id}")
@ -77,11 +83,12 @@ async def api_delete_activity(
request: Request, request: Request,
activity_id: str, activity_id: str,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
): ):
""" """
Delete activity by activity_id Delete activity by activity_id
""" """
return await delete_activity(request, activity_id, current_user) return await delete_activity(request, activity_id, current_user, db_session)
# Video activity # Video activity
@ -91,15 +98,21 @@ async def api_delete_activity(
async def api_create_video_activity( async def api_create_video_activity(
request: Request, request: Request,
name: str = Form(), name: str = Form(),
coursechapter_id: str = Form(), chapter_id: str = Form(),
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
video_file: UploadFile | None = None, video_file: UploadFile | None = None,
): db_session=Depends(get_db_session),
) -> ActivityRead:
""" """
Create new activity Create new activity
""" """
return await create_video_activity( return await create_video_activity(
request, name, coursechapter_id, current_user, video_file request,
name,
chapter_id,
current_user,
db_session,
video_file,
) )
@ -108,24 +121,28 @@ async def api_create_external_video_activity(
request: Request, request: Request,
external_video: ExternalVideo, external_video: ExternalVideo,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
): db_session=Depends(get_db_session),
) -> ActivityRead:
""" """
Create new activity Create new activity
""" """
return await create_external_video_activity(request, current_user, external_video) return await create_external_video_activity(
request, current_user, external_video, db_session
)
@router.post("/documentpdf") @router.post("/documentpdf")
async def api_create_documentpdf_activity( async def api_create_documentpdf_activity(
request: Request, request: Request,
name: str = Form(), name: str = Form(),
coursechapter_id: str = Form(), chapter_id: str = Form(),
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
pdf_file: UploadFile | None = None, pdf_file: UploadFile | None = None,
): db_session=Depends(get_db_session),
) -> ActivityRead:
""" """
Create new activity Create new activity
""" """
return await create_documentpdf_activity( return await create_documentpdf_activity(
request, name, coursechapter_id, current_user, pdf_file request, name, chapter_id, current_user, db_session, pdf_file
) )

View file

@ -1,6 +1,22 @@
from typing import List
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from src.core.events.database import get_db_session
from src.db.chapters import (
ChapterCreate,
ChapterRead,
ChapterUpdate,
ChapterUpdateOrder,
)
from src.services.courses.chapters import (
DEPRECEATED_get_course_chapters,
create_chapter,
delete_chapter,
get_chapter,
get_course_chapters,
reorder_chapters_and_activities,
update_chapter,
)
from src.services.courses.chapters import CourseChapter, CourseChapterMetaData, create_coursechapter, delete_coursechapter, get_coursechapter, get_coursechapters, get_coursechapters_meta, update_coursechapter, update_coursechapters_meta
from src.services.users.users import PublicUser from src.services.users.users import PublicUser
from src.security.auth import get_current_user from src.security.auth import get_current_user
@ -8,57 +24,104 @@ router = APIRouter()
@router.post("/") @router.post("/")
async def api_create_coursechapter(request: Request,coursechapter_object: CourseChapter, course_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_create_coursechapter(
request: Request,
coursechapter_object: ChapterCreate,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
) -> ChapterRead:
""" """
Create new CourseChapter Create new Course Chapter
""" """
return await create_coursechapter(request, coursechapter_object, course_id, current_user) return await create_chapter(request, coursechapter_object, current_user, db_session)
@router.get("/{coursechapter_id}") @router.get("/{chapter_id}")
async def api_get_coursechapter(request: Request,coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_get_coursechapter(
request: Request,
chapter_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
) -> ChapterRead:
""" """
Get single CourseChapter by coursechapter_id Get single CourseChapter by chapter_id
""" """
return await get_coursechapter(request, coursechapter_id, current_user=current_user) return await get_chapter(request, chapter_id, current_user, db_session)
@router.get("/meta/{course_id}") @router.get("/course/{course_uuid}/meta", deprecated=True)
async def api_get_coursechapter_meta(request: Request,course_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_get_chapter_meta(
request: Request,
course_uuid: str,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
""" """
Get coursechapter metadata Get Chapters metadata
""" """
return await get_coursechapters_meta(request, course_id, current_user=current_user) return await DEPRECEATED_get_course_chapters(
request, course_uuid, current_user, db_session
)
@router.put("/meta/{course_id}") @router.put("/course/{course_uuid}/order")
async def api_update_coursechapter_meta(request: Request,course_id: str, coursechapters_metadata: CourseChapterMetaData, current_user: PublicUser = Depends(get_current_user)): async def api_update_chapter_meta(
request: Request,
course_uuid: str,
order: ChapterUpdateOrder,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
""" """
Update coursechapter metadata Update Chapter metadata
""" """
return await update_coursechapters_meta(request, course_id, coursechapters_metadata, current_user=current_user) return await reorder_chapters_and_activities(
request, course_uuid, order, current_user, db_session
)
@router.get("/{course_id}/page/{page}/limit/{limit}") @router.get("/course/{course_id}/page/{page}/limit/{limit}")
async def api_get_coursechapter_by(request: Request,course_id: str, page: int, limit: int): async def api_get_chapter_by(
request: Request,
course_id: int,
page: int,
limit: int,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
) -> List[ChapterRead]:
""" """
Get CourseChapters by page and limit Get Course Chapters by page and limit
""" """
return await get_coursechapters(request, course_id, page, limit) return await get_course_chapters(
request, course_id, db_session, current_user, page, limit
)
@router.put("/{coursechapter_id}") @router.put("/{chapter_id}")
async def api_update_coursechapter(request: Request,coursechapter_object: CourseChapter, coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_update_coursechapter(
request: Request,
coursechapter_object: ChapterUpdate,
chapter_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
) -> ChapterRead:
""" """
Update CourseChapters by course_id Update CourseChapters by course_id
""" """
return await update_coursechapter(request, coursechapter_object, coursechapter_id, current_user) return await update_chapter(
request, coursechapter_object, chapter_id, current_user, db_session
)
@router.delete("/{coursechapter_id}") @router.delete("/{chapter_id}")
async def api_delete_coursechapter(request: Request,coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_delete_coursechapter(
request: Request,
chapter_id: str,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
""" """
Delete CourseChapters by ID Delete CourseChapters by ID
""" """
return await delete_coursechapter(request,coursechapter_id, current_user) return await delete_chapter(request, chapter_id, current_user, db_session)

View file

@ -1,8 +1,10 @@
from typing import List
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from src.core.events.database import get_db_session
from src.db.collections import CollectionCreate, CollectionRead, CollectionUpdate
from src.security.auth import get_current_user from src.security.auth import get_current_user
from src.services.users.users import PublicUser from src.services.users.users import PublicUser
from src.services.courses.collections import ( from src.services.courses.collections import (
Collection,
create_collection, create_collection,
get_collection, get_collection,
get_collections, get_collections,
@ -17,64 +19,69 @@ router = APIRouter()
@router.post("/") @router.post("/")
async def api_create_collection( async def api_create_collection(
request: Request, request: Request,
collection_object: Collection, collection_object: CollectionCreate,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
): db_session=Depends(get_db_session),
) -> CollectionRead:
""" """
Create new Collection Create new Collection
""" """
return await create_collection(request, collection_object, current_user) return await create_collection(request, collection_object, current_user, db_session)
@router.get("/{collection_id}") @router.get("/{collection_uuid}")
async def api_get_collection( async def api_get_collection(
request: Request, request: Request,
collection_id: str, collection_uuid: str,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
): db_session=Depends(get_db_session),
) -> CollectionRead:
""" """
Get single collection by ID Get single collection by ID
""" """
return await get_collection(request, collection_id, current_user) return await get_collection(request, collection_uuid, current_user, db_session)
@router.get("/org_id/{org_id}/page/{page}/limit/{limit}") @router.get("/org/{org_id}/page/{page}/limit/{limit}")
async def api_get_collections_by( async def api_get_collections_by(
request: Request, request: Request,
page: int, page: int,
limit: int, limit: int,
org_id: str, org_id: str,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
): db_session=Depends(get_db_session),
) -> List[CollectionRead]:
""" """
Get collections by page and limit Get collections by page and limit
""" """
return await get_collections(request, org_id, current_user, page, limit) return await get_collections(request, org_id, current_user, db_session, page, limit)
@router.put("/{collection_id}") @router.put("/{collection_uuid}")
async def api_update_collection( async def api_update_collection(
request: Request, request: Request,
collection_object: Collection, collection_object: CollectionUpdate,
collection_id: str, collection_uuid: str,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
): db_session=Depends(get_db_session),
) -> CollectionRead:
""" """
Update collection by ID Update collection by ID
""" """
return await update_collection( return await update_collection(
request, collection_object, collection_id, current_user request, collection_object, collection_uuid, current_user, db_session
) )
@router.delete("/{collection_id}") @router.delete("/{collection_uuid}")
async def api_delete_collection( async def api_delete_collection(
request: Request, request: Request,
collection_id: str, collection_uuid: str,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
): ):
""" """
Delete collection by ID Delete collection by ID
""" """
return await delete_collection(request, collection_id, current_user) return await delete_collection(request, collection_uuid, current_user, db_session)

View file

@ -1,66 +1,147 @@
from typing import List
from fastapi import APIRouter, Depends, UploadFile, Form, Request from fastapi import APIRouter, Depends, UploadFile, Form, Request
from sqlmodel import Session
from src.core.events.database import get_db_session
from src.db.users import PublicUser
from src.db.courses import (
CourseCreate,
CourseRead,
CourseUpdate,
FullCourseReadWithTrail,
)
from src.security.auth import get_current_user from src.security.auth import get_current_user
from src.services.courses.courses import (
from src.services.courses.courses import Course, create_course, get_course, get_course_meta, get_courses_orgslug, update_course, delete_course, update_course_thumbnail create_course,
from src.services.users.users import PublicUser get_course,
get_course_meta,
get_courses_orgslug,
update_course,
delete_course,
update_course_thumbnail,
)
router = APIRouter() router = APIRouter()
@router.post("/") @router.post("/")
async def api_create_course(request: Request, org_id: str, name: str = Form(), mini_description: str = Form(), description: str = Form(), public: bool = Form(), current_user: PublicUser = Depends(get_current_user), thumbnail: UploadFile | None = None): async def api_create_course(
request: Request,
org_id: int,
name: str = Form(),
description: str = Form(),
public: bool = Form(),
learnings: str = Form(),
tags: str = Form(),
about: str = Form(),
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
thumbnail: UploadFile | None = None,
) -> CourseRead:
""" """
Create new Course Create new Course
""" """
course = Course(name=name, mini_description=mini_description, description=description, course = CourseCreate(
org_id=org_id, public=public, thumbnail="", chapters=[], chapters_content=[], learnings=[]) name=name,
return await create_course(request, course, org_id, current_user, thumbnail) description=description,
org_id=org_id,
public=public,
thumbnail_image="",
about=about,
learnings=learnings,
tags=tags,
)
return await create_course(request, org_id, course, current_user, db_session, thumbnail)
@router.put("/thumbnail/{course_id}") @router.put("/{course_uuid}/thumbnail")
async def api_create_course_thumbnail(request: Request, course_id: str, thumbnail: UploadFile | None = None, current_user: PublicUser = Depends(get_current_user)): async def api_create_course_thumbnail(
request: Request,
course_uuid: str,
thumbnail: UploadFile | None = None,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> CourseRead:
""" """
Update new Course Thumbnail Update new Course Thumbnail
""" """
return await update_course_thumbnail(request, course_id, current_user, thumbnail) return await update_course_thumbnail(
request, course_uuid, current_user, db_session, thumbnail
)
@router.get("/{course_id}") @router.get("/{course_uuid}")
async def api_get_course(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_get_course(
request: Request,
course_uuid: str,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> CourseRead:
""" """
Get single Course by course_id Get single Course by course_uuid
""" """
return await get_course(request, course_id, current_user=current_user) return await get_course(
request, course_uuid, current_user=current_user, db_session=db_session
)
@router.get("/meta/{course_id}") @router.get("/{course_uuid}/meta")
async def api_get_course_meta(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_get_course_meta(
request: Request,
course_uuid: str,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> FullCourseReadWithTrail:
""" """
Get single Course Metadata (chapters, activities) by course_id Get single Course Metadata (chapters, activities) by course_uuid
""" """
return await get_course_meta(request, course_id, current_user=current_user) return await get_course_meta(
request, course_uuid, current_user=current_user, db_session=db_session
)
@router.get("/org_slug/{org_slug}/page/{page}/limit/{limit}") @router.get("/org_slug/{org_slug}/page/{page}/limit/{limit}")
async def api_get_course_by_orgslug(request: Request, page: int, limit: int, org_slug: str, current_user: PublicUser = Depends(get_current_user)): async def api_get_course_by_orgslug(
request: Request,
page: int,
limit: int,
org_slug: str,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> List[CourseRead]:
""" """
Get houses by page and limit Get courses by page and limit
""" """
return await get_courses_orgslug(request, current_user, page, limit, org_slug) return await get_courses_orgslug(
request, current_user, org_slug, db_session, page, limit
)
@router.put("/{course_id}") @router.put("/{course_uuid}")
async def api_update_course(request: Request, course_object: Course, course_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_update_course(
request: Request,
course_object: CourseUpdate,
course_uuid: str,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> CourseRead:
""" """
Update Course by course_id Update Course by course_uuid
""" """
return await update_course(request, course_object, course_id, current_user) return await update_course(
request, course_object, course_uuid, current_user, db_session
)
@router.delete("/{course_id}") @router.delete("/{course_uuid}")
async def api_delete_course(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_delete_course(
request: Request,
course_uuid: str,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
):
""" """
Delete Course by ID Delete Course by ID
""" """
return await delete_course(request, course_id, current_user) return await delete_course(request, course_uuid, current_user, db_session)

View file

@ -1,6 +1,8 @@
from fastapi import APIRouter, Request from fastapi import APIRouter, Depends, Request
from sqlmodel import Session
from src.core.events.database import get_db_session
from src.services.dev.migration_from_mongo import start_migrate_from_mongo
from config.config import get_learnhouse_config from config.config import get_learnhouse_config
from src.services.dev.mocks.initial import create_initial_data
router = APIRouter() router = APIRouter()
@ -12,7 +14,9 @@ async def config():
return config.dict() return config.dict()
@router.get("/mock/initial") @router.get("/migrate_from_mongo")
async def initial_data(request: Request): async def migrate_from_mongo(
await create_initial_data(request) request: Request,
return {"Message": "Initial data created 🤖"} db_session: Session = Depends(get_db_session),
):
return await start_migrate_from_mongo(request, db_session)

View file

@ -1,70 +1,87 @@
from fastapi import APIRouter, Request from fastapi import APIRouter, Depends, Request
from src.db.install import InstallRead
from src.core.events.database import get_db_session
from src.db.organizations import OrganizationCreate
from src.db.users import UserCreate
from src.services.install.install import ( from src.services.install.install import (
create_install_instance, create_install_instance,
create_sample_data,
get_latest_install_instance, get_latest_install_instance,
install_create_organization, install_create_organization,
install_create_organization_user, install_create_organization_user,
install_default_elements, install_default_elements,
update_install_instance, update_install_instance,
) )
from src.services.orgs.schemas.orgs import Organization
from src.services.users.schemas.users import UserWithPassword
router = APIRouter() router = APIRouter()
@router.post("/start") @router.post("/start")
async def api_create_install_instance(request: Request, data: dict): async def api_create_install_instance(
request: Request,
data: dict,
db_session=Depends(get_db_session),
) -> InstallRead:
# create install # create install
install = await create_install_instance(request, data) install = await create_install_instance(request, data, db_session)
return install return install
@router.get("/latest") @router.get("/latest")
async def api_get_latest_install_instance(request: Request): async def api_get_latest_install_instance(
request: Request, db_session=Depends(get_db_session)
) -> InstallRead:
# get latest created install # get latest created install
install = await get_latest_install_instance(request) install = await get_latest_install_instance(request, db_session=db_session)
return install return install
@router.post("/default_elements") @router.post("/default_elements")
async def api_install_def_elements(request: Request): async def api_install_def_elements(
elements = await install_default_elements(request, {}) request: Request,
db_session=Depends(get_db_session),
):
elements = await install_default_elements(request, {}, db_session)
return elements return elements
@router.post("/org") @router.post("/org")
async def api_install_org(request: Request, org: Organization): async def api_install_org(
organization = await install_create_organization(request, org) request: Request,
org: OrganizationCreate,
db_session=Depends(get_db_session),
):
organization = await install_create_organization(request, org, db_session)
return organization return organization
@router.post("/user") @router.post("/user")
async def api_install_user(request: Request, data: UserWithPassword, org_slug: str): async def api_install_user(
user = await install_create_organization_user(request, data, org_slug) request: Request,
data: UserCreate,
org_slug: str,
db_session=Depends(get_db_session),
):
user = await install_create_organization_user(request, data, org_slug, db_session)
return user return user
@router.post("/sample")
async def api_install_user_sample(request: Request, username: str, org_slug: str):
sample = await create_sample_data(org_slug, username, request)
return sample
@router.post("/update") @router.post("/update")
async def api_update_install_instance(request: Request, data: dict, step: int): async def api_update_install_instance(
request: Request,
data: dict,
step: int,
db_session=Depends(get_db_session),
) -> InstallRead:
request.app.db["installs"] request.app.db["installs"]
# get latest created install # get latest created install
install = await update_install_instance(request, data, step) install = await update_install_instance(request, data, step, db_session)
return install return install

View file

@ -1,63 +1,127 @@
from typing import List
from fastapi import APIRouter, Depends, Request, UploadFile from fastapi import APIRouter, Depends, Request, UploadFile
from sqlmodel import Session
from src.db.users import PublicUser
from src.db.organizations import (
Organization,
OrganizationCreate,
OrganizationRead,
OrganizationUpdate,
)
from src.core.events.database import get_db_session
from src.security.auth import get_current_user from src.security.auth import get_current_user
from src.services.orgs.orgs import Organization, create_org, delete_org, get_organization, get_organization_by_slug, get_orgs_by_user, update_org, update_org_logo from src.services.orgs.orgs import (
from src.services.users.users import PublicUser, User create_org,
delete_org,
get_organization,
get_organization_by_slug,
get_orgs_by_user,
update_org,
update_org_logo,
)
router = APIRouter() router = APIRouter()
@router.post("/") @router.post("/")
async def api_create_org(request: Request, org_object: Organization, current_user: PublicUser = Depends(get_current_user)): async def api_create_org(
request: Request,
org_object: OrganizationCreate,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> OrganizationRead:
""" """
Create new organization Create new organization
""" """
return await create_org(request, org_object, current_user) return await create_org(request, org_object, current_user, db_session)
@router.get("/{org_id}") @router.get("/{org_id}")
async def api_get_org(request: Request, org_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_get_org(
request: Request,
org_id: str,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> OrganizationRead:
""" """
Get single Org by ID Get single Org by ID
""" """
return await get_organization(request, org_id) return await get_organization(request, org_id, db_session, current_user)
@router.get("/slug/{org_slug}") @router.get("/slug/{org_slug}")
async def api_get_org_by_slug(request: Request, org_slug: str, current_user: User = Depends(get_current_user)): async def api_get_org_by_slug(
request: Request,
org_slug: str,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> OrganizationRead:
""" """
Get single Org by Slug Get single Org by Slug
""" """
return await get_organization_by_slug(request, org_slug) return await get_organization_by_slug(request, org_slug, db_session, current_user)
@router.put("/{org_id}/logo") @router.put("/{org_id}/logo")
async def api_update_org_logo(request: Request, org_id: str, logo_file:UploadFile, current_user: PublicUser = Depends(get_current_user)): async def api_update_org_logo(
request: Request,
org_id: str,
logo_file: UploadFile,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
""" """
Get single Org by Slug Get single Org by Slug
""" """
return await update_org_logo(request=request,logo_file=logo_file, org_id=org_id, current_user=current_user) return await update_org_logo(
request=request,
logo_file=logo_file,
org_id=org_id,
current_user=current_user,
db_session=db_session,
)
@router.get("/user/page/{page}/limit/{limit}") @router.get("/user/page/{page}/limit/{limit}")
async def api_user_orgs(request: Request, page: int, limit: int, current_user: PublicUser = Depends(get_current_user)): async def api_user_orgs(
request: Request,
page: int,
limit: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> List[Organization]:
""" """
Get orgs by page and limit by user Get orgs by page and limit by current user
""" """
return await get_orgs_by_user(request, current_user.user_id, page, limit) return await get_orgs_by_user(
request, db_session, str(current_user.id), page, limit
)
@router.put("/{org_id}") @router.put("/{org_id}")
async def api_update_org(request: Request, org_object: Organization, org_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_update_org(
request: Request,
org_object: OrganizationUpdate,
org_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> OrganizationRead:
""" """
Update Org by ID Update Org by ID
""" """
return await update_org(request, org_object, org_id, current_user) return await update_org(request, org_object,org_id, current_user, db_session)
@router.delete("/{org_id}") @router.delete("/{org_id}")
async def api_delete_org(request: Request, org_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_delete_org(
request: Request,
org_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
""" """
Delete Org by ID Delete Org by ID
""" """
return await delete_org(request, org_id, current_user) return await delete_org(request, org_id, current_user, db_session)

View file

@ -1,41 +1,63 @@
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from sqlmodel import Session
from src.core.events.database import get_db_session
from src.db.roles import RoleCreate, RoleRead, RoleUpdate
from src.security.auth import get_current_user from src.security.auth import get_current_user
from src.services.roles.schemas.roles import Role from src.services.roles.roles import create_role, delete_role, read_role, update_role
from src.services.roles.roles import create_role, delete_role, read_role, update_role from src.db.users import PublicUser
from src.services.users.schemas.users import PublicUser
router = APIRouter() router = APIRouter()
@router.post("/") @router.post("/")
async def api_create_role(request: Request, role_object: Role, current_user: PublicUser = Depends(get_current_user)): async def api_create_role(
request: Request,
role_object: RoleCreate,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
)-> RoleRead:
""" """
Create new role Create new role
""" """
return await create_role(request, role_object, current_user) return await create_role(request, db_session, role_object, current_user)
@router.get("/{role_id}") @router.get("/{role_id}")
async def api_get_role(request: Request, role_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_get_role(
request: Request,
role_id: str,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
)-> RoleRead:
""" """
Get single role by role_id Get single role by role_id
""" """
return await read_role(request, role_id, current_user) return await read_role(request, db_session, role_id, current_user)
@router.put("/{role_id}") @router.put("/{role_id}")
async def api_update_role(request: Request, role_object: Role, role_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_update_role(
request: Request,
role_object: RoleUpdate,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
)-> RoleRead:
""" """
Update role by role_id Update role by role_id
""" """
return await update_role(request, role_id, role_object, current_user) return await update_role(request, db_session, role_object, current_user)
@router.delete("/{role_id}") @router.delete("/{role_id}")
async def api_delete_role(request: Request, role_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_delete_role(
request: Request,
role_id: str,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
""" """
Delete role by ID Delete role by ID
""" """
return await delete_role(request, role_id, current_user) return await delete_role(request, db_session, role_id, current_user)

View file

@ -1,56 +1,97 @@
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from src.core.events.database import get_db_session
from src.db.trails import TrailCreate, TrailRead
from src.security.auth import get_current_user from src.security.auth import get_current_user
from src.services.trail.trail import Trail, add_activity_to_trail, add_course_to_trail, create_trail, get_user_trail_with_orgslug, get_user_trail, remove_course_from_trail from src.services.trail.trail import (
Trail,
add_activity_to_trail,
add_course_to_trail,
create_user_trail,
get_user_trails,
get_user_trail_with_orgid,
remove_course_from_trail,
)
router = APIRouter() router = APIRouter()
@router.post("/start") @router.post("/start")
async def api_start_trail(request: Request, trail_object: Trail, org_id: str, user=Depends(get_current_user)) -> Trail: async def api_start_trail(
request: Request,
trail_object: TrailCreate,
user=Depends(get_current_user),
db_session=Depends(get_db_session),
) -> Trail:
""" """
Start trail Start trail
""" """
return await create_trail(request, user, org_id, trail_object) return await create_user_trail(request, user, trail_object, db_session)
@router.get("/org_id/{org_id}/trail") @router.get("/")
async def api_get_trail_by_orgid(request: Request, org_slug: str, user=Depends(get_current_user)): async def api_get_user_trail(
request: Request,
user=Depends(get_current_user),
db_session=Depends(get_db_session),
) -> TrailRead:
""" """
Get a user trails Get a user trails
""" """
return await get_user_trail(request, user=user, org_slug=org_slug) return await get_user_trails(request, user=user, db_session=db_session)
@router.get("/org_slug/{org_slug}/trail") @router.get("/org/{org_id}/trail")
async def api_get_trail_by_orgslug(request: Request, org_slug: str, user=Depends(get_current_user)): async def api_get_trail_by_org_id(
request: Request,
org_id: int,
user=Depends(get_current_user),
db_session=Depends(get_db_session),
) -> TrailRead:
""" """
Get a user trails using org slug Get a user trails using org slug
""" """
return await get_user_trail_with_orgslug(request, user, org_slug=org_slug) return await get_user_trail_with_orgid(
request, user, org_id=org_id, db_session=db_session
# Courses in trail )
@router.post("/org_slug/{org_slug}/add_course/{course_id}") @router.post("/add_course/{course_uuid}")
async def api_add_course_to_trail(request: Request, course_id: str, org_slug: str, user=Depends(get_current_user)): async def api_add_course_to_trail(
request: Request,
course_uuid: str,
user=Depends(get_current_user),
db_session=Depends(get_db_session),
) -> TrailRead:
""" """
Add Course to trail Add Course to trail
""" """
return await add_course_to_trail(request, user, org_slug, course_id) return await add_course_to_trail(request, user, course_uuid, db_session)
@router.post("/org_slug/{org_slug}/remove_course/{course_id}") @router.delete("/remove_course/{course_uuid}")
async def api_remove_course_to_trail(request: Request, course_id: str, org_slug: str, user=Depends(get_current_user)): async def api_remove_course_to_trail(
request: Request,
course_uuid: str,
user=Depends(get_current_user),
db_session=Depends(get_db_session),
) -> TrailRead:
""" """
Remove Course from trail Remove Course from trail
""" """
return await remove_course_from_trail(request, user, org_slug, course_id) return await remove_course_from_trail(request, user, course_uuid, db_session)
@router.post("/org_slug/{org_slug}/add_activity/course_id/{course_id}/activity_id/{activity_id}") @router.post("/add_activity/{activity_uuid}")
async def api_add_activity_to_trail(request: Request, activity_id: str, course_id: str, org_slug: str, user=Depends(get_current_user)): async def api_add_activity_to_trail(
request: Request,
activity_uuid: str,
user=Depends(get_current_user),
db_session=Depends(get_db_session),
) -> TrailRead:
""" """
Add Course to trail Add Course to trail
""" """
return await add_activity_to_trail(request, user, course_id, org_slug, activity_id) return await add_activity_to_trail(
request, user, activity_uuid, db_session
)

View file

@ -1,10 +1,27 @@
from fastapi import Depends, APIRouter, Request from typing import Literal
from fastapi import APIRouter, Depends, Request
from sqlmodel import Session
from src.security.auth import get_current_user from src.security.auth import get_current_user
from src.services.users.schemas.users import PasswordChangeForm, PublicUser, User, UserWithPassword from src.core.events.database import get_db_session
from src.services.users.users import create_user, delete_user, get_profile_metadata, get_user_by_userid, update_user, update_user_password
from src.db.users import (
PublicUser,
User,
UserCreate,
UserRead,
UserUpdate,
UserUpdatePassword,
)
from src.services.users.users import (
authorize_user_action,
create_user,
create_user_without_org,
delete_user_by_id,
read_user_by_id,
read_user_by_uuid,
update_user,
update_user_password,
)
router = APIRouter() router = APIRouter()
@ -17,50 +34,119 @@ async def api_get_current_user(current_user: User = Depends(get_current_user)):
""" """
return current_user.dict() return current_user.dict()
@router.get("/profile_metadata")
async def api_get_current_user_metadata(request: Request,current_user: User = Depends(get_current_user)): @router.get("/authorize/ressource/{ressource_uuid}/action/{action}")
async def api_get_authorization_status(
request: Request,
ressource_uuid: str,
action: Literal["create", "read", "update", "delete"],
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
):
""" """
Get current user Get current user authorization status
""" """
return await get_profile_metadata(request , current_user.dict()) return await authorize_user_action(
request, db_session, current_user, ressource_uuid, action
)
@router.post("/{org_id}", response_model=UserRead, tags=["users"])
@router.get("/user_id/{user_id}") async def api_create_user_with_orgid(
async def api_get_user_by_userid(request: Request,user_id: str): *,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
user_object: UserCreate,
org_id: int,
) -> UserRead:
""" """
Get single user by user_id Create User with Org ID
""" """
return await get_user_by_userid(request, user_id) return await create_user(request, db_session, current_user, user_object, org_id)
@router.post("/") @router.post("/", response_model=UserRead, tags=["users"])
async def api_create_user(request: Request,user_object: UserWithPassword, org_slug: str ): async def api_create_user_without_org(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
user_object: UserCreate,
) -> UserRead:
""" """
Create new user Create User
""" """
return await create_user(request, None, user_object, org_slug) return await create_user_without_org(request, db_session, current_user, user_object)
@router.delete("/user_id/{user_id}") @router.get("/id/{user_id}", response_model=UserRead, tags=["users"])
async def api_delete_user(request: Request, user_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_get_user_by_id(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
user_id: int,
) -> UserRead:
""" """
Delete user by ID Get User by ID
""" """
return await read_user_by_id(request, db_session, current_user, user_id)
return await delete_user(request, current_user, user_id)
@router.put("/user_id/{user_id}") @router.get("/uuid/{user_uuid}", response_model=UserRead, tags=["users"])
async def api_update_user(request: Request, user_object: User, user_id: str, current_user: PublicUser = Depends(get_current_user)): async def api_get_user_by_uuid(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
user_uuid: str,
) -> UserRead:
""" """
Update user by ID Get User by UUID
""" """
return await update_user(request, user_id, user_object, current_user) return await read_user_by_uuid(request, db_session, current_user, user_uuid)
@router.put("/password/user_id/{user_id}")
async def api_update_user_password(request: Request, user_id: str , passwordChangeForm : PasswordChangeForm, current_user: PublicUser = Depends(get_current_user)): @router.put("/{user_id}", response_model=UserRead, tags=["users"])
async def api_update_user(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
user_id: int,
user_object: UserUpdate,
) -> UserRead:
""" """
Update user password by ID Update User
""" """
return await update_user_password(request,current_user, user_id, passwordChangeForm) return await update_user(request, db_session, user_id, current_user, user_object)
@router.put("/change_password/{user_id}", response_model=UserRead, tags=["users"])
async def api_update_user_password(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
user_id: int,
form: UserUpdatePassword,
) -> UserRead:
"""
Update User Password
"""
return await update_user_password(request, db_session, current_user, user_id, form)
@router.delete("/user_id/{user_id}", tags=["users"])
async def api_delete_user(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
user_id: int,
):
"""
Delete User
"""
return await delete_user_by_id(request, db_session, current_user, user_id)

View file

@ -1,3 +1,7 @@
from sqlmodel import Session
from src.core.events.database import get_db_session
from src.db.users import AnonymousUser, PublicUser, User, UserRead
from src.services.users.users import security_get_user
from config.config import get_learnhouse_config from config.config import get_learnhouse_config
from pydantic import BaseModel from pydantic import BaseModel
from fastapi import Depends, HTTPException, Request, status from fastapi import Depends, HTTPException, Request, status
@ -5,8 +9,7 @@ from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt from jose import JWTError, jwt
from datetime import datetime, timedelta from datetime import datetime, timedelta
from src.services.dev.dev import isDevModeEnabled from src.services.dev.dev import isDevModeEnabled
from src.services.users.schemas.users import AnonymousUser, PublicUser from src.services.users.users import security_verify_password
from src.services.users.users import security_get_user, security_verify_password
from src.security.security import ALGORITHM, SECRET_KEY from src.security.security import ALGORITHM, SECRET_KEY
from fastapi_jwt_auth import AuthJWT from fastapi_jwt_auth import AuthJWT
@ -45,10 +48,13 @@ class TokenData(BaseModel):
#### Classes #################################################### #### Classes ####################################################
async def authenticate_user(
request: Request,
async def authenticate_user(request: Request, email: str, password: str): email: str,
user = await security_get_user(request, email) password: str,
db_session: Session,
) -> User | bool:
user = await security_get_user(request, db_session, email)
if not user: if not user:
return False return False
if not await security_verify_password(password, user.password): if not await security_verify_password(password, user.password):
@ -67,7 +73,11 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None):
return encoded_jwt return encoded_jwt
async def get_current_user(request: Request, Authorize: AuthJWT = Depends()): async def get_current_user(
request: Request,
Authorize: AuthJWT = Depends(),
db_session: Session = Depends(get_db_session),
):
credentials_exception = HTTPException( credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials", detail="Could not validate credentials",
@ -81,7 +91,7 @@ async def get_current_user(request: Request, Authorize: AuthJWT = Depends()):
except JWTError: except JWTError:
raise credentials_exception raise credentials_exception
if username: if username:
user = await security_get_user(request, email=token_data.username) # type: ignore # treated as an email user = await security_get_user(request, db_session, email=token_data.username) # type: ignore # treated as an email
if user is None: if user is None:
raise credentials_exception raise credentials_exception
return PublicUser(**user.dict()) return PublicUser(**user.dict())
@ -89,6 +99,6 @@ async def get_current_user(request: Request, Authorize: AuthJWT = Depends()):
return AnonymousUser() return AnonymousUser()
async def non_public_endpoint(current_user: PublicUser): async def non_public_endpoint(current_user: UserRead | AnonymousUser):
if isinstance(current_user, AnonymousUser): if isinstance(current_user, AnonymousUser):
raise HTTPException(status_code=401, detail="Not authenticated") raise HTTPException(status_code=401, detail="Not authenticated")

View file

@ -1,29 +1,31 @@
from typing import Literal from typing import Literal
from fastapi import HTTPException, status, Request from fastapi import HTTPException, status, Request
from src.security.rbac.utils import check_element_type, get_id_identifier_of_element from sqlalchemy import null
from src.services.roles.schemas.roles import RoleInDB from sqlmodel import Session, select
from src.services.users.schemas.users import UserRolesInOrganization from src.db.collections import Collection
from src.db.courses import Course
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
from src.db.roles import Role
from src.db.user_organizations import UserOrganization
from src.security.rbac.utils import check_element_type
# Tested and working
async def authorization_verify_if_element_is_public( async def authorization_verify_if_element_is_public(
request, request,
element_id: str, element_uuid: str,
user_id: str,
action: Literal["read"], action: Literal["read"],
db_session: Session,
): ):
element_nature = await check_element_type(element_id) element_nature = await check_element_type(element_uuid)
# Verifies if the element is public # Verifies if the element is public
if ( if element_nature == ("courses" or "collections") and action == "read":
element_nature == ("courses" or "collections")
and action == "read"
and user_id == "anonymous"
):
if element_nature == "courses": if element_nature == "courses":
courses = request.app.db["courses"] statement = select(Course).where(
course = await courses.find_one({"course_id": element_id}) Course.public is True, Course.course_uuid == element_uuid
)
if course["public"]: course = db_session.exec(statement).first()
if course:
return True return True
else: else:
raise HTTPException( raise HTTPException(
@ -32,10 +34,12 @@ async def authorization_verify_if_element_is_public(
) )
if element_nature == "collections": if element_nature == "collections":
collections = request.app.db["collections"] statement = select(Collection).where(
collection = await collections.find_one({"collection_id": element_id}) Collection.public is True, Collection.collection_uuid == element_uuid
)
collection = db_session.exec(statement).first()
if collection["public"]: if collection:
return True return True
else: else:
raise HTTPException( raise HTTPException(
@ -49,87 +53,81 @@ async def authorization_verify_if_element_is_public(
) )
# Tested and working
async def authorization_verify_if_user_is_author( async def authorization_verify_if_user_is_author(
request, request,
user_id: str, user_id: int,
action: Literal["read", "update", "delete", "create"], action: Literal["read", "update", "delete", "create"],
element_id: str, element_uuid: str,
db_session: Session,
): ):
if action == "update" or "delete" or "read": if action == "update" or "delete" or "read":
element_nature = await check_element_type(element_id) statement = select(ResourceAuthor).where(
elements = request.app.db[element_nature] ResourceAuthor.resource_uuid == element_uuid
element_identifier = await get_id_identifier_of_element(element_id) )
element = await elements.find_one({element_identifier: element_id}) resource_author = db_session.exec(statement).first()
if user_id in element["authors"]:
return True if resource_author:
if resource_author.user_id == int(user_id):
if (resource_author.authorship == ResourceAuthorshipEnum.CREATOR) or (
resource_author.authorship == ResourceAuthorshipEnum.MAINTAINER
):
return True
else:
return False
else:
return False
else: else:
return False return False
# Tested and working
async def authorization_verify_based_on_roles(
request: Request,
user_id: int,
action: Literal["read", "update", "delete", "create"],
element_uuid: str,
db_session: Session,
):
element_type = await check_element_type(element_uuid)
# Get user roles bound to an organization and standard roles
statement = (
select(Role)
.join(UserOrganization)
.where((UserOrganization.org_id == Role.org_id) | (Role.org_id == null()))
.where(UserOrganization.user_id == user_id)
)
user_roles_in_organization_and_standard_roles = db_session.exec(statement).all()
# Find in roles list if there is a role that matches users action for this type of element
for role in user_roles_in_organization_and_standard_roles:
role = Role.from_orm(role)
if role.rights:
rights = role.rights
if rights[element_type][f"action_{action}"] is True:
return True
else:
return False
else: else:
return False return False
async def authorization_verify_based_on_roles( # Tested and working
request: Request,
user_id: str,
action: Literal["read", "update", "delete", "create"],
roles_list: list[UserRolesInOrganization],
element_id: str,
):
element_type = await check_element_type(element_id)
element = request.app.db[element_type]
roles = request.app.db["roles"]
# Get the element
element_identifier = await get_id_identifier_of_element(element_id)
element = await element.find_one({element_identifier: element_id})
# Get the roles of the user
roles_id_list = [role["role_id"] for role in roles_list]
roles = await roles.find({"role_id": {"$in": roles_id_list}}).to_list(length=100)
async def checkRoles():
# Check Roles
for role in roles:
role = RoleInDB(**role)
if role.elements[element_type][f"action_{action}"] is True:
return True
else:
return False
async def checkOrgRoles():
# Check Org Roles
users = request.app.db["users"]
user = await users.find_one({"user_id": user_id})
if element is not None:
for org in user["orgs"]:
if org["org_id"] == element["org_id"]:
if org["org_role"] == "owner" or org["org_role"] == "editor":
return True
else:
return False
if await checkRoles() or await checkOrgRoles():
return True
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User rights (roless) : You don't have the right to perform this action",
)
async def authorization_verify_based_on_roles_and_authorship( async def authorization_verify_based_on_roles_and_authorship(
request: Request, request: Request,
user_id: str, user_id: int,
action: Literal["read", "update", "delete", "create"], action: Literal["read", "update", "delete", "create"],
roles_list: list[UserRolesInOrganization], element_uuid: str,
element_id: str, db_session: Session,
): ):
isAuthor = await authorization_verify_if_user_is_author( isAuthor = await authorization_verify_if_user_is_author(
request, user_id, action, element_id request, user_id, action, element_uuid, db_session
) )
isRole = await authorization_verify_based_on_roles( isRole = await authorization_verify_based_on_roles(
request, user_id, action, roles_list, element_id request, user_id, action, element_uuid, db_session
) )
if isAuthor or isRole: if isAuthor or isRole:
@ -141,8 +139,8 @@ async def authorization_verify_based_on_roles_and_authorship(
) )
async def authorization_verify_if_user_is_anon(user_id: str): async def authorization_verify_if_user_is_anon(user_id: int):
if user_id == "anonymous": if user_id == 0:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="You should be logged in to perform this action", detail="You should be logged in to perform this action",

View file

@ -5,6 +5,7 @@ async def check_element_type(element_id):
""" """
Check if the element is a course, a user, a house or a collection, by checking its prefix Check if the element is a course, a user, a house or a collection, by checking its prefix
""" """
print("element_id", element_id)
if element_id.startswith("course_"): if element_id.startswith("course_"):
return "courses" return "courses"
elif element_id.startswith("user_"): elif element_id.startswith("user_"):
@ -13,12 +14,14 @@ async def check_element_type(element_id):
return "houses" return "houses"
elif element_id.startswith("org_"): elif element_id.startswith("org_"):
return "organizations" return "organizations"
elif element_id.startswith("coursechapter_"): elif element_id.startswith("chapter_"):
return "coursechapters" return "coursechapters"
elif element_id.startswith("collection_"): elif element_id.startswith("collection_"):
return "collections" return "collections"
elif element_id.startswith("activity_"): elif element_id.startswith("activity_"):
return "activities" return "activities"
elif element_id.startswith("role_"):
return "roles"
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, status_code=status.HTTP_409_CONFLICT,

View file

@ -0,0 +1,89 @@
from datetime import datetime
from uuid import uuid4
from src.db.organizations import Organization
from fastapi import HTTPException, status, UploadFile, Request
from sqlmodel import Session, select
from src.db.activities import Activity
from src.db.blocks import Block, BlockRead, BlockTypeEnum
from src.db.courses import Course
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
from src.services.users.users import PublicUser
async def create_image_block(
request: Request, image_file: UploadFile, activity_uuid: str, db_session: Session
):
statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
activity = db_session.exec(statement).first()
if not activity:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found"
)
block_type = "imageBlock"
# get org_uuid
statement = select(Organization).where(Organization.id == activity.org_id)
org = db_session.exec(statement).first()
# get course
statement = select(Course).where(Course.id == activity.course_id)
course = db_session.exec(statement).first()
if not course:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Course not found"
)
# get block id
block_uuid = str(f"block_{uuid4()}")
block_data = await upload_file_and_return_file_object(
request,
image_file,
activity_uuid,
block_uuid,
["jpg", "jpeg", "png", "gif"],
block_type,
org.org_uuid,
str(course.course_uuid),
)
# create block
block = Block(
activity_id=activity.id if activity.id else 0,
block_type=BlockTypeEnum.BLOCK_IMAGE,
content=block_data.dict(),
org_id=org.id if org.id else 0,
course_id=course.id if course.id else 0,
block_uuid=block_uuid,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
# insert block
db_session.add(block)
db_session.commit()
db_session.refresh(block)
block = BlockRead.from_orm(block)
return block
async def get_image_block(
request: Request, block_uuid: str, current_user: PublicUser, db_session: Session
):
statement = select(Block).where(Block.block_uuid == block_uuid)
block = db_session.exec(statement).first()
if block:
block = BlockRead.from_orm(block)
return block
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Image block does not exist"
)

View file

@ -1,72 +0,0 @@
from uuid import uuid4
from fastapi import HTTPException, status, UploadFile, Request
from src.services.blocks.schemas.blocks import Block
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
from src.services.users.users import PublicUser
async def create_image_block(
request: Request, image_file: UploadFile, activity_id: str
):
blocks = request.app.db["blocks"]
activity = request.app.db["activities"]
courses = request.app.db["courses"]
block_type = "imageBlock"
# get org_id from activity
activity = await activity.find_one({"activity_id": activity_id}, {"_id": 0})
org_id = activity["org_id"]
coursechapter_id = activity["coursechapter_id"]
# get course_id from coursechapter
course = await courses.find_one(
{"chapters": coursechapter_id},
{"_id": 0},
)
# get block id
block_id = str(f"block_{uuid4()}")
block_data = await upload_file_and_return_file_object(
request,
image_file,
activity_id,
block_id,
["jpg", "jpeg", "png", "gif"],
block_type,
org_id,
course["course_id"],
)
# create block
block = Block(
block_id=block_id,
activity_id=activity_id,
block_type=block_type,
block_data=block_data,
org_id=org_id,
course_id=course["course_id"],
)
# insert block
await blocks.insert_one(block.dict())
return block
async def get_image_block(request: Request, file_id: str, current_user: PublicUser):
blocks = request.app.db["blocks"]
video_block = await blocks.find_one({"block_id": file_id})
if video_block:
return Block(**video_block)
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Image block does not exist"
)

View file

@ -1,69 +1,89 @@
from datetime import datetime
from uuid import uuid4 from uuid import uuid4
from src.db.organizations import Organization
from fastapi import HTTPException, status, UploadFile, Request from fastapi import HTTPException, status, UploadFile, Request
from src.services.blocks.schemas.blocks import Block from sqlmodel import Session, select
from src.db.activities import Activity
from src.db.blocks import Block, BlockRead, BlockTypeEnum
from src.db.courses import Course
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
from src.services.users.users import PublicUser from src.services.users.users import PublicUser
async def create_pdf_block(request: Request, pdf_file: UploadFile, activity_id: str): async def create_pdf_block(
blocks = request.app.db["blocks"] request: Request, pdf_file: UploadFile, activity_uuid: str, db_session: Session
activity = request.app.db["activities"] ):
courses = request.app.db["courses"] statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
activity = db_session.exec(statement).first()
if not activity:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found"
)
block_type = "pdfBlock" block_type = "pdfBlock"
# get org_id from activity # get org_uuid
activity = await activity.find_one({"activity_id": activity_id}, {"_id": 0}) statement = select(Organization).where(Organization.id == activity.org_id)
org_id = activity["org_id"] org = db_session.exec(statement).first()
# get course
statement = select(Course).where(Course.id == activity.course_id)
course = db_session.exec(statement).first()
if not course:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Course not found"
)
# get block id # get block id
block_id = str(f"block_{uuid4()}") block_uuid = str(f"block_{uuid4()}")
coursechapter_id = activity["coursechapter_id"]
# get course_id from coursechapter
course = await courses.find_one(
{"chapters": coursechapter_id},
{"_id": 0},
)
block_data = await upload_file_and_return_file_object( block_data = await upload_file_and_return_file_object(
request, request,
pdf_file, pdf_file,
activity_id, activity_uuid,
block_id, block_uuid,
["pdf"], ["pdf"],
block_type, block_type,
org_id, org.org_uuid,
course["course_id"], str(course.course_uuid),
) )
# create block # create block
block = Block( block = Block(
block_id=block_id, activity_id=activity.id if activity.id else 0,
activity_id=activity_id, block_type=BlockTypeEnum.BLOCK_DOCUMENT_PDF,
block_type=block_type, content=block_data.dict(),
block_data=block_data, org_id=org.id if org.id else 0,
org_id=org_id, course_id=course.id if course.id else 0,
course_id=course["course_id"], block_uuid=block_uuid,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
) )
# insert block # insert block
await blocks.insert_one(block.dict()) db_session.add(block)
db_session.commit()
db_session.refresh(block)
block = BlockRead.from_orm(block)
return block return block
async def get_pdf_block(request: Request, file_id: str, current_user: PublicUser): async def get_pdf_block(
blocks = request.app.db["blocks"] request: Request, block_uuid: str, current_user: PublicUser, db_session: Session
):
statement = select(Block).where(Block.block_uuid == block_uuid)
block = db_session.exec(statement).first()
pdf_block = await blocks.find_one({"block_id": file_id}) if not block:
if pdf_block:
return Block(**pdf_block)
else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Video file does not exist" status_code=status.HTTP_404_NOT_FOUND, detail="Video file does not exist"
) )
block = BlockRead.from_orm(block)
return block

View file

@ -1,70 +0,0 @@
from typing import List, Literal
from uuid import uuid4
from fastapi import Request
from pydantic import BaseModel
from src.services.blocks.schemas.blocks import Block
from src.services.users.users import PublicUser
class option(BaseModel):
option_id: str
option_type: Literal["text", "image"]
option_data: str
class answer(BaseModel):
question_id: str
option_id: str
class question(BaseModel):
question_id: str
question_value:str
options: List[option]
class quizBlock(BaseModel):
questions: List[question]
answers: List[answer]
async def create_quiz_block(request: Request, quizBlock: quizBlock, activity_id: str, user: PublicUser):
blocks = request.app.db["blocks"]
activities = request.app.db["activities"]
request.app.db["courses"]
# Get org_id from activity
activity = await activities.find_one({"activity_id": activity_id}, {"_id": 0, "org_id": 1})
org_id = activity["org_id"]
# Get course_id from activity
course = await activities.find_one({"activity_id": activity_id}, {"_id": 0, "course_id": 1})
block_id = str(f"block_{uuid4()}")
# create block
block = Block(block_id=block_id, activity_id=activity_id,
block_type="quizBlock", block_data=quizBlock, org_id=org_id, course_id=course["course_id"])
# insert block
await blocks.insert_one(block.dict())
return block
async def get_quiz_block_options(request: Request, block_id: str, user: PublicUser):
blocks = request.app.db["blocks"]
# find block but get only the options
block = await blocks.find_one({"block_id": block_id, }, {
"_id": 0, "block_data.answers": 0})
return block
async def get_quiz_block_answers(request: Request, block_id: str, user: PublicUser):
blocks = request.app.db["blocks"]
# find block but get only the answers
block = await blocks.find_one({"block_id": block_id, }, {
"_id": 0, "block_data.questions": 0})
return block

View file

@ -1,73 +1,89 @@
from datetime import datetime
from uuid import uuid4 from uuid import uuid4
from src.db.organizations import Organization
from fastapi import HTTPException, status, UploadFile, Request from fastapi import HTTPException, status, UploadFile, Request
from src.services.blocks.schemas.blocks import Block from sqlmodel import Session, select
from src.db.activities import Activity
from src.db.blocks import Block, BlockRead, BlockTypeEnum
from src.db.courses import Course
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
from src.services.users.users import PublicUser from src.services.users.users import PublicUser
async def create_video_block( async def create_video_block(
request: Request, video_file: UploadFile, activity_id: str request: Request, video_file: UploadFile, activity_uuid: str, db_session: Session
): ):
blocks = request.app.db["blocks"] statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
activity = request.app.db["activities"] activity = db_session.exec(statement).first()
courses = request.app.db["courses"]
if not activity:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found"
)
block_type = "videoBlock" block_type = "videoBlock"
# get org_id from activity # get org_uuid
activity = await activity.find_one( statement = select(Organization).where(Organization.id == activity.org_id)
{"activity_id": activity_id}, {"_id": 0} org = db_session.exec(statement).first()
)
org_id = activity["org_id"] # get course
statement = select(Course).where(Course.id == activity.course_id)
course = db_session.exec(statement).first()
if not course:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Course not found"
)
# get block id # get block id
block_id = str(f"block_{uuid4()}") block_uuid = str(f"block_{uuid4()}")
coursechapter_id = activity["coursechapter_id"]
# get course_id from coursechapter
course = await courses.find_one(
{"chapters": coursechapter_id},
{"_id": 0},
)
block_data = await upload_file_and_return_file_object( block_data = await upload_file_and_return_file_object(
request, request,
video_file, video_file,
activity_id, activity_uuid,
block_id, block_uuid,
["mp4", "webm", "ogg"], ["mp4", "webm", "ogg"],
block_type, block_type,
org_id, org.org_uuid,
course["course_id"], str(course.course_uuid),
) )
# create block # create block
block = Block( block = Block(
block_id=block_id, activity_id=activity.id if activity.id else 0,
activity_id=activity_id, block_type=BlockTypeEnum.BLOCK_VIDEO,
block_type=block_type, content=block_data.dict(),
block_data=block_data, org_id=org.id if org.id else 0,
org_id=org_id, course_id=course.id if course.id else 0,
course_id=course["course_id"], block_uuid=block_uuid,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
) )
# insert block # insert block
await blocks.insert_one(block.dict()) db_session.add(block)
db_session.commit()
db_session.refresh(block)
block = BlockRead.from_orm(block)
return block return block
async def get_video_block(request: Request, file_id: str, current_user: PublicUser): async def get_video_block(
blocks = request.app.db["blocks"] request: Request, block_uuid: str, current_user: PublicUser, db_session: Session
):
statement = select(Block).where(Block.block_uuid == block_uuid)
block = db_session.exec(statement).first()
video_block = await blocks.find_one({"block_id": file_id}) if not block:
if video_block:
return Block(**video_block)
else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Video file does not exist" status_code=status.HTTP_404_NOT_FOUND, detail="Video file does not exist"
) )
block = BlockRead.from_orm(block)
return block

View file

@ -7,4 +7,4 @@ class BlockFile(BaseModel):
file_name: str file_name: str
file_size: int file_size: int
file_type: str file_type: str
activity_id: str activity_uuid: str

View file

@ -7,12 +7,12 @@ from src.services.utils.upload_content import upload_content
async def upload_file_and_return_file_object( async def upload_file_and_return_file_object(
request: Request, request: Request,
file: UploadFile, file: UploadFile,
activity_id: str, activity_uuid: str,
block_id: str, block_id: str,
list_of_allowed_file_formats: list, list_of_allowed_file_formats: list,
type_of_block: str, type_of_block: str,
org_id: str, org_uuid: str,
course_id: str, course_uuid: str,
): ):
# get file id # get file id
file_id = str(uuid.uuid4()) file_id = str(uuid.uuid4())
@ -45,12 +45,12 @@ async def upload_file_and_return_file_object(
file_name=file_name, file_name=file_name,
file_size=file_size, file_size=file_size,
file_type=file_type, file_type=file_type,
activity_id=activity_id, activity_uuid=activity_uuid,
) )
await upload_content( await upload_content(
f"courses/{course_id}/activities/{activity_id}/dynamic/blocks/{type_of_block}/{block_id}", f"courses/{course_uuid}/activities/{activity_uuid}/dynamic/blocks/{type_of_block}/{block_id}",
org_id=org_id, org_uuid=org_uuid,
file_binary=file_binary, file_binary=file_binary,
file_and_format=f"{file_id}.{file_format}", file_and_format=f"{file_id}.{file_format}",
) )

View file

@ -1,35 +1,17 @@
from typing import Literal from typing import Literal
from pydantic import BaseModel from sqlmodel import Session, select
from src.db.chapters import Chapter
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles, 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.services.users.schemas.users import AnonymousUser, PublicUser from src.db.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate
from fastapi import HTTPException, status, Request from src.db.chapter_activities import ChapterActivity
from src.db.users import AnonymousUser, PublicUser
from fastapi import HTTPException, Request
from uuid import uuid4 from uuid import uuid4
from datetime import datetime from datetime import datetime
#### Classes ####################################################
class Activity(BaseModel):
name: str
type: str
content: object
class ActivityInDB(Activity):
activity_id: str
course_id: str
coursechapter_id: str
org_id: str
creationDate: str
updateDate: str
#### Classes ####################################################
#################################################### ####################################################
# CRUD # CRUD
@ -38,148 +20,162 @@ class ActivityInDB(Activity):
async def create_activity( async def create_activity(
request: Request, request: Request,
activity_object: Activity, activity_object: ActivityCreate,
org_id: str, current_user: PublicUser | AnonymousUser,
coursechapter_id: str, db_session: Session,
current_user: PublicUser,
): ):
activities = request.app.db["activities"] activity = Activity.from_orm(activity_object)
courses = request.app.db["courses"]
users = request.app.db["users"]
# get user # CHeck if org exists
user = await users.find_one({"user_id": current_user.user_id}) statement = select(Chapter).where(Chapter.id == activity_object.chapter_id)
chapter = db_session.exec(statement).first()
# generate activity_id if not chapter:
activity_id = str(f"activity_{uuid4()}") raise HTTPException(
status_code=404,
detail="Chapter not found",
)
# verify activity rights # RBAC check
await authorization_verify_based_on_roles( await rbac_check(request, chapter.chapter_uuid, current_user, "create", db_session)
request,
current_user.user_id, activity.activity_uuid = str(f"activity_{uuid4()}")
"create", activity.creation_date = str(datetime.now())
user["roles"], activity.update_date = str(datetime.now())
activity_id, activity.org_id = chapter.org_id
activity.course_id = chapter.course_id
# Insert Activity in DB
db_session.add(activity)
db_session.commit()
db_session.refresh(activity)
# Find the last activity in the Chapter and add it to the list
statement = (
select(ChapterActivity)
.where(ChapterActivity.chapter_id == activity_object.chapter_id)
.order_by(ChapterActivity.order)
)
chapter_activities = db_session.exec(statement).all()
last_order = chapter_activities[-1].order if chapter_activities else 0
to_be_used_order = last_order + 1
# Add activity to chapter
activity_chapter = ChapterActivity(
chapter_id=activity_object.chapter_id,
activity_id=activity.id if activity.id else 0,
course_id=chapter.course_id,
org_id=chapter.org_id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
order=to_be_used_order,
) )
# get course_id from activity # Insert ChapterActivity link in DB
course = await courses.find_one({"chapters": coursechapter_id}) db_session.add(activity_chapter)
db_session.commit()
db_session.refresh(activity_chapter)
# create activity return ActivityRead.from_orm(activity)
activity = ActivityInDB(
**activity_object.dict(),
creationDate=str(datetime.now()),
coursechapter_id=coursechapter_id,
updateDate=str(datetime.now()),
activity_id=activity_id,
org_id=org_id,
course_id=course["course_id"],
)
await activities.insert_one(activity.dict())
# update chapter
await courses.update_one(
{"chapters_content.coursechapter_id": coursechapter_id},
{"$addToSet": {"chapters_content.$.activities": activity_id}},
)
return activity
async def get_activity(request: Request, activity_id: str, current_user: PublicUser): async def get_activity(
activities = request.app.db["activities"] request: Request,
courses = request.app.db["courses"] activity_uuid: str,
current_user: PublicUser,
activity = await activities.find_one({"activity_id": activity_id}) db_session: Session,
):
# get course_id from activity statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
coursechapter_id = activity["coursechapter_id"] activity = db_session.exec(statement).first()
await courses.find_one({"chapters": coursechapter_id})
# verify course rights
await verify_rights(request, activity["course_id"], current_user, "read")
if not activity: if not activity:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" status_code=404,
detail="Activity not found",
) )
activity = ActivityInDB(**activity) # RBAC check
await rbac_check(request, activity.activity_uuid, current_user, "read", db_session)
activity = ActivityRead.from_orm(activity)
return activity return activity
async def update_activity( async def update_activity(
request: Request, request: Request,
activity_object: Activity, activity_object: ActivityUpdate,
activity_id: str, activity_uuid: str,
current_user: PublicUser, current_user: PublicUser | AnonymousUser,
db_session: Session,
): ):
activities = request.app.db["activities"] statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
activity = db_session.exec(statement).first()
activity = await activities.find_one({"activity_id": activity_id})
# verify course rights
await verify_rights(request, activity_id, current_user, "update")
if activity:
creationDate = activity["creationDate"]
# get today's date
datetime_object = datetime.now()
updated_course = ActivityInDB(
activity_id=activity_id,
coursechapter_id=activity["coursechapter_id"],
creationDate=creationDate,
updateDate=str(datetime_object),
course_id=activity["course_id"],
org_id=activity["org_id"],
**activity_object.dict(),
)
await activities.update_one(
{"activity_id": activity_id}, {"$set": updated_course.dict()}
)
return ActivityInDB(**updated_course.dict())
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="activity does not exist"
)
async def delete_activity(request: Request, activity_id: str, current_user: PublicUser):
activities = request.app.db["activities"]
activity = await activities.find_one({"activity_id": activity_id})
# verify course rights
await verify_rights(request, activity_id, current_user, "delete")
if not activity: if not activity:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="activity does not exist" status_code=404,
detail="Activity not found",
) )
# Remove Activity # RBAC check
isDeleted = await activities.delete_one({"activity_id": activity_id}) await rbac_check(
request, activity.activity_uuid, current_user, "update", db_session
# Remove Activity from chapter
courses = request.app.db["courses"]
isDeletedFromChapter = await courses.update_one(
{"chapters_content.activities": activity_id},
{"$pull": {"chapters_content.$.activities": activity_id}},
) )
if isDeleted and isDeletedFromChapter: # Update only the fields that were passed in
return {"detail": "Activity deleted"} for var, value in vars(activity_object).items():
else: if value is not None:
setattr(activity, var, value)
db_session.add(activity)
db_session.commit()
db_session.refresh(activity)
activity = ActivityRead.from_orm(activity)
return activity
async def delete_activity(
request: Request,
activity_uuid: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
activity = db_session.exec(statement).first()
if not activity:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=404,
detail="Unavailable database", detail="Activity not found",
) )
# RBAC check
await rbac_check(
request, activity.activity_uuid, current_user, "delete", db_session
)
# Delete activity from chapter
statement = select(ChapterActivity).where(
ChapterActivity.activity_id == activity.id
)
activity_chapter = db_session.exec(statement).first()
if not activity_chapter:
raise HTTPException(
status_code=404,
detail="Activity not found in chapter",
)
db_session.delete(activity_chapter)
db_session.delete(activity)
db_session.commit()
return {"detail": "Activity deleted"}
#################################################### ####################################################
# Misc # Misc
@ -187,64 +183,49 @@ async def delete_activity(request: Request, activity_id: str, current_user: Publ
async def get_activities( async def get_activities(
request: Request, coursechapter_id: str, current_user: PublicUser request: Request,
): coursechapter_id: int,
activities = request.app.db["activities"] current_user: PublicUser | AnonymousUser,
db_session: Session,
activities = activities.find({"coursechapter_id": coursechapter_id}) ) -> list[ActivityRead]:
statement = select(ChapterActivity).where(
ChapterActivity.chapter_id == coursechapter_id
)
activities = db_session.exec(statement).all()
if not activities: if not activities:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" status_code=404,
detail="No activities found",
) )
activities = [ # RBAC check
ActivityInDB(**activity) for activity in await activities.to_list(length=100) await rbac_check(request, "activity_x", current_user, "read", db_session)
]
activities = [ActivityRead.from_orm(activity) for activity in activities]
return activities return activities
#### Security #################################################### ## 🔒 RBAC Utils ##
async def verify_rights( async def rbac_check(
request: Request, request: Request,
activity_id: str, # course_id in case of read course_id: str,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"], action: Literal["create", "read", "update", "delete"],
db_session: Session,
): ):
if action == "read": await authorization_verify_if_user_is_anon(current_user.id)
if current_user.user_id == "anonymous":
await authorization_verify_if_element_is_public(
request, activity_id, current_user.user_id, action
)
else:
users = request.app.db["users"]
user = await users.find_one({"user_id": current_user.user_id})
await authorization_verify_if_user_is_anon(current_user.user_id) await authorization_verify_based_on_roles_and_authorship(
request,
await authorization_verify_based_on_roles( current_user.id,
request, action,
current_user.user_id, course_id,
action, db_session,
user["roles"], )
activity_id,
)
else:
users = request.app.db["users"]
user = await users.find_one({"user_id": current_user.user_id})
await authorization_verify_if_user_is_anon(current_user.user_id)
await authorization_verify_based_on_roles(
request,
current_user.user_id,
action,
user["roles"],
activity_id,
)
#### Security #################################################### ## 🔒 RBAC Utils ##

View file

@ -1,7 +1,22 @@
from src.security.rbac.rbac import authorization_verify_based_on_roles from typing import Literal
from src.db.courses import Course
from src.db.organizations import Organization
from sqlmodel import Session, select
from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
)
from src.db.chapters import Chapter
from src.db.activities import (
Activity,
ActivityRead,
ActivitySubTypeEnum,
ActivityTypeEnum,
)
from src.db.chapter_activities import ChapterActivity
from src.db.course_chapters import CourseChapter
from src.db.users import AnonymousUser, PublicUser
from src.services.courses.activities.uploads.pdfs import upload_pdf from src.services.courses.activities.uploads.pdfs import upload_pdf
from src.services.users.users import PublicUser
from src.services.courses.activities.activities import ActivityInDB
from fastapi import HTTPException, status, UploadFile, Request from fastapi import HTTPException, status, UploadFile, Request
from uuid import uuid4 from uuid import uuid4
from datetime import datetime from datetime import datetime
@ -10,26 +25,46 @@ from datetime import datetime
async def create_documentpdf_activity( async def create_documentpdf_activity(
request: Request, request: Request,
name: str, name: str,
coursechapter_id: str, chapter_id: str,
current_user: PublicUser, current_user: PublicUser | AnonymousUser,
db_session: Session,
pdf_file: UploadFile | None = None, pdf_file: UploadFile | None = None,
): ):
activities = request.app.db["activities"] # RBAC check
courses = request.app.db["courses"] await rbac_check(request, "activity_x", current_user, "create", db_session)
users = request.app.db["users"]
# get user # get chapter_id
user = await users.find_one({"user_id": current_user.user_id}) statement = select(Chapter).where(Chapter.id == chapter_id)
chapter = db_session.exec(statement).first()
# generate activity_id if not chapter:
activity_id = str(f"activity_{uuid4()}") raise HTTPException(
status_code=404,
detail="Chapter not found",
)
# get org_id from course statement = select(CourseChapter).where(CourseChapter.chapter_id == chapter_id)
coursechapter = await courses.find_one( coursechapter = db_session.exec(statement).first()
{"chapters_content.coursechapter_id": coursechapter_id}
)
org_id = coursechapter["org_id"] if not coursechapter:
raise HTTPException(
status_code=404,
detail="CourseChapter not found",
)
# get org_id
org_id = coursechapter.org_id
# Get org_uuid
statement = select(Organization).where(Organization.id == coursechapter.org_id)
organization = db_session.exec(statement).first()
# Get course_uuid
statement = select(Course).where(Course.id == coursechapter.course_id)
course = db_session.exec(statement).first()
# create activity uuid
activity_uuid = f"activity_{uuid4()}"
# check if pdf_file is not None # check if pdf_file is not None
if not pdf_file: if not pdf_file:
@ -51,45 +86,77 @@ async def create_documentpdf_activity(
status_code=status.HTTP_409_CONFLICT, detail="Pdf : No pdf file provided" status_code=status.HTTP_409_CONFLICT, detail="Pdf : No pdf file provided"
) )
activity_object = ActivityInDB( # Create activity
org_id=org_id, activity = Activity(
activity_id=activity_id,
coursechapter_id=coursechapter_id,
name=name, name=name,
type="documentpdf", activity_type=ActivityTypeEnum.TYPE_DOCUMENT,
course_id=coursechapter["course_id"], activity_sub_type=ActivitySubTypeEnum.SUBTYPE_DOCUMENT_PDF,
content={ content={
"documentpdf": { "filename": "documentpdf." + pdf_format,
"filename": "documentpdf." + pdf_format, "activity_uuid": activity_uuid,
"activity_id": activity_id,
}
}, },
creationDate=str(datetime.now()), published_version=1,
updateDate=str(datetime.now()), version=1,
org_id=org_id if org_id else 0,
course_id=coursechapter.course_id,
activity_uuid=activity_uuid,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
) )
await authorization_verify_based_on_roles( # Insert Activity in DB
request, db_session.add(activity)
current_user.user_id, db_session.commit()
"create", db_session.refresh(activity)
user["roles"],
activity_id,
)
# create activity # Add activity to chapter
activity = ActivityInDB(**activity_object.dict()) activity_chapter = ChapterActivity(
await activities.insert_one(activity.dict()) chapter_id=(int(chapter_id)),
activity_id=activity.id, # type: ignore
course_id=coursechapter.course_id,
org_id=coursechapter.org_id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
order=1,
)
# upload pdf # upload pdf
if pdf_file: if pdf_file:
# get pdffile format # get pdffile format
await upload_pdf(pdf_file, activity_id, org_id, coursechapter["course_id"]) await upload_pdf(
pdf_file,
activity.activity_uuid,
organization.org_uuid,
course.course_uuid,
)
# todo : choose whether to update the chapter or not # Insert ChapterActivity link in DB
# update chapter db_session.add(activity_chapter)
await courses.update_one( db_session.commit()
{"chapters_content.coursechapter_id": coursechapter_id}, db_session.refresh(activity_chapter)
{"$addToSet": {"chapters_content.$.activities": activity_id}},
return ActivityRead.from_orm(activity)
## 🔒 RBAC Utils ##
async def rbac_check(
request: Request,
course_id: str,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
db_session: Session,
):
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,
db_session,
) )
return activity
## 🔒 RBAC Utils ##

View file

@ -1,15 +1,14 @@
from src.services.utils.upload_content import upload_content from src.services.utils.upload_content import upload_content
async def upload_pdf(pdf_file, activity_id, org_id, course_id): async def upload_pdf(pdf_file, activity_uuid, org_uuid, course_uuid):
contents = pdf_file.file.read() contents = pdf_file.file.read()
pdf_format = pdf_file.filename.split(".")[-1] pdf_format = pdf_file.filename.split(".")[-1]
try: try:
await upload_content( await upload_content(
f"courses/{course_id}/activities/{activity_id}/documentpdf", f"courses/{course_uuid}/activities/{activity_uuid}/documentpdf",
org_id, org_uuid,
contents, contents,
f"documentpdf.{pdf_format}", f"documentpdf.{pdf_format}",
) )

View file

@ -2,14 +2,14 @@
from src.services.utils.upload_content import upload_content from src.services.utils.upload_content import upload_content
async def upload_video(video_file, activity_id, org_id, course_id): async def upload_video(video_file, activity_uuid, org_uuid, course_uuid):
contents = video_file.file.read() contents = video_file.file.read()
video_format = video_file.filename.split(".")[-1] video_format = video_file.filename.split(".")[-1]
try: try:
await upload_content( await upload_content(
f"courses/{course_id}/activities/{activity_id}/video", f"courses/{course_uuid}/activities/{activity_uuid}/video",
org_id, org_uuid,
contents, contents,
f"video.{video_format}", f"video.{video_format}",
) )

View file

@ -1,12 +1,24 @@
from typing import Literal from typing import Literal
from src.db.courses import Course
from src.db.organizations import Organization
from pydantic import BaseModel from pydantic import BaseModel
from sqlmodel import Session, select
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles, authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
) )
from src.db.chapters import Chapter
from src.db.activities import (
Activity,
ActivityRead,
ActivitySubTypeEnum,
ActivityTypeEnum,
)
from src.db.chapter_activities import ChapterActivity
from src.db.course_chapters import CourseChapter
from src.db.users import AnonymousUser, PublicUser
from src.services.courses.activities.uploads.videos import upload_video from src.services.courses.activities.uploads.videos import upload_video
from src.services.users.users import PublicUser
from src.services.courses.activities.activities import ActivityInDB
from fastapi import HTTPException, status, UploadFile, Request from fastapi import HTTPException, status, UploadFile, Request
from uuid import uuid4 from uuid import uuid4
from datetime import datetime from datetime import datetime
@ -15,32 +27,43 @@ from datetime import datetime
async def create_video_activity( async def create_video_activity(
request: Request, request: Request,
name: str, name: str,
coursechapter_id: str, chapter_id: str,
current_user: PublicUser, current_user: PublicUser,
db_session: Session,
video_file: UploadFile | None = None, video_file: UploadFile | None = None,
): ):
activities = request.app.db["activities"] # RBAC check
courses = request.app.db["courses"] await rbac_check(request, "activity_x", current_user, "create", db_session)
users = request.app.db["users"]
# get user # get chapter_id
user = await users.find_one({"user_id": current_user.user_id}) statement = select(Chapter).where(Chapter.id == chapter_id)
chapter = db_session.exec(statement).first()
# generate activity_id if not chapter:
activity_id = str(f"activity_{uuid4()}") raise HTTPException(
status_code=404,
detail="Chapter not found",
)
# get org_id from course statement = select(CourseChapter).where(CourseChapter.chapter_id == chapter_id)
coursechapter = await courses.find_one( coursechapter = db_session.exec(statement).first()
{"chapters_content.coursechapter_id": coursechapter_id}
)
if not coursechapter: if not coursechapter:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, status_code=404,
detail="CourseChapter : No coursechapter found", detail="CourseChapter not found",
) )
org_id = coursechapter["org_id"] # Get org_uuid
statement = select(Organization).where(Organization.id == coursechapter.org_id)
organization = db_session.exec(statement).first()
# Get course_uuid
statement = select(Course).where(Course.id == coursechapter.course_id)
course = db_session.exec(statement).first()
# generate activity_uuid
activity_uuid = str(f"activity_{uuid4()}")
# check if video_file is not None # check if video_file is not None
if not video_file: if not video_file:
@ -64,55 +87,63 @@ async def create_video_activity(
detail="Video : No video file provided", detail="Video : No video file provided",
) )
activity_object = ActivityInDB( activity_object = Activity(
org_id=org_id,
activity_id=activity_id,
coursechapter_id=coursechapter_id,
course_id=coursechapter["course_id"],
name=name, name=name,
type="video", activity_type=ActivityTypeEnum.TYPE_VIDEO,
activity_sub_type=ActivitySubTypeEnum.SUBTYPE_VIDEO_HOSTED,
activity_uuid=activity_uuid,
org_id=coursechapter.org_id,
course_id=coursechapter.course_id,
published_version=1,
content={ content={
"video": { "filename": "video." + video_format,
"filename": "video." + video_format, "activity_uuid": activity_uuid,
"activity_id": activity_id,
}
}, },
creationDate=str(datetime.now()), version=1,
updateDate=str(datetime.now()), creation_date=str(datetime.now()),
) update_date=str(datetime.now()),
await authorization_verify_based_on_roles(
request,
current_user.user_id,
"create",
user["roles"],
activity_id,
) )
# create activity # create activity
activity = ActivityInDB(**activity_object.dict()) activity = Activity.from_orm(activity_object)
await activities.insert_one(activity.dict()) db_session.add(activity)
db_session.commit()
db_session.refresh(activity)
# upload video # upload video
if video_file: if video_file:
# get videofile format # get videofile format
await upload_video(video_file, activity_id, org_id, coursechapter["course_id"]) await upload_video(
video_file,
activity.activity_uuid,
organization.org_uuid,
course.course_uuid,
)
# todo : choose whether to update the chapter or not
# update chapter # update chapter
await courses.update_one( chapter_activity_object = ChapterActivity(
{"chapters_content.coursechapter_id": coursechapter_id}, chapter_id=chapter.id, # type: ignore
{"$addToSet": {"chapters_content.$.activities": activity_id}}, activity_id=activity.id, # type: ignore
course_id=coursechapter.course_id,
org_id=coursechapter.org_id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
order=1,
) )
return activity # Insert ChapterActivity link in DB
db_session.add(chapter_activity_object)
db_session.commit()
db_session.refresh(chapter_activity_object)
return ActivityRead.from_orm(activity)
class ExternalVideo(BaseModel): class ExternalVideo(BaseModel):
name: str name: str
uri: str uri: str
type: Literal["youtube", "vimeo"] type: Literal["youtube", "vimeo"]
coursechapter_id: str chapter_id: str
class ExternalVideoInDB(BaseModel): class ExternalVideoInDB(BaseModel):
@ -121,67 +152,93 @@ class ExternalVideoInDB(BaseModel):
async def create_external_video_activity( async def create_external_video_activity(
request: Request, request: Request,
current_user: PublicUser, current_user: PublicUser | AnonymousUser,
data: ExternalVideo, data: ExternalVideo,
db_session: Session,
): ):
activities = request.app.db["activities"] # RBAC check
courses = request.app.db["courses"] await rbac_check(request, "activity_x", current_user, "create", db_session)
users = request.app.db["users"]
# get user # get chapter_id
user = await users.find_one({"user_id": current_user.user_id}) statement = select(Chapter).where(Chapter.id == data.chapter_id)
chapter = db_session.exec(statement).first()
# generate activity_id if not chapter:
activity_id = str(f"activity_{uuid4()}") raise HTTPException(
status_code=404,
detail="Chapter not found",
)
# get org_id from course statement = select(CourseChapter).where(CourseChapter.chapter_id == data.chapter_id)
coursechapter = await courses.find_one( coursechapter = db_session.exec(statement).first()
{"chapters_content.coursechapter_id": data.coursechapter_id}
)
if not coursechapter: if not coursechapter:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, status_code=404,
detail="CourseChapter : No coursechapter found", detail="CourseChapter not found",
) )
org_id = coursechapter["org_id"] # generate activity_uuid
activity_uuid = str(f"activity_{uuid4()}")
activity_object = ActivityInDB( activity_object = Activity(
org_id=org_id,
activity_id=activity_id,
coursechapter_id=data.coursechapter_id,
name=data.name, name=data.name,
type="video", activity_type=ActivityTypeEnum.TYPE_VIDEO,
activity_sub_type=ActivitySubTypeEnum.SUBTYPE_VIDEO_YOUTUBE,
activity_uuid=activity_uuid,
course_id=coursechapter.course_id,
org_id=coursechapter.org_id,
published_version=1,
content={ content={
"external_video": { "uri": data.uri,
"uri": data.uri, "type": data.type,
"activity_id": activity_id, "activity_uuid": activity_uuid,
"type": data.type,
}
}, },
course_id=coursechapter["course_id"], version=1,
creationDate=str(datetime.now()), creation_date=str(datetime.now()),
updateDate=str(datetime.now()), update_date=str(datetime.now()),
)
await authorization_verify_based_on_roles(
request,
current_user.user_id,
"create",
user["roles"],
activity_id,
) )
# create activity # create activity
activity = ActivityInDB(**activity_object.dict()) activity = Activity.from_orm(activity_object)
await activities.insert_one(activity.dict()) db_session.add(activity)
db_session.commit()
db_session.refresh(activity)
# todo : choose whether to update the chapter or not
# update chapter # update chapter
await courses.update_one( chapter_activity_object = ChapterActivity(
{"chapters_content.coursechapter_id": data.coursechapter_id}, chapter_id=coursechapter.chapter_id, # type: ignore
{"$addToSet": {"chapters_content.$.activities": activity_id}}, activity_id=activity.id, # type: ignore
course_id=coursechapter.course_id,
org_id=coursechapter.org_id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
order=1,
) )
return activity # Insert ChapterActivity link in DB
db_session.add(chapter_activity_object)
db_session.commit()
return ActivityRead.from_orm(activity)
async def rbac_check(
request: Request,
course_id: str,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
db_session: Session,
):
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,
db_session,
)
## 🔒 RBAC Utils ##

View file

@ -1,367 +1,551 @@
from datetime import datetime from datetime import datetime
from typing import List, Literal from typing import List, Literal
from uuid import uuid4 from uuid import uuid4
from pydantic import BaseModel from sqlmodel import Session, select
from src.security.auth import non_public_endpoint from src.db.users import AnonymousUser
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles,
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.activities import Activity, ActivityRead
from src.db.chapter_activities import ChapterActivity
from src.db.chapters import (
Chapter,
ChapterCreate,
ChapterRead,
ChapterUpdate,
ChapterUpdateOrder,
)
from src.services.courses.courses import Course from src.services.courses.courses import Course
from src.services.courses.activities.activities import ActivityInDB
from src.services.users.users import PublicUser from src.services.users.users import PublicUser
from fastapi import HTTPException, status, Request from fastapi import HTTPException, status, Request
class CourseChapter(BaseModel):
name: str
description: str
activities: list
class CourseChapterInDB(CourseChapter):
coursechapter_id: str
course_id: str
creationDate: str
updateDate: str
# Frontend
class CourseChapterMetaData(BaseModel):
chapterOrder: List[str]
chapters: dict
activities: object
#### Classes ####################################################
#################################################### ####################################################
# CRUD # CRUD
#################################################### ####################################################
async def create_coursechapter( async def create_chapter(
request: Request, request: Request,
coursechapter_object: CourseChapter, chapter_object: ChapterCreate,
course_id: str, current_user: PublicUser | AnonymousUser,
current_user: PublicUser, db_session: Session,
): ) -> ChapterRead:
courses = request.app.db["courses"] chapter = Chapter.from_orm(chapter_object)
users = request.app.db["users"]
# get course org_id and verify rights
await courses.find_one({"course_id": course_id})
user = await users.find_one({"user_id": current_user.user_id})
# generate coursechapter_id with uuid4 # Get COurse
coursechapter_id = str(f"coursechapter_{uuid4()}") statement = select(Course).where(Course.id == chapter_object.course_id)
hasRoleRights = await authorization_verify_based_on_roles( course = db_session.exec(statement).one()
request, current_user.user_id, "create", user["roles"], course_id
# RBAC check
await rbac_check(request, "chapter_x", current_user, "create", db_session)
# complete chapter object
chapter.course_id = chapter_object.course_id
chapter.chapter_uuid = f"chapter_{uuid4()}"
chapter.creation_date = str(datetime.now())
chapter.update_date = str(datetime.now())
chapter.org_id = course.org_id
# Find the last chapter in the course and add it to the list
statement = (
select(CourseChapter)
.where(CourseChapter.course_id == chapter.course_id)
.order_by(CourseChapter.order)
) )
course_chapters = db_session.exec(statement).all()
if not hasRoleRights: # get last chapter order
raise HTTPException( last_order = course_chapters[-1].order if course_chapters else 0
status_code=status.HTTP_409_CONFLICT, to_be_used_order = last_order + 1
detail="Roles : Insufficient rights to perform this action",
# Add chapter to database
db_session.add(chapter)
db_session.commit()
db_session.refresh(chapter)
chapter = ChapterRead(**chapter.dict(), activities=[])
# Check if COurseChapter link exists
statement = (
select(CourseChapter)
.where(CourseChapter.chapter_id == chapter.id)
.where(CourseChapter.course_id == chapter.course_id)
.where(CourseChapter.order == to_be_used_order)
)
course_chapter = db_session.exec(statement).first()
if not course_chapter:
# Add CourseChapter link
course_chapter = CourseChapter(
course_id=chapter.course_id,
chapter_id=chapter.id,
org_id=chapter.org_id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
order=to_be_used_order,
) )
coursechapter = CourseChapterInDB( # Insert CourseChapter link in DB
coursechapter_id=coursechapter_id, db_session.add(course_chapter)
creationDate=str(datetime.now()), db_session.commit()
updateDate=str(datetime.now()),
course_id=course_id,
**coursechapter_object.dict(),
)
courses.update_one( return chapter
{"course_id": course_id},
{
"$addToSet": {
"chapters": coursechapter_id,
"chapters_content": coursechapter.dict(),
}
},
)
return coursechapter.dict()
async def get_coursechapter( async def get_chapter(
request: Request, coursechapter_id: str, current_user: PublicUser
):
courses = request.app.db["courses"]
coursechapter = await courses.find_one(
{"chapters_content.coursechapter_id": coursechapter_id}
)
if coursechapter:
# verify course rights
await verify_rights(request, coursechapter["course_id"], current_user, "read")
coursechapter = CourseChapter(**coursechapter)
return coursechapter
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="CourseChapter does not exist"
)
async def update_coursechapter(
request: Request, request: Request,
coursechapter_object: CourseChapter, chapter_id: int,
coursechapter_id: str, current_user: PublicUser | AnonymousUser,
current_user: PublicUser, db_session: Session,
): ) -> ChapterRead:
courses = request.app.db["courses"] statement = select(Chapter).where(Chapter.id == chapter_id)
chapter = db_session.exec(statement).first()
coursechapter = await courses.find_one( if not chapter:
{"chapters_content.coursechapter_id": coursechapter_id} raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist"
)
# RBAC check
await rbac_check(request, chapter.chapter_uuid, current_user, "read", db_session)
# Get activities for this chapter
statement = (
select(Activity)
.join(ChapterActivity, Activity.id == ChapterActivity.activity_id)
.where(ChapterActivity.chapter_id == chapter_id)
.distinct(Activity.id)
) )
if coursechapter: activities = db_session.exec(statement).all()
# verify course rights
await verify_rights(request, coursechapter["course_id"], current_user, "update")
coursechapter = CourseChapterInDB( chapter = ChapterRead(
coursechapter_id=coursechapter_id, **chapter.dict(),
creationDate=str(datetime.now()), activities=[ActivityRead(**activity.dict()) for activity in activities],
updateDate=str(datetime.now()),
course_id=coursechapter["course_id"],
**coursechapter_object.dict(),
)
courses.update_one(
{"chapters_content.coursechapter_id": coursechapter_id},
{"$set": {"chapters_content.$": coursechapter.dict()}},
)
return coursechapter
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Coursechapter does not exist"
)
async def delete_coursechapter(
request: Request, coursechapter_id: str, current_user: PublicUser
):
courses = request.app.db["courses"]
course = await courses.find_one(
{"chapters_content.coursechapter_id": coursechapter_id}
) )
if course: return chapter
# verify course rights
await verify_rights(request, course["course_id"], current_user, "delete")
# Remove coursechapter from course
await courses.update_one(
{"course_id": course["course_id"]},
{"$pull": {"chapters": coursechapter_id}},
)
await courses.update_one(
{"chapters_content.coursechapter_id": coursechapter_id},
{"$pull": {"chapters_content": {"coursechapter_id": coursechapter_id}}},
)
return {"message": "Coursechapter deleted"}
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
)
#################################################### async def update_chapter(
# Misc
####################################################
async def get_coursechapters(
request: Request, course_id: str, page: int = 1, limit: int = 10
):
courses = request.app.db["courses"]
course = await courses.find_one({"course_id": course_id})
if course:
course = Course(**course)
coursechapters = course.chapters_content
return coursechapters
async def get_coursechapters_meta(
request: Request, course_id: str, current_user: PublicUser
):
courses = request.app.db["courses"]
activities = request.app.db["activities"]
await non_public_endpoint(current_user)
await verify_rights(request, course_id, current_user, "read")
coursechapters = await courses.find_one(
{"course_id": course_id}, {"chapters": 1, "chapters_content": 1, "_id": 0}
)
coursechapters = coursechapters
if not coursechapters:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
)
# activities
coursechapter_activityIds_global = []
# chapters
chapters = {}
if coursechapters["chapters_content"]:
for coursechapter in coursechapters["chapters_content"]:
coursechapter = CourseChapterInDB(**coursechapter)
coursechapter_activityIds = []
for activity in coursechapter.activities:
coursechapter_activityIds.append(activity)
coursechapter_activityIds_global.append(activity)
chapters[coursechapter.coursechapter_id] = {
"id": coursechapter.coursechapter_id,
"name": coursechapter.name,
"activityIds": coursechapter_activityIds,
}
# activities
activities_list = {}
for activity in await activities.find(
{"activity_id": {"$in": coursechapter_activityIds_global}}
).to_list(length=100):
activity = ActivityInDB(**activity)
activities_list[activity.activity_id] = {
"id": activity.activity_id,
"name": activity.name,
"type": activity.type,
"content": activity.content,
}
final = {
"chapters": chapters,
"chapterOrder": coursechapters["chapters"],
"activities": activities_list,
}
return final
async def update_coursechapters_meta(
request: Request, request: Request,
course_id: str, chapter_object: ChapterUpdate,
coursechapters_metadata: CourseChapterMetaData, chapter_id: int,
current_user: PublicUser, current_user: PublicUser | AnonymousUser,
): db_session: Session,
courses = request.app.db["courses"] ) -> ChapterRead:
statement = select(Chapter).where(Chapter.id == chapter_id)
chapter = db_session.exec(statement).first()
await verify_rights(request, course_id, current_user, "update") if not chapter:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist"
)
# update chapters in course # RBAC check
await courses.update_one( await rbac_check(request, chapter.chapter_uuid, current_user, "update", db_session)
{"course_id": course_id},
{"$set": {"chapters": coursechapters_metadata.chapterOrder}},
)
if coursechapters_metadata.chapters is not None: # Update only the fields that were passed in
for ( for var, value in vars(chapter_object).items():
coursechapter_id, if value is not None:
chapter_metadata, setattr(chapter, var, value)
) in coursechapters_metadata.chapters.items():
filter_query = {"chapters_content.coursechapter_id": coursechapter_id}
update_query = {
"$set": {
"chapters_content.$.activities": chapter_metadata["activityIds"]
}
}
result = await courses.update_one(filter_query, update_query)
if result.matched_count == 0:
# handle error when no documents are matched by the filter query
print(f"No documents found for course chapter ID {coursechapter_id}")
# update activities in coursechapters chapter.update_date = str(datetime.now())
activity = request.app.db["activities"]
if coursechapters_metadata.chapters is not None:
for (
coursechapter_id,
chapter_metadata,
) in coursechapters_metadata.chapters.items():
# Update coursechapter_id in activities
filter_query = {"activity_id": {"$in": chapter_metadata["activityIds"]}}
update_query = {"$set": {"coursechapter_id": coursechapter_id}}
result = await activity.update_many(filter_query, update_query) db_session.commit()
if result.matched_count == 0: db_session.refresh(chapter)
# handle error when no documents are matched by the filter query
print(f"No documents found for course chapter ID {coursechapter_id}")
return {"detail": "coursechapters metadata updated"} if chapter:
chapter = await get_chapter(
request, chapter.id, current_user, db_session # type: ignore
)
return chapter
#### Security #################################################### async def delete_chapter(
async def verify_rights(
request: Request, request: Request,
course_id: str, chapter_id: str,
current_user: PublicUser, current_user: PublicUser | AnonymousUser,
action: Literal["read", "update", "delete"], db_session: Session,
): ):
courses = request.app.db["courses"] statement = select(Chapter).where(Chapter.id == chapter_id)
users = request.app.db["users"] chapter = db_session.exec(statement).first()
user = await users.find_one({"user_id": current_user.user_id})
course = await courses.find_one({"course_id": course_id}) if not chapter:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist"
)
# RBAC check
await rbac_check(request, chapter.chapter_uuid, current_user, "delete", db_session)
db_session.delete(chapter)
db_session.commit()
# Remove all linked activities
statement = select(ChapterActivity).where(ChapterActivity.id == chapter.id)
chapter_activities = db_session.exec(statement).all()
for chapter_activity in chapter_activities:
db_session.delete(chapter_activity)
db_session.commit()
return {"detail": "chapter deleted"}
async def get_course_chapters(
request: Request,
course_id: int,
db_session: Session,
current_user: PublicUser | AnonymousUser,
page: int = 1,
limit: int = 10,
) -> List[ChapterRead]:
statement = (
select(Chapter)
.join(CourseChapter, Chapter.id == CourseChapter.chapter_id)
.where(CourseChapter.course_id == course_id)
.where(Chapter.course_id == course_id)
.order_by(CourseChapter.order)
.group_by(Chapter.id, CourseChapter.order)
)
chapters = db_session.exec(statement).all()
chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters]
# RBAC check
await rbac_check(request, "chapter_x", current_user, "read", db_session)
# Get activities for each chapter
for chapter in chapters:
statement = (
select(ChapterActivity)
.where(ChapterActivity.chapter_id == chapter.id)
.order_by(ChapterActivity.order)
.distinct(ChapterActivity.id, ChapterActivity.order)
)
chapter_activities = db_session.exec(statement).all()
for chapter_activity in chapter_activities:
statement = (
select(Activity)
.where(Activity.id == chapter_activity.activity_id)
.distinct(Activity.id)
)
activity = db_session.exec(statement).first()
if activity:
chapter.activities.append(ActivityRead(**activity.dict()))
return chapters
# Important Note : this is legacy code that has been used because
# the frontend is still not adapted for the new data structure, this implementation is absolutely not the best one
# and should not be used for future features
async def DEPRECEATED_get_course_chapters(
request: Request,
course_uuid: str,
current_user: PublicUser,
db_session: Session,
):
statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
if not course: if not course:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
) )
if action == "read": # RBAC check
if current_user.user_id == "anonymous": await rbac_check(request, course.course_uuid, current_user, "read", db_session)
await authorization_verify_if_element_is_public(
request, course_id, current_user.user_id, action
)
else:
users = request.app.db["users"]
user = await users.find_one({"user_id": current_user.user_id})
await authorization_verify_if_user_is_anon(current_user.user_id) chapters_in_db = await get_course_chapters(request, course.id, db_session, current_user) # type: ignore
await authorization_verify_based_on_roles_and_authorship( # activities
request,
current_user.user_id,
action,
user["roles"],
course_id,
)
else:
users = request.app.db["users"]
user = await users.find_one({"user_id": current_user.user_id})
await authorization_verify_if_user_is_anon(current_user.user_id) # chapters
chapters = {}
await authorization_verify_based_on_roles_and_authorship( for chapter in chapters_in_db:
request, chapter_activityIds = []
current_user.user_id,
action, for activity in chapter.activities:
user["roles"], print("test", activity)
course_id, chapter_activityIds.append(activity.activity_uuid)
chapters[chapter.chapter_uuid] = {
"uuid": chapter.chapter_uuid,
"id": chapter.id,
"name": chapter.name,
"activityIds": chapter_activityIds,
}
# activities
activities_list = {}
statement = (
select(Activity)
.join(ChapterActivity, ChapterActivity.activity_id == Activity.id)
.where(ChapterActivity.activity_id == Activity.id)
.group_by(Activity.id)
)
activities_in_db = db_session.exec(statement).all()
for activity in activities_in_db:
activities_list[activity.activity_uuid] = {
"uuid": activity.activity_uuid,
"id": activity.id,
"name": activity.name,
"type": activity.activity_type,
"content": activity.content,
}
# get chapter order
statement = (
select(Chapter)
.join(CourseChapter, CourseChapter.chapter_id == Chapter.id)
.where(CourseChapter.chapter_id == Chapter.id)
.group_by(Chapter.id, CourseChapter.order)
.order_by(CourseChapter.order)
)
chapters_in_db = db_session.exec(statement).all()
chapterOrder = []
for chapter in chapters_in_db:
chapterOrder.append(chapter.chapter_uuid)
final = {
"chapters": chapters,
"chapterOrder": chapterOrder,
"activities": activities_list,
}
return final
async def reorder_chapters_and_activities(
request: Request,
course_uuid: str,
chapters_order: ChapterUpdateOrder,
current_user: PublicUser,
db_session: Session,
):
statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
if not course:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
) )
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
#### Security #################################################### ###########
# Chapters
###########
# Delete CourseChapters that are not linked to chapter_id and activity_id and org_id and course_id
statement = (
select(CourseChapter)
.where(
CourseChapter.course_id == course.id, CourseChapter.org_id == course.org_id
)
.order_by(CourseChapter.order)
)
course_chapters = db_session.exec(statement).all()
chapter_ids_to_keep = [
chapter_order.chapter_id
for chapter_order in chapters_order.chapter_order_by_ids
]
for course_chapter in course_chapters:
if course_chapter.chapter_id not in chapter_ids_to_keep:
db_session.delete(course_chapter)
db_session.commit()
# Delete Chapters that are not in the list of chapters_order
statement = select(Chapter).where(Chapter.course_id == course.id)
chapters = db_session.exec(statement).all()
chapter_ids_to_keep = [
chapter_order.chapter_id
for chapter_order in chapters_order.chapter_order_by_ids
]
for chapter in chapters:
if chapter.id not in chapter_ids_to_keep:
db_session.delete(chapter)
db_session.commit()
# If links do not exists, create them
for chapter_order in chapters_order.chapter_order_by_ids:
statement = (
select(CourseChapter)
.where(
CourseChapter.chapter_id == chapter_order.chapter_id,
CourseChapter.course_id == course.id,
)
.order_by(CourseChapter.order)
)
course_chapter = db_session.exec(statement).first()
if not course_chapter:
# Add CourseChapter link
course_chapter = CourseChapter(
chapter_id=chapter_order.chapter_id,
course_id=course.id, # type: ignore
org_id=course.org_id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
order=chapter_order.chapter_id,
)
# Insert CourseChapter link in DB
db_session.add(course_chapter)
db_session.commit()
# Update order of chapters
for chapter_order in chapters_order.chapter_order_by_ids:
statement = (
select(CourseChapter)
.where(
CourseChapter.chapter_id == chapter_order.chapter_id,
CourseChapter.course_id == course.id,
)
.order_by(CourseChapter.order)
)
course_chapter = db_session.exec(statement).first()
if course_chapter:
# Get the order from the index of the chapter_order_by_ids list
course_chapter.order = chapters_order.chapter_order_by_ids.index(
chapter_order
)
db_session.commit()
###########
# Activities
###########
# Delete ChapterActivities that are no longer part of the new order
statement = (
select(ChapterActivity)
.where(
ChapterActivity.course_id == course.id,
ChapterActivity.org_id == course.org_id,
)
.order_by(ChapterActivity.order)
)
chapter_activities = db_session.exec(statement).all()
activity_ids_to_delete = []
for chapter_activity in chapter_activities:
if (
chapter_activity.chapter_id not in chapter_ids_to_keep
or chapter_activity.activity_id not in activity_ids_to_delete
):
activity_ids_to_delete.append(chapter_activity.activity_id)
for activity_id in activity_ids_to_delete:
statement = (
select(ChapterActivity)
.where(
ChapterActivity.activity_id == activity_id,
ChapterActivity.course_id == course.id,
)
.order_by(ChapterActivity.order)
)
chapter_activity = db_session.exec(statement).first()
db_session.delete(chapter_activity)
db_session.commit()
# If links do not exist, create them
chapter_activity_map = {}
for chapter_order in chapters_order.chapter_order_by_ids:
for activity_order in chapter_order.activities_order_by_ids:
if activity_order.activity_id in chapter_activity_map and chapter_activity_map[activity_order.activity_id] != chapter_order.chapter_id:
continue
statement = (
select(ChapterActivity)
.where(
ChapterActivity.chapter_id == chapter_order.chapter_id,
ChapterActivity.activity_id == activity_order.activity_id,
)
.order_by(ChapterActivity.order)
)
chapter_activity = db_session.exec(statement).first()
if not chapter_activity:
# Add ChapterActivity link
chapter_activity = ChapterActivity(
chapter_id=chapter_order.chapter_id,
activity_id=activity_order.activity_id,
org_id=course.org_id,
course_id=course.id, # type: ignore
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
order=activity_order.activity_id,
)
# Insert ChapterActivity link in DB
db_session.add(chapter_activity)
db_session.commit()
chapter_activity_map[activity_order.activity_id] = chapter_order.chapter_id
# Update order of activities
for chapter_order in chapters_order.chapter_order_by_ids:
for activity_order in chapter_order.activities_order_by_ids:
statement = (
select(ChapterActivity)
.where(
ChapterActivity.chapter_id == chapter_order.chapter_id,
ChapterActivity.activity_id == activity_order.activity_id,
)
.order_by(ChapterActivity.order)
)
chapter_activity = db_session.exec(statement).first()
if chapter_activity:
# Get the order from the index of the chapter_order_by_ids list
chapter_activity.order = chapter_order.activities_order_by_ids.index(
activity_order
)
db_session.commit()
return {"detail": "Chapters reordered"}
## 🔒 RBAC Utils ##
async def rbac_check(
request: Request,
course_id: str,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
db_session: Session,
):
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,
db_session,
)
## 🔒 RBAC Utils ##

View file

@ -1,27 +1,23 @@
from datetime import datetime
from typing import List, Literal from typing import List, Literal
from uuid import uuid4 from uuid import uuid4
from pydantic import BaseModel from sqlmodel import Session, select
from src.security.rbac.rbac import authorization_verify_based_on_roles_and_authorship, authorization_verify_if_user_is_anon from src.db.users import AnonymousUser
from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
)
from src.db.collections import (
Collection,
CollectionCreate,
CollectionRead,
CollectionUpdate,
)
from src.db.collections_courses import CollectionCourse
from src.db.courses import Course
from src.services.users.users import PublicUser from src.services.users.users import PublicUser
from fastapi import HTTPException, status, Request from fastapi import HTTPException, status, Request
#### Classes ####################################################
class Collection(BaseModel):
name: str
description: str
courses: List[str] # course_id
public: bool
org_id: str # org_id
class CollectionInDB(Collection):
collection_id: str
authors: List[str] # user_id
#### Classes ####################################################
#################################################### ####################################################
# CRUD # CRUD
@ -29,134 +25,181 @@ class CollectionInDB(Collection):
async def get_collection( async def get_collection(
request: Request, collection_id: str, current_user: PublicUser request: Request, collection_uuid: str, current_user: PublicUser, db_session: Session
): ) -> CollectionRead:
collections = request.app.db["collections"] statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
collection = db_session.exec(statement).first()
collection = await collections.find_one({"collection_id": collection_id})
# verify collection rights
await verify_collection_rights(
request, collection_id, current_user, "read", collection["org_id"]
)
if not collection: if not collection:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist" status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
) )
collection = Collection(**collection) # RBAC check
await rbac_check(
request, collection.collection_uuid, current_user, "read", db_session
)
# add courses to collection # get courses in collection
courses = request.app.db["courses"] statement = (
courseids = [course for course in collection.courses] select(Course)
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
.distinct(Course.id)
)
courses = db_session.exec(statement).all()
collection.courses = [] collection = CollectionRead(**collection.dict(), courses=courses)
collection.courses = courses.find({"course_id": {"$in": courseids}}, {"_id": 0})
collection.courses = [
course for course in await collection.courses.to_list(length=100)
]
return collection return collection
async def create_collection( async def create_collection(
request: Request, collection_object: Collection, current_user: PublicUser request: Request,
): collection_object: CollectionCreate,
collections = request.app.db["collections"] current_user: PublicUser,
db_session: Session,
) -> CollectionRead:
collection = Collection.from_orm(collection_object)
# find if collection already exists using name # RBAC check
isCollectionNameAvailable = await collections.find_one( await rbac_check(request, "collection_x", current_user, "create", db_session)
{"name": collection_object.name}
# Complete the collection object
collection.collection_uuid = f"collection_{uuid4()}"
collection.creation_date = str(datetime.now())
collection.update_date = str(datetime.now())
# Add collection to database
db_session.add(collection)
db_session.commit()
db_session.refresh(collection)
# Link courses to collection
if collection:
for course_id in collection_object.courses:
collection_course = CollectionCourse(
collection_id=int(collection.id), # type: ignore
course_id=course_id,
org_id=int(collection_object.org_id),
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
# Add collection_course to database
db_session.add(collection_course)
db_session.commit()
db_session.refresh(collection)
# Get courses once again
statement = (
select(Course)
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
.distinct(Course.id)
) )
courses = db_session.exec(statement).all()
# TODO collection = CollectionRead(**collection.dict(), courses=courses)
# await verify_collection_rights("*", current_user, "create")
if isCollectionNameAvailable: return CollectionRead.from_orm(collection)
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Collection name already exists",
)
# generate collection_id with uuid4
collection_id = str(f"collection_{uuid4()}")
collection = CollectionInDB(
collection_id=collection_id,
authors=[current_user.user_id],
**collection_object.dict(),
)
collection_in_db = await collections.insert_one(collection.dict())
if not collection_in_db:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Unavailable database",
)
return collection.dict()
async def update_collection( async def update_collection(
request: Request, request: Request,
collection_object: Collection, collection_object: CollectionUpdate,
collection_id: str, collection_uuid: str,
current_user: PublicUser, current_user: PublicUser,
): db_session: Session,
# verify collection rights ) -> CollectionRead:
statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
collections = request.app.db["collections"] collection = db_session.exec(statement).first()
collection = await collections.find_one({"collection_id": collection_id})
await verify_collection_rights(
request, collection_id, current_user, "update", collection["org_id"]
)
if not collection: if not collection:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist" status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
) )
updated_collection = CollectionInDB( # RBAC check
collection_id=collection_id, **collection_object.dict() await rbac_check(
request, collection.collection_uuid, current_user, "update", db_session
) )
await collections.update_one( courses = collection_object.courses
{"collection_id": collection_id}, {"$set": updated_collection.dict()}
del collection_object.courses
# Update only the fields that were passed in
for var, value in vars(collection_object).items():
if value is not None:
setattr(collection, var, value)
collection.update_date = str(datetime.now())
# Update only the fields that were passed in
for var, value in vars(collection_object).items():
if value is not None:
setattr(collection, var, value)
statement = select(CollectionCourse).where(
CollectionCourse.collection_id == collection.id
)
collection_courses = db_session.exec(statement).all()
# Delete all collection_courses
for collection_course in collection_courses:
db_session.delete(collection_course)
# Add new collection_courses
for course in courses or []:
collection_course = CollectionCourse(
collection_id=int(collection.id), # type: ignore
course_id=int(course),
org_id=int(collection.org_id),
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
# Add collection_course to database
db_session.add(collection_course)
db_session.commit()
db_session.refresh(collection)
# Get courses once again
statement = (
select(Course)
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
.distinct(Course.id)
) )
return Collection(**updated_collection.dict()) courses = db_session.exec(statement).all()
collection = CollectionRead(**collection.dict(), courses=courses)
return collection
async def delete_collection( async def delete_collection(
request: Request, collection_id: str, current_user: PublicUser request: Request, collection_uuid: str, current_user: PublicUser, db_session: Session
): ):
collections = request.app.db["collections"] statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
collection = db_session.exec(statement).first()
collection = await collections.find_one({"collection_id": collection_id})
await verify_collection_rights(
request, collection_id, current_user, "delete", collection["org_id"]
)
if not collection: if not collection:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist" status_code=404,
detail="Collection not found",
) )
isDeleted = await collections.delete_one({"collection_id": collection_id}) # RBAC check
await rbac_check(
request, collection.collection_uuid, current_user, "delete", db_session
)
if isDeleted: # delete collection from database
return {"detail": "collection deleted"} db_session.delete(collection)
else: db_session.commit()
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, return {"detail": "Collection deleted"}
detail="Unavailable database",
)
#################################################### ####################################################
@ -167,76 +210,55 @@ async def delete_collection(
async def get_collections( async def get_collections(
request: Request, request: Request,
org_id: str, org_id: str,
current_user: PublicUser, current_user: PublicUser | AnonymousUser,
db_session: Session,
page: int = 1, page: int = 1,
limit: int = 10, limit: int = 10,
): ) -> List[CollectionRead]:
collections = request.app.db["collections"] # RBAC check
await rbac_check(request, "collection_x", current_user, "read", db_session)
statement = (
select(Collection).where(Collection.org_id == org_id).distinct(Collection.id)
)
collections = db_session.exec(statement).all()
if current_user.user_id == "anonymous":
all_collections = collections.find( collections_with_courses = []
{"org_id": org_id, "public": True}, {"_id": 0} for collection in collections:
) statement = (
else: select(Course)
# get all collections from database without ObjectId .join(CollectionCourse, Course.id == CollectionCourse.course_id)
all_collections = ( .distinct(Course.id)
collections.find({"org_id": org_id})
.sort("name", 1)
.skip(10 * (page - 1))
.limit(limit)
) )
courses = db_session.exec(statement).all()
# create list of collections and include courses in each collection collection = CollectionRead(**collection.dict(), courses=courses)
collections_list = [] collections_with_courses.append(collection)
for collection in await all_collections.to_list(length=100):
collection = CollectionInDB(**collection)
collections_list.append(collection)
collection_courses = [course for course in collection.courses] return collections_with_courses
# add courses to collection
courses = request.app.db["courses"]
collection.courses = []
collection.courses = courses.find(
{"course_id": {"$in": collection_courses}}, {"_id": 0}
)
collection.courses = [
course for course in await collection.courses.to_list(length=100)
]
return collections_list
#### Security #################################################### ## 🔒 RBAC Utils ##
async def verify_collection_rights( async def rbac_check(
request: Request, request: Request,
collection_id: str, course_id: str,
current_user: PublicUser, current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"], action: Literal["create", "read", "update", "delete"],
org_id: str, db_session: Session,
): ):
collections = request.app.db["collections"] await authorization_verify_if_user_is_anon(current_user.id)
users = request.app.db["users"]
user = await users.find_one({"user_id": current_user.user_id})
collection = await collections.find_one({"collection_id": collection_id})
if not collection and action != "create" and collection_id != "*":
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
)
# Collections are public by default for now
if current_user.user_id == "anonymous" and action == "read":
return True
await authorization_verify_if_user_is_anon(current_user.user_id)
await authorization_verify_based_on_roles_and_authorship( await authorization_verify_based_on_roles_and_authorship(
request, current_user.user_id, action, user["roles"], collection_id request,
current_user.id,
action,
course_id,
db_session,
) )
#### Security #################################################### ## 🔒 RBAC Utils ##

View file

@ -1,413 +1,391 @@
import json from typing import Literal
from typing import List, Literal, Optional
from uuid import uuid4 from uuid import uuid4
from pydantic import BaseModel from sqlmodel import Session, select
from src.db.organizations import Organization
from src.db.trails import TrailRead
from src.services.trail.trail import get_user_trail_with_orgid
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
from src.db.users import PublicUser, AnonymousUser, User, UserRead
from src.db.courses import (
Course,
CourseCreate,
CourseRead,
CourseUpdate,
FullCourseReadWithTrail,
)
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles,
authorization_verify_based_on_roles_and_authorship, authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_element_is_public, authorization_verify_if_element_is_public,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
) )
from src.services.courses.activities.activities import ActivityInDB
from src.services.courses.thumbnails import upload_thumbnail from src.services.courses.thumbnails import upload_thumbnail
from src.services.users.schemas.users import AnonymousUser from fastapi import HTTPException, Request, UploadFile
from src.services.users.users import PublicUser
from fastapi import HTTPException, Request, status, UploadFile
from datetime import datetime from datetime import datetime
#### Classes ####################################################
async def get_course(
class Course(BaseModel): request: Request,
name: str course_uuid: str,
mini_description: str current_user: PublicUser | AnonymousUser,
description: str db_session: Session,
learnings: List[str] ):
thumbnail: str statement = select(Course).where(Course.course_uuid == course_uuid)
public: bool course = db_session.exec(statement).first()
chapters: List[str]
chapters_content: Optional[List]
org_id: str
class CourseInDB(Course):
course_id: str
creationDate: str
updateDate: str
authors: List[str]
# TODO : wow terrible, fix this
# those models need to be available only in the chapters service
class CourseChapter(BaseModel):
name: str
description: str
activities: list
class CourseChapterInDB(CourseChapter):
coursechapter_id: str
course_id: str
creationDate: str
updateDate: str
#### Classes ####################################################
# TODO : Add courses photo & cover upload and delete
####################################################
# CRUD
####################################################
async def get_course(request: Request, course_id: str, current_user: PublicUser):
courses = request.app.db["courses"]
course = await courses.find_one({"course_id": course_id})
# verify course rights
await verify_rights(request, course_id, current_user, "read")
if not course: if not course:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" status_code=404,
detail="Course not found",
) )
course = Course(**course) # RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
# Get course authors
authors_statement = (
select(User)
.join(ResourceAuthor)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
)
authors = db_session.exec(authors_statement).all()
# convert from User to UserRead
authors = [UserRead.from_orm(author) for author in authors]
course = CourseRead(**course.dict(), authors=authors)
return course return course
async def get_course_meta(request: Request, course_id: str, current_user: PublicUser): async def get_course_meta(
courses = request.app.db["courses"] request: Request,
trails = request.app.db["trails"] course_uuid: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> FullCourseReadWithTrail:
# Avoid circular import
from src.services.courses.chapters import get_course_chapters
course = await courses.find_one({"course_id": course_id}) course_statement = select(Course).where(Course.course_uuid == course_uuid)
activities = request.app.db["activities"] course = db_session.exec(course_statement).first()
# verify course rights
await verify_rights(request, course_id, current_user, "read")
if not course: if not course:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" status_code=404,
detail="Course not found",
) )
coursechapters = await courses.find_one( # RBAC check
{"course_id": course_id}, {"chapters_content": 1, "_id": 0} await rbac_check(request, course.course_uuid, current_user, "read", db_session)
# Get course authors
authors_statement = (
select(User)
.join(ResourceAuthor)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
)
authors = db_session.exec(authors_statement).all()
# convert from User to UserRead
authors = [UserRead.from_orm(author) for author in authors]
course = CourseRead(**course.dict(), authors=authors)
# Get course chapters
chapters = await get_course_chapters(request, course.id, db_session, current_user)
# Trail
trail = await get_user_trail_with_orgid(
request, current_user, course.org_id, db_session
) )
# activities trail = TrailRead.from_orm(trail)
coursechapter_activityIds_global = []
# chapters return FullCourseReadWithTrail(
chapters = {} **course.dict(),
if coursechapters["chapters_content"]: chapters=chapters,
for coursechapter in coursechapters["chapters_content"]: trail=trail if trail else None,
coursechapter = CourseChapterInDB(**coursechapter)
coursechapter_activityIds = []
for activity in coursechapter.activities:
coursechapter_activityIds.append(activity)
coursechapter_activityIds_global.append(activity)
chapters[coursechapter.coursechapter_id] = {
"id": coursechapter.coursechapter_id,
"name": coursechapter.name,
"activityIds": coursechapter_activityIds,
}
# activities
activities_list = {}
for activity in await activities.find(
{"activity_id": {"$in": coursechapter_activityIds_global}}
).to_list(length=100):
activity = ActivityInDB(**activity)
activities_list[activity.activity_id] = {
"id": activity.activity_id,
"name": activity.name,
"type": activity.type,
"content": activity.content,
}
chapters_list_with_activities = []
for chapter in chapters:
chapters_list_with_activities.append(
{
"id": chapters[chapter]["id"],
"name": chapters[chapter]["name"],
"activities": [
activities_list[activity]
for activity in chapters[chapter]["activityIds"]
],
}
)
course = CourseInDB(**course)
# Get activity by user
trail = await trails.find_one(
{"courses.course_id": course_id, "user_id": current_user.user_id}
) )
if trail:
# get only the course where course_id == course_id
trail_course = next(
(course for course in trail["courses"] if course["course_id"] == course_id),
None,
)
else:
trail_course = ""
return {
"course": course,
"chapters": chapters_list_with_activities,
"trail": trail_course,
}
async def create_course( async def create_course(
request: Request, request: Request,
course_object: Course, org_id: int,
org_id: str, course_object: CourseCreate,
current_user: PublicUser, current_user: PublicUser | AnonymousUser,
db_session: Session,
thumbnail_file: UploadFile | None = None, thumbnail_file: UploadFile | None = None,
): ):
courses = request.app.db["courses"] course = Course.from_orm(course_object)
users = request.app.db["users"]
user = await users.find_one({"user_id": current_user.user_id})
# generate course_id with uuid4 # RBAC check
course_id = str(f"course_{uuid4()}") await rbac_check(request, "course_x", current_user, "create", db_session)
# TODO(fix) : the implementation here is clearly not the best one (this entire function) # Complete course object
course_object.org_id = org_id course.org_id = course.org_id
course_object.chapters_content = []
await authorization_verify_based_on_roles( # Get org uuid
request, org_statement = select(Organization).where(Organization.id == org_id)
current_user.user_id, org = db_session.exec(org_statement).first()
"create",
user["roles"],
course_id,
)
course.course_uuid = str(f"course_{uuid4()}")
course.creation_date = str(datetime.now())
course.update_date = str(datetime.now())
# Upload thumbnail
if thumbnail_file and thumbnail_file.filename: if thumbnail_file and thumbnail_file.filename:
name_in_disk = ( name_in_disk = f"{course.course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
)
await upload_thumbnail( await upload_thumbnail(
thumbnail_file, name_in_disk, course_object.org_id, course_id thumbnail_file, name_in_disk, org.org_uuid, course.course_uuid
) )
course_object.thumbnail = name_in_disk course.thumbnail_image = name_in_disk
course = CourseInDB( # Insert course
course_id=course_id, db_session.add(course)
authors=[current_user.user_id], db_session.commit()
creationDate=str(datetime.now()), db_session.refresh(course)
updateDate=str(datetime.now()),
**course_object.dict(), # Make the user the creator of the course
resource_author = ResourceAuthor(
resource_uuid=course.course_uuid,
user_id=current_user.id,
authorship=ResourceAuthorshipEnum.CREATOR,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
) )
course_in_db = await courses.insert_one(course.dict()) # Insert course author
db_session.add(resource_author)
db_session.commit()
db_session.refresh(resource_author)
if not course_in_db: # Get course authors
raise HTTPException( authors_statement = (
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, select(User)
detail="Unavailable database", .join(ResourceAuthor)
) .where(ResourceAuthor.resource_uuid == course.course_uuid)
)
authors = db_session.exec(authors_statement).all()
return course.dict() # convert from User to UserRead
authors = [UserRead.from_orm(author) for author in authors]
course = CourseRead(**course.dict(), authors=authors)
return CourseRead.from_orm(course)
async def update_course_thumbnail( async def update_course_thumbnail(
request: Request, request: Request,
course_id: str, course_uuid: str,
current_user: PublicUser, current_user: PublicUser | AnonymousUser,
db_session: Session,
thumbnail_file: UploadFile | None = None, thumbnail_file: UploadFile | None = None,
): ):
courses = request.app.db["courses"] statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
course = await courses.find_one({"course_id": course_id}) name_in_disk = None
# verify course rights
await verify_rights(request, course_id, current_user, "update")
# TODO(fix) : the implementation here is clearly not the best one
if course:
creationDate = course["creationDate"]
authors = course["authors"]
if thumbnail_file and thumbnail_file.filename:
name_in_disk = f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
course = Course(**course).copy(update={"thumbnail": name_in_disk})
await upload_thumbnail(
thumbnail_file, name_in_disk, course.org_id, course_id
)
updated_course = CourseInDB(
course_id=course_id,
creationDate=creationDate,
authors=authors,
updateDate=str(datetime.now()),
**course.dict(),
)
await courses.update_one(
{"course_id": course_id}, {"$set": updated_course.dict()}
)
return CourseInDB(**updated_course.dict())
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
)
async def update_course(
request: Request, course_object: Course, course_id: str, current_user: PublicUser
):
courses = request.app.db["courses"]
course = await courses.find_one({"course_id": course_id})
# verify course rights
await verify_rights(request, course_id, current_user, "update")
if course:
creationDate = course["creationDate"]
authors = course["authors"]
# get today's date
datetime_object = datetime.now()
updated_course = CourseInDB(
course_id=course_id,
creationDate=creationDate,
authors=authors,
updateDate=str(datetime_object),
**course_object.dict(),
)
await courses.update_one(
{"course_id": course_id}, {"$set": updated_course.dict()}
)
return CourseInDB(**updated_course.dict())
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
)
async def delete_course(request: Request, course_id: str, current_user: PublicUser):
courses = request.app.db["courses"]
course = await courses.find_one({"course_id": course_id})
# verify course rights
await verify_rights(request, course_id, current_user, "delete")
if not course: if not course:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" status_code=404,
detail="Course not found",
) )
isDeleted = await courses.delete_one({"course_id": course_id}) # RBAC check
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
if isDeleted: # Get org uuid
return {"detail": "Course deleted"} org_statement = select(Organization).where(Organization.id == course.org_id)
org = db_session.exec(org_statement).first()
# Upload thumbnail
if thumbnail_file and thumbnail_file.filename:
name_in_disk = f"{course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
await upload_thumbnail(
thumbnail_file, name_in_disk, org.org_uuid, course.course_uuid
)
# Update course
if name_in_disk:
course.thumbnail_image = name_in_disk
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=500,
detail="Unavailable database", detail="Issue with thumbnail upload",
) )
# Complete the course object
course.update_date = str(datetime.now())
#################################################### db_session.add(course)
# Misc db_session.commit()
#################################################### db_session.refresh(course)
# Get course authors
authors_statement = (
select(User)
.join(ResourceAuthor)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
)
authors = db_session.exec(authors_statement).all()
# convert from User to UserRead
authors = [UserRead.from_orm(author) for author in authors]
course = CourseRead(**course.dict(), authors=authors)
return course
async def update_course(
request: Request,
course_object: CourseUpdate,
course_uuid: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
if not course:
raise HTTPException(
status_code=404,
detail="Course not found",
)
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
# Update only the fields that were passed in
for var, value in vars(course_object).items():
if value is not None:
setattr(course, var, value)
# Complete the course object
course.update_date = str(datetime.now())
db_session.add(course)
db_session.commit()
db_session.refresh(course)
# Get course authors
authors_statement = (
select(User)
.join(ResourceAuthor)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
)
authors = db_session.exec(authors_statement).all()
# convert from User to UserRead
authors = [UserRead.from_orm(author) for author in authors]
course = CourseRead(**course.dict(), authors=authors)
return course
async def delete_course(
request: Request,
course_uuid: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
if not course:
raise HTTPException(
status_code=404,
detail="Course not found",
)
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
db_session.delete(course)
db_session.commit()
return {"detail": "Course deleted"}
async def get_courses_orgslug( async def get_courses_orgslug(
request: Request, request: Request,
current_user: PublicUser, current_user: PublicUser | AnonymousUser,
org_slug: str,
db_session: Session,
page: int = 1, page: int = 1,
limit: int = 10, limit: int = 10,
org_slug: str | None = None,
): ):
courses = request.app.db["courses"] statement_public = (
orgs = request.app.db["organizations"] select(Course)
.join(Organization)
.where(Organization.slug == org_slug, Course.public is True)
)
statement_all = (
select(Course).join(Organization).where(Organization.slug == org_slug)
)
# get org_id from slug if current_user.id == 0:
org = await orgs.find_one({"slug": org_slug}) statement = statement_public
if not org:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
)
# show only public courses if user is not logged in
if current_user.user_id == "anonymous":
all_courses = (
courses.find({"org_id": org["org_id"], "public": True})
.sort("name", 1)
.skip(10 * (page - 1))
.limit(limit)
)
else: else:
all_courses = ( # RBAC check
courses.find({"org_id": org["org_id"]}) await authorization_verify_if_user_is_anon(current_user.id)
.sort("name", 1)
.skip(10 * (page - 1)) statement = statement_all
.limit(limit)
courses = db_session.exec(statement)
courses = [CourseRead(**course.dict(), authors=[]) for course in courses]
# for every course, get the authors
for course in courses:
authors_statement = (
select(User)
.join(ResourceAuthor)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
) )
authors = db_session.exec(authors_statement).all()
return [ # convert from User to UserRead
json.loads(json.dumps(course, default=str)) authors = [UserRead.from_orm(author) for author in authors]
for course in await all_courses.to_list(length=100)
] course.authors = authors
return courses
#### Security #################################################### ## 🔒 RBAC Utils ##
async def verify_rights( 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,
): ):
if action == "read": if action == "read":
if current_user.user_id == "anonymous": if current_user.id == 0: # Anonymous user
await authorization_verify_if_element_is_public( await authorization_verify_if_element_is_public(
request, course_id, current_user.user_id, action request, course_uuid, action, db_session
) )
else: else:
users = request.app.db["users"]
user = await users.find_one({"user_id": current_user.user_id})
await authorization_verify_based_on_roles_and_authorship( await authorization_verify_based_on_roles_and_authorship(
request, request, current_user.id, action, course_uuid, db_session
current_user.user_id,
action,
user["roles"],
course_id,
) )
else: else:
users = request.app.db["users"] await authorization_verify_if_user_is_anon(current_user.id)
user = await users.find_one({"user_id": current_user.user_id})
await authorization_verify_if_user_is_anon(current_user.user_id)
await authorization_verify_based_on_roles_and_authorship( await authorization_verify_based_on_roles_and_authorship(
request, request,
current_user.user_id, current_user.id,
action, action,
user["roles"], course_uuid,
course_id, db_session,
) )
#### Security #################################################### ## 🔒 RBAC Utils ##

View file

@ -16,3 +16,5 @@ def isDevModeEnabledOrRaise():
return True return True
else: else:
raise HTTPException(status_code=403, detail="Development mode is disabled") raise HTTPException(status_code=403, detail="Development mode is disabled")

View file

@ -0,0 +1,275 @@
import datetime
from fastapi import Request
from sqlmodel import Session, select
from src.db.blocks import Block, BlockTypeEnum
from src.db.chapter_activities import ChapterActivity
from src.db.activities import Activity, ActivitySubTypeEnum, ActivityTypeEnum
from src.db.course_chapters import CourseChapter
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
from src.db.user_organizations import UserOrganization
from src.db.chapters import Chapter
from src.db.courses import Course
from src.db.users import User
from src.db.organizations import Organization
async def start_migrate_from_mongo(request: Request, db_session: Session):
orgs = request.app.db["organizations"]
## ----> Organizations migration
org_db_list = await orgs.find().to_list(length=100)
for org in org_db_list:
org_to_add = Organization(
name=org["name"],
description=org["description"],
slug=org["slug"],
logo_image=org["logo"],
email=org["email"],
org_uuid=org["org_id"],
creation_date=str(datetime.datetime.now()),
update_date=str(datetime.datetime.now()),
)
db_session.add(org_to_add)
db_session.commit()
print("Migrated organizations.")
## ----> Users migration
users = request.app.db["users"]
users_db_list = await users.find().to_list(length=100)
for user in users_db_list:
user_to_add = User(
email=user["email"],
username=user["username"],
first_name="",
last_name="",
user_uuid=user["user_id"],
password=user["password"],
creation_date=user["creation_date"],
update_date=user["update_date"],
)
db_session.add(user_to_add)
db_session.commit()
# Link Orgs to users and make them owners
for org in user["orgs"]:
statement = select(Organization).where(
Organization.org_uuid == org["org_id"]
)
org_from_db = db_session.exec(statement).first()
statement = select(User).where(User.user_uuid == user["user_id"])
user_from_db = db_session.exec(statement).first()
user_org_object = UserOrganization(
user_id=user_from_db.id, # type: ignore
org_id=org_from_db.id if org_from_db is not None else None, # type: ignore
role_id=1,
creation_date=str(datetime.datetime.now()),
update_date=str(datetime.datetime.now()),
)
db_session.add(user_org_object)
db_session.commit()
print("Migrated users and linked them to orgs.")
## ----> Courses migration
courses = request.app.db["courses"]
courses_db_list = await courses.find().to_list(length=300)
for course in courses_db_list:
# Get the organization id
statement = select(Organization).where(
Organization.org_uuid == course["org_id"]
)
org_from_db = db_session.exec(statement).first()
course_to_add = Course(
name=course["name"],
description=course["description"],
about=course["description"],
learnings="",
course_uuid=course["course_id"],
thumbnail_image=course["thumbnail"],
tags="",
org_id=org_from_db.id if org_from_db is not None else None, # type: ignore
public=course["public"],
creation_date=str(course["creationDate"]),
update_date=str(course["updateDate"]),
)
db_session.add(course_to_add)
db_session.commit()
# Get this course
statement = select(Course).where(Course.course_uuid == course["course_id"])
course_from_db = db_session.exec(statement).first()
# Add Authorship
authors = course["authors"]
for author in authors:
# Get the user id
statement = select(User).where(User.user_uuid == author)
user_from_db = db_session.exec(statement).first()
authorship = ResourceAuthor(
resource_uuid=course_from_db.course_uuid, # type: ignore
user_id=user_from_db.id if user_from_db is not None else None, # type: ignore
authorship=ResourceAuthorshipEnum.CREATOR,
creation_date=str(datetime.datetime.now()),
update_date=str(datetime.datetime.now()),
)
db_session.add(authorship)
db_session.commit()
print("Added authorship.")
## ----> Chapters migration & Link
chapter_object = course["chapters_content"]
order = 0
for chapter in chapter_object:
chapter_to_add = Chapter(
name=chapter["name"],
description=chapter["description"],
chapter_uuid=chapter["coursechapter_id"].replace(
"coursechapter", "chapter"
),
org_id=org_from_db.id if org_from_db is not None else None, # type: ignore
course_id=course_from_db.id, # type: ignore
creation_date=str(datetime.datetime.now()),
update_date=str(datetime.datetime.now()),
)
db_session.add(chapter_to_add)
db_session.commit()
# Get this chapter
statement = select(Chapter).where(
Chapter.chapter_uuid
== chapter["coursechapter_id"].replace("coursechapter", "chapter")
)
chapter_from_db = db_session.exec(statement).first()
# Link chapter to course
coursechapter_to_add = CourseChapter(
chapter_id=chapter_from_db.id, # type: ignore
course_id=course_from_db.id, # type: ignore
order=order,
org_id=org_from_db.id if org_from_db is not None else None, # type: ignore
creation_date=str(datetime.datetime.now()),
update_date=str(datetime.datetime.now()),
)
db_session.add(coursechapter_to_add)
db_session.commit()
order += 1
## ----> Activities migration
activities = request.app.db["activities"]
activities_db_list = await activities.find(
{"coursechapter_id": chapter["coursechapter_id"]}
).to_list(length=100)
activity_order = 0
for activity in activities_db_list:
type_to_use = ActivityTypeEnum.TYPE_CUSTOM
sub_type_to_use = ActivityTypeEnum.TYPE_CUSTOM
if activity["type"] == "video":
type_to_use = ActivityTypeEnum.TYPE_VIDEO
sub_type_to_use = ActivitySubTypeEnum.SUBTYPE_VIDEO_HOSTED
if "external_video" in activity["content"]:
type_to_use = ActivityTypeEnum.TYPE_VIDEO
sub_type_to_use = ActivitySubTypeEnum.SUBTYPE_VIDEO_YOUTUBE
if activity["type"] == "documentpdf":
type_to_use = ActivityTypeEnum.TYPE_DOCUMENT
sub_type_to_use = ActivitySubTypeEnum.SUBTYPE_DOCUMENT_PDF
if activity["type"] == "dynamic":
type_to_use = ActivityTypeEnum.TYPE_DYNAMIC
sub_type_to_use = ActivitySubTypeEnum.SUBTYPE_DYNAMIC_PAGE
activity_to_add = Activity(
name=activity["name"],
activity_uuid=activity["activity_id"],
version=1,
published_version=1,
activity_type=type_to_use,
content=activity["content"],
activity_sub_type=sub_type_to_use,
chapter_id=chapter_from_db.id, # type: ignore
org_id=org_from_db.id if org_from_db is not None else None, # type: ignore
course_id=course_from_db.id, # type: ignore
creation_date=str(activity["creationDate"]),
update_date=str(activity["updateDate"]),
)
db_session.add(activity_to_add)
db_session.commit()
# Link activity to chapter
statement = select(Activity).where(
Activity.activity_uuid == activity["activity_id"]
)
activity_from_db = db_session.exec(statement).first()
activitychapter_to_add = ChapterActivity(
chapter_id=chapter_from_db.id, # type: ignore
activity_id=activity_from_db.id, # type: ignore
order=activity_order,
course_id=course_from_db.id, # type: ignore
org_id=org_from_db.id if org_from_db is not None else None, # type: ignore
creation_date=str(datetime.datetime.now()),
update_date=str(datetime.datetime.now()),
)
db_session.add(activitychapter_to_add)
db_session.commit()
activity_order += 1
## ----> Blocks migration
blocks = request.app.db["blocks"]
blocks_db_list = await blocks.find(
{"activity_id": activity["activity_id"]}
).to_list(length=200)
for block in blocks_db_list:
type_to_use = BlockTypeEnum.BLOCK_CUSTOM
if block["block_type"] == "imageBlock":
type_to_use = BlockTypeEnum.BLOCK_IMAGE
if block["block_type"] == "videoBlock":
type_to_use = BlockTypeEnum.BLOCK_VIDEO
if block["block_type"] == "pdfBlock":
type_to_use = BlockTypeEnum.BLOCK_DOCUMENT_PDF
print('block', block)
block_to_add = Block(
block_uuid=block["block_id"],
content=block["block_data"],
block_type=type_to_use,
activity_id=activity_from_db.id, # type: ignore
org_id=org_from_db.id if org_from_db is not None else None, # type: ignore
course_id=course_from_db.id, # type: ignore
chapter_id=chapter_from_db.id, # type: ignore
creation_date=str(datetime.datetime.now()),
update_date=str(datetime.datetime.now()),
)
db_session.add(block_to_add)
db_session.commit()
return "Migration successfull."

View file

@ -1,214 +0,0 @@
import os
import requests
from datetime import datetime
from uuid import uuid4
from fastapi import Request
from src.security.security import security_hash_password
from src.services.courses.chapters import CourseChapter, create_coursechapter
from src.services.courses.activities.activities import Activity, create_activity
from src.services.users.users import PublicUser, UserInDB
from src.services.orgs.orgs import Organization, create_org
from src.services.roles.schemas.roles import Permission, Elements, RoleInDB
from src.services.courses.courses import CourseInDB
from faker import Faker
async def create_initial_data(request: Request):
fake = Faker(['en_US'])
fake_multilang = Faker(
['en_US', 'de_DE', 'ja_JP', 'es_ES', 'it_IT', 'pt_BR', 'ar_PS'])
# Create users
########################################
database_users = request.app.db["users"]
await database_users.delete_many({})
users = []
admin_user = UserInDB(
user_id="user_admin",
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
roles= [],
orgs=[],
username="admin",
email="admin@admin.admin",
password=str(await security_hash_password("admin")),
)
await database_users.insert_one(admin_user.dict())
# find admin user
users = request.app.db["users"]
admin_user = await users.find_one({"username": "admin"})
if admin_user:
admin_user = UserInDB(**admin_user)
current_user = PublicUser(**admin_user.dict())
else:
raise Exception("Admin user not found")
# Create roles
########################################
database_roles = request.app.db["roles"]
await database_roles.delete_many({})
roles = []
admin_role = RoleInDB(
name="Admin",
description="Admin",
elements=Elements(
courses=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
users=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
houses=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
collections=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
organizations=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
coursechapters=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
activities=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
),
org_id="org_test",
role_id="role_admin",
created_at=str(datetime.now()),
updated_at=str(datetime.now()),
)
roles.append(admin_role)
for role in roles:
database_roles.insert_one(role.dict())
# Create organizations
########################################
database_orgs = request.app.db["organizations"]
await database_orgs.delete_many({})
organizations = []
for i in range(0, 2):
company = fake.company()
# remove whitespace and special characters and make lowercase
slug = ''.join(e for e in company if e.isalnum()).lower()
org = Organization(
name=company,
description=fake.unique.text(),
email=fake.unique.email(),
slug=slug,
logo="",
default=False
)
organizations.append(org)
await create_org(request, org, current_user)
# Generate Courses and CourseChapters
########################################
database_courses = request.app.db["courses"]
await database_courses.delete_many({})
courses = []
orgs = request.app.db["organizations"]
if await orgs.count_documents({}) > 0:
for org in await orgs.find().to_list(length=100):
for i in range(0, 5):
# get image in BinaryIO format from unsplash and save it to disk
image = requests.get(
"https://source.unsplash.com/random/800x600")
with open("thumbnail.jpg", "wb") as f:
f.write(image.content)
course_id = f"course_{uuid4()}"
course = CourseInDB(
name=fake_multilang.unique.sentence(),
description=fake_multilang.unique.text(),
mini_description=fake_multilang.unique.text(),
thumbnail="thumbnail",
org_id=org['org_id'],
learnings=[fake_multilang.unique.sentence()
for i in range(0, 5)],
public=True,
chapters=[],
course_id=course_id,
creationDate=str(datetime.now()),
updateDate=str(datetime.now()),
authors=[current_user.user_id],
chapters_content=[],
)
courses = request.app.db["courses"]
name_in_disk = f"test_mock{course_id}.jpeg"
image = requests.get(
"https://source.unsplash.com/random/800x600/?img=1")
# check if folder exists and create it if not
if not os.path.exists("content/uploads/img"):
os.makedirs("content/uploads/img")
with open(f"content/uploads/img/{name_in_disk}", "wb") as f:
f.write(image.content)
course.thumbnail = name_in_disk
course = CourseInDB(**course.dict())
await courses.insert_one(course.dict())
# create chapters
for i in range(0, 5):
coursechapter = CourseChapter(
name=fake_multilang.unique.sentence(),
description=fake_multilang.unique.text(),
activities=[],
)
coursechapter = await create_coursechapter(request,coursechapter, course_id, current_user)
if coursechapter:
# create activities
for i in range(0, 5):
activity = Activity(
name=fake_multilang.unique.sentence(),
type="dynamic",
content={},
)
activity = await create_activity(request,activity, "org_test", coursechapter['coursechapter_id'], current_user)

View file

@ -1,35 +1,15 @@
from datetime import datetime from datetime import datetime
from uuid import uuid4 from uuid import uuid4
from fastapi import HTTPException, Request, status from fastapi import HTTPException, Request
from pydantic import BaseModel from sqlalchemy import desc
import requests from sqlmodel import Session, select
from src.db.install import Install, InstallRead
from src.db.organizations import Organization, OrganizationCreate
from src.db.roles import Permission, Rights, Role, RoleTypeEnum
from src.db.user_organizations import UserOrganization
from src.db.users import User, UserCreate, UserRead
from config.config import get_learnhouse_config from config.config import get_learnhouse_config
from src.security.security import security_hash_password from src.security.security import security_hash_password
from src.services.courses.activities.activities import Activity, create_activity
from src.services.courses.chapters import create_coursechapter, CourseChapter
from src.services.courses.courses import CourseInDB
from src.services.orgs.schemas.orgs import Organization, OrganizationInDB
from faker import Faker
from src.services.roles.schemas.roles import Elements, Permission, RoleInDB
from src.services.users.schemas.users import (
PublicUser,
User,
UserInDB,
UserOrganization,
UserRolesInOrganization,
UserWithPassword,
)
class InstallInstance(BaseModel):
install_id: str
created_date: str
updated_date: str
step: int
data: dict
async def isInstallModeEnabled(): async def isInstallModeEnabled():
@ -44,37 +24,31 @@ async def isInstallModeEnabled():
) )
async def create_install_instance(request: Request, data: dict): async def create_install_instance(request: Request, data: dict, db_session: Session):
installs = request.app.db["installs"] install = Install.from_orm(data)
# get install_id # complete install instance
install_id = str(f"install_{uuid4()}") install.install_uuid = str(f"install_{uuid4()}")
created_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") install.update_date = str(datetime.now())
updated_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") install.creation_date = str(datetime.now())
step = 1
# create install # insert install instance
install = InstallInstance( db_session.add(install)
install_id=install_id,
created_date=created_date,
updated_date=updated_date,
step=step,
data=data,
)
# insert install # commit changes
installs.insert_one(install.dict()) db_session.commit()
# refresh install instance
db_session.refresh(install)
install = InstallRead.from_orm(install)
return install return install
async def get_latest_install_instance(request: Request): async def get_latest_install_instance(request: Request, db_session: Session):
installs = request.app.db["installs"] statement = select(Install).order_by(desc(Install.creation_date)).limit(1)
install = db_session.exec(statement).first()
# get latest created install instance using find_one
install = await installs.find_one(
sort=[("created_date", -1)], limit=1, projection={"_id": 0}
)
if install is None: if install is None:
raise HTTPException( raise HTTPException(
@ -82,37 +56,35 @@ async def get_latest_install_instance(request: Request):
detail="No install instance found", detail="No install instance found",
) )
else: install = InstallRead.from_orm(install)
install = InstallInstance(**install)
return install return install
async def update_install_instance(request: Request, data: dict, step: int): async def update_install_instance(
installs = request.app.db["installs"] request: Request, data: dict, step: int, db_session: Session
):
# get latest created install statement = select(Install).order_by(desc(Install.creation_date)).limit(1)
install = await installs.find_one( install = db_session.exec(statement).first()
sort=[("created_date", -1)], limit=1, projection={"_id": 0}
)
if install is None: if install is None:
return None raise HTTPException(
status_code=404,
else: detail="No install instance found",
# update install
install["data"] = data
install["step"] = step
install["updated_date"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# update install
await installs.update_one(
{"install_id": install["install_id"]}, {"$set": install}
) )
install = InstallInstance(**install) install.step = step
install.data = data
return install # commit changes
db_session.commit()
# refresh install instance
db_session.refresh(install)
install = InstallRead.from_orm(install)
return install
############################################################################################################ ############################################################################################################
@ -121,24 +93,34 @@ async def update_install_instance(request: Request, data: dict, step: int):
# Install Default roles # Install Default roles
async def install_default_elements(request: Request, data: dict): async def install_default_elements(request: Request, data: dict, db_session: Session):
roles = request.app.db["roles"] # remove all default roles
statement = select(Role).where(Role.role_type == RoleTypeEnum.TYPE_GLOBAL)
roles = db_session.exec(statement).all()
# check if default roles ADMIN_ROLE and USER_ROLE already exist for role in roles:
admin_role = await roles.find_one({"role_id": "role_admin"}) db_session.delete(role)
user_role = await roles.find_one({"role_id": "role_member"})
if admin_role is not None or user_role is not None: db_session.commit()
# Check if default roles already exist
statement = select(Role).where(Role.role_type == RoleTypeEnum.TYPE_GLOBAL)
roles = db_session.exec(statement).all()
if roles and len(roles) == 3:
raise HTTPException( raise HTTPException(
status_code=400, status_code=409,
detail="Default roles already exist", detail="Default roles already exist",
) )
# get default roles # Create default roles
ADMIN_ROLE = RoleInDB( role_global_admin = Role(
name="Admin Role", name="Admin",
description="This role grants all permissions to the user", description="Standard Admin Role",
elements=Elements( id=1,
role_type=RoleTypeEnum.TYPE_GLOBAL,
role_uuid="role_global_admin",
rights=Rights(
courses=Permission( courses=Permission(
action_create=True, action_create=True,
action_read=True, action_read=True,
@ -151,12 +133,6 @@ async def install_default_elements(request: Request, data: dict):
action_update=True, action_update=True,
action_delete=True, action_delete=True,
), ),
houses=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
collections=Permission( collections=Permission(
action_create=True, action_create=True,
action_read=True, action_read=True,
@ -182,16 +158,65 @@ async def install_default_elements(request: Request, data: dict):
action_delete=True, action_delete=True,
), ),
), ),
org_id="*", creation_date=str(datetime.now()),
role_id="role_admin", update_date=str(datetime.now()),
created_at=str(datetime.now()),
updated_at=str(datetime.now()),
) )
USER_ROLE = RoleInDB( role_global_maintainer = Role(
name="Member Role", name="Maintainer",
description="This role grants read-only permissions to the user", description="Standard Maintainer Role",
elements=Elements( id=2,
role_type=RoleTypeEnum.TYPE_GLOBAL,
role_uuid="role_global_maintainer",
rights=Rights(
courses=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
users=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
collections=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
organizations=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
coursechapters=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
activities=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
),
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
role_global_user = Role(
name="User",
description="Standard User Role",
role_type=RoleTypeEnum.TYPE_GLOBAL,
role_uuid="role_global_user",
id=3,
rights=Rights(
courses=Permission( courses=Permission(
action_create=False, action_create=False,
action_read=True, action_read=True,
@ -199,13 +224,7 @@ async def install_default_elements(request: Request, data: dict):
action_delete=False, action_delete=False,
), ),
users=Permission( users=Permission(
action_create=False, action_create=True,
action_read=True,
action_update=False,
action_delete=False,
),
houses=Permission(
action_create=False,
action_read=True, action_read=True,
action_update=False, action_update=False,
action_delete=False, action_delete=False,
@ -235,185 +254,122 @@ async def install_default_elements(request: Request, data: dict):
action_delete=False, action_delete=False,
), ),
), ),
org_id="*", creation_date=str(datetime.now()),
role_id="role_member", update_date=str(datetime.now()),
created_at=str(datetime.now()),
updated_at=str(datetime.now()),
) )
try: # Serialize rights to JSON
# insert default roles role_global_admin.rights = role_global_admin.rights.dict() # type: ignore
await roles.insert_many([USER_ROLE.dict(), ADMIN_ROLE.dict()]) role_global_maintainer.rights = role_global_maintainer.rights.dict() # type: ignore
return True role_global_user.rights = role_global_user.rights.dict() # type: ignore
except Exception: # Insert roles in DB
raise HTTPException( db_session.add(role_global_admin)
status_code=400, db_session.add(role_global_maintainer)
detail="Error while inserting default roles", db_session.add(role_global_user)
)
# commit changes
db_session.commit()
# refresh roles
db_session.refresh(role_global_admin)
return True
# Organization creation # Organization creation
async def install_create_organization( async def install_create_organization(
request: Request, request: Request, org_object: OrganizationCreate, db_session: Session
org_object: Organization,
): ):
orgs = request.app.db["organizations"] org = Organization.from_orm(org_object)
request.app.db["users"]
# find if org already exists using name # Complete the org object
org.org_uuid = f"org_{uuid4()}"
org.creation_date = str(datetime.now())
org.update_date = str(datetime.now())
isOrgAvailable = await orgs.find_one({"slug": org_object.slug.lower()}) db_session.add(org)
db_session.commit()
db_session.refresh(org)
if isOrgAvailable: return org
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Organization slug already exists",
)
# generate org_id with uuid4
org_id = str(f"org_{uuid4()}")
org = OrganizationInDB(org_id=org_id, **org_object.dict())
org_in_db = await orgs.insert_one(org.dict())
if not org_in_db:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Unavailable database",
)
return org.dict()
async def install_create_organization_user( async def install_create_organization_user(
request: Request, user_object: UserWithPassword, org_slug: str request: Request, user_object: UserCreate, org_slug: str, db_session: Session
): ):
users = request.app.db["users"] user = User.from_orm(user_object)
isUsernameAvailable = await users.find_one({"username": user_object.username}) # Complete the user object
isEmailAvailable = await users.find_one({"email": user_object.email}) user.user_uuid = f"user_{uuid4()}"
user.password = await security_hash_password(user_object.password)
user.email_verified = False
user.creation_date = str(datetime.now())
user.update_date = str(datetime.now())
if isUsernameAvailable: # Verifications
# Check if Organization exists
statement = select(Organization).where(Organization.slug == org_slug)
org = db_session.exec(statement)
if not org.first():
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Username already exists" status_code=400,
detail="Organization does not exist",
) )
if isEmailAvailable: # Username
statement = select(User).where(User.username == user.username)
result = db_session.exec(statement)
if result.first():
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Email already exists" status_code=400,
detail="Username already exists",
) )
# Generate user_id with uuid4 # Email
user_id = str(f"user_{uuid4()}") statement = select(User).where(User.email == user.email)
result = db_session.exec(statement)
# Set the username & hash the password if result.first():
user_object.username = user_object.username.lower()
user_object.password = await security_hash_password(user_object.password)
# Get org_id from org_slug
orgs = request.app.db["organizations"]
# Check if the org exists
isOrgExists = await orgs.find_one({"slug": org_slug})
# If the org does not exist, raise an error
if not isOrgExists:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, status_code=400,
detail="You are trying to create a user in an organization that does not exist", detail="Email already exists",
) )
org_id = isOrgExists["org_id"] # Exclude unset values
user_data = user.dict(exclude_unset=True)
for key, value in user_data.items():
setattr(user, key, value)
# Create initial orgs list with the org_id passed in # Add user to database
orgs = [UserOrganization(org_id=org_id, org_role="owner")] db_session.add(user)
db_session.commit()
db_session.refresh(user)
# Give role
roles = [UserRolesInOrganization(role_id="role_admin", org_id=org_id)]
# Create the user
user = UserInDB( # get org id
user_id=user_id, statement = select(Organization).where(Organization.slug == org_slug)
org = db_session.exec(statement)
org = org.first()
org_id = org.id if org else 0
# Link user and organization
user_organization = UserOrganization(
user_id=user.id if user.id else 0,
org_id=org_id or 0,
role_id=1,
creation_date=str(datetime.now()), creation_date=str(datetime.now()),
update_date=str(datetime.now()), update_date=str(datetime.now()),
orgs=orgs,
roles=roles,
**user_object.dict(),
) )
# Insert the user into the database db_session.add(user_organization)
await users.insert_one(user.dict()) db_session.commit()
db_session.refresh(user_organization)
return User(**user.dict()) user = UserRead.from_orm(user)
return user
async def create_sample_data(org_slug: str, username: str, request: Request):
Faker(["en_US"])
fake_multilang = Faker(
["en_US", "de_DE", "ja_JP", "es_ES", "it_IT", "pt_BR", "ar_PS"]
)
users = request.app.db["users"]
orgs = request.app.db["organizations"]
user = await users.find_one({"username": username})
org = await orgs.find_one({"slug": org_slug.lower()})
user_id = user["user_id"]
org_id = org["org_id"]
current_user = PublicUser(**user)
for i in range(0, 5):
# get image in BinaryIO format from unsplash and save it to disk
image = requests.get("https://source.unsplash.com/random/800x600")
with open("thumbnail.jpg", "wb") as f:
f.write(image.content)
course_id = f"course_{uuid4()}"
course = CourseInDB(
name=fake_multilang.unique.sentence(),
description=fake_multilang.unique.text(),
mini_description=fake_multilang.unique.text(),
thumbnail="thumbnail",
org_id=org_id,
learnings=[fake_multilang.unique.sentence() for i in range(0, 5)],
public=True,
chapters=[],
course_id=course_id,
creationDate=str(datetime.now()),
updateDate=str(datetime.now()),
authors=[user_id],
chapters_content=[],
)
courses = request.app.db["courses"]
course = CourseInDB(**course.dict())
await courses.insert_one(course.dict())
# create chapters
for i in range(0, 5):
coursechapter = CourseChapter(
name=fake_multilang.unique.sentence(),
description=fake_multilang.unique.text(),
activities=[],
)
coursechapter = await create_coursechapter(
request, coursechapter, course_id, current_user
)
if coursechapter:
# create activities
for i in range(0, 5):
activity = Activity(
name=fake_multilang.unique.sentence(),
type="dynamic",
content={},
)
activity = await create_activity(
request,
activity,
org_id,
coursechapter["coursechapter_id"],
current_user,
)

View file

@ -3,13 +3,13 @@ from uuid import uuid4
from src.services.utils.upload_content import upload_content from src.services.utils.upload_content import upload_content
async def upload_org_logo(logo_file, org_id): async def upload_org_logo(logo_file, org_uuid):
contents = logo_file.file.read() contents = logo_file.file.read()
name_in_disk = f"{uuid4()}.{logo_file.filename.split('.')[-1]}" name_in_disk = f"{uuid4()}.{logo_file.filename.split('.')[-1]}"
await upload_content( await upload_content(
"logos", "logos",
org_id, org_uuid,
contents, contents,
name_in_disk, name_in_disk,
) )

View file

@ -1,230 +1,287 @@
import json from datetime import datetime
from typing import Literal from typing import Literal
from uuid import uuid4 from uuid import uuid4
from sqlmodel import Session, select
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles, authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
) )
from src.services.orgs.logos import upload_org_logo from src.db.users import AnonymousUser, PublicUser
from src.services.orgs.schemas.orgs import ( from src.db.user_organizations import UserOrganization
from src.db.organizations import (
Organization, Organization,
OrganizationInDB, OrganizationCreate,
PublicOrganization, OrganizationRead,
OrganizationUpdate,
) )
from src.services.users.schemas.users import UserOrganization from src.services.orgs.logos import upload_org_logo
from src.services.users.users import PublicUser
from fastapi import HTTPException, UploadFile, status, Request from fastapi import HTTPException, UploadFile, status, Request
async def get_organization(request: Request, org_id: str): async def get_organization(
orgs = request.app.db["organizations"] request: Request,
org_id: str,
db_session: Session,
current_user: PublicUser | AnonymousUser,
):
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = await orgs.find_one({"org_id": org_id}) org = result.first()
if not org: if not org:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" status_code=404,
detail="Organization not found",
) )
org = PublicOrganization(**org) # RBAC check
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
org = OrganizationRead.from_orm(org)
return org return org
async def get_organization_by_slug(request: Request, org_slug: str): async def get_organization_by_slug(
orgs = request.app.db["organizations"] request: Request,
org_slug: str,
db_session: Session,
current_user: PublicUser | AnonymousUser,
):
statement = select(Organization).where(Organization.slug == org_slug)
result = db_session.exec(statement)
org = await orgs.find_one({"slug": org_slug}) org = result.first()
if not org: if not org:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" status_code=404,
detail="Organization not found",
) )
org = PublicOrganization(**org) # RBAC check
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
org = OrganizationRead.from_orm(org)
return org return org
async def create_org( async def create_org(
request: Request, org_object: Organization, current_user: PublicUser request: Request,
org_object: OrganizationCreate,
current_user: PublicUser | AnonymousUser,
db_session: Session,
): ):
orgs = request.app.db["organizations"] statement = select(Organization).where(Organization.slug == org_object.slug)
user = request.app.db["users"] result = db_session.exec(statement)
# find if org already exists using name org = result.first()
isOrgAvailable = await orgs.find_one({"slug": org_object.slug})
if isOrgAvailable: if org:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Organization already exists",
)
org = Organization.from_orm(org_object)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "create", db_session)
# Complete the org object
org.org_uuid = f"org_{uuid4()}"
org.creation_date = str(datetime.now())
org.update_date = str(datetime.now())
db_session.add(org)
db_session.commit()
db_session.refresh(org)
# Link user to org
user_org = UserOrganization(
user_id=int(current_user.id),
org_id=int(org.id if org.id else 0),
role_id=1,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
db_session.add(user_org)
db_session.commit()
db_session.refresh(user_org)
return OrganizationRead.from_orm(org)
async def update_org(
request: Request,
org_object: OrganizationUpdate,
org_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization slug not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "update", db_session)
# Verify if the new slug is already in use
statement = select(Organization).where(Organization.slug == org_object.slug)
result = db_session.exec(statement)
slug_available = result.first()
if slug_available and slug_available.id != org_id:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, status_code=status.HTTP_409_CONFLICT,
detail="Organization slug already exists", detail="Organization slug already exists",
) )
# generate org_id with uuid4 # Update only the fields that were passed in
org_id = str(f"org_{uuid4()}") for var, value in vars(org_object).items():
if value is not None:
setattr(org, var, value)
# force lowercase slug # Complete the org object
org_object.slug = org_object.slug.lower() org.update_date = str(datetime.now())
org = OrganizationInDB( db_session.add(org)
org_id=org_id, **org_object.dict() db_session.commit()
) db_session.refresh(org)
org_in_db = await orgs.insert_one(org.dict()) org = OrganizationRead.from_orm(org)
user_organization: UserOrganization = UserOrganization( return org
org_id=org_id, org_role="owner"
)
# add org to user
await user.update_one(
{"user_id": current_user.user_id},
{"$addToSet": {"orgs": user_organization.dict()}},
)
# add role admin to org
await user.update_one(
{"user_id": current_user.user_id},
{"$addToSet": {"roles": {"org_id": org_id, "role_id": "role_admin"}}},
)
if not org_in_db:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Unavailable database",
)
return org.dict()
async def update_org(
request: Request, org_object: Organization, org_id: str, current_user: PublicUser
):
# verify org rights
await verify_org_rights(request, org_id, current_user, "update")
orgs = request.app.db["organizations"]
await orgs.find_one({"org_id": org_id})
updated_org = OrganizationInDB(org_id=org_id, **org_object.dict())
# update org
await orgs.update_one({"org_id": org_id}, {"$set": updated_org.dict()})
return updated_org.dict()
async def update_org_logo( async def update_org_logo(
request: Request, logo_file: UploadFile, org_id: str, current_user: PublicUser request: Request,
logo_file: UploadFile,
org_id: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
): ):
# verify org rights statement = select(Organization).where(Organization.id == org_id)
await verify_org_rights(request, org_id, current_user, "update") result = db_session.exec(statement)
orgs = request.app.db["organizations"] org = result.first()
await orgs.find_one({"org_id": org_id}) if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
name_in_disk = await upload_org_logo(logo_file, org_id) # RBAC check
await rbac_check(request, org.org_uuid, current_user, "update", db_session)
# update org # Upload logo
await orgs.update_one({"org_id": org_id}, {"$set": {"logo": name_in_disk}}) name_in_disk = await upload_org_logo(logo_file, org.org_uuid)
# Update org
org.logo_image = name_in_disk
# Complete the org object
org.update_date = str(datetime.now())
db_session.add(org)
db_session.commit()
db_session.refresh(org)
return {"detail": "Logo updated"} return {"detail": "Logo updated"}
async def delete_org(request: Request, org_id: str, current_user: PublicUser): async def delete_org(
await verify_org_rights(request, org_id, current_user, "delete") request: Request,
org_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
orgs = request.app.db["organizations"] org = result.first()
org = await orgs.find_one({"org_id": org_id})
if not org: if not org:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" status_code=404,
detail="Organization not found",
) )
isDeleted = await orgs.delete_one({"org_id": org_id}) # RBAC check
await rbac_check(request, org.org_uuid, current_user, "delete", db_session)
# remove org from all users db_session.delete(org)
users = request.app.db["users"] db_session.commit()
await users.update_many({}, {"$pull": {"orgs": {"org_id": org_id}}})
if isDeleted: # Delete links to org
return {"detail": "Org deleted"} statement = select(UserOrganization).where(UserOrganization.org_id == org_id)
else: result = db_session.exec(statement)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, user_orgs = result.all()
detail="Unavailable database",
) for user_org in user_orgs:
db_session.delete(user_org)
db_session.commit()
db_session.refresh(org)
return {"detail": "Organization deleted"}
async def get_orgs_by_user( async def get_orgs_by_user(
request: Request, user_id: str, page: int = 1, limit: int = 10 request: Request,
): db_session: Session,
orgs = request.app.db["organizations"] user_id: str,
user = request.app.db["users"] page: int = 1,
limit: int = 10,
if user_id == "anonymous": ) -> list[Organization]:
# raise error statement = (
raise HTTPException( select(Organization)
status_code=status.HTTP_409_CONFLICT, detail="User not logged in" .join(UserOrganization)
.where(
Organization.id == UserOrganization.org_id,
UserOrganization.user_id == user_id,
) )
# get user orgs
user_orgs = await user.find_one({"user_id": user_id})
org_ids: list[UserOrganization] = []
for org in user_orgs["orgs"]:
if (
org["org_role"] == "owner"
or org["org_role"] == "editor"
or org["org_role"] == "member"
):
org_ids.append(org["org_id"])
# find all orgs where org_id is in org_ids array
all_orgs = (
orgs.find({"org_id": {"$in": org_ids}})
.sort("name", 1)
.skip(10 * (page - 1))
.limit(100)
) )
result = db_session.exec(statement)
return [ orgs = result.all()
json.loads(json.dumps(org, default=str))
for org in await all_orgs.to_list(length=100) return orgs
]
#### Security #################################################### ## 🔒 RBAC Utils ##
async def verify_org_rights( async def rbac_check(
request: Request, request: Request,
org_id: str, org_id: str,
current_user: PublicUser, current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"], action: Literal["create", "read", "update", "delete"],
db_session: Session,
): ):
orgs = request.app.db["organizations"] # Organizations are readable by anyone
users = request.app.db["users"] if action == "read":
return True
user = await users.find_one({"user_id": current_user.user_id}) else:
await authorization_verify_if_user_is_anon(current_user.id)
org = await orgs.find_one({"org_id": org_id}) await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, org_id, db_session
if not org:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
) )
await authorization_verify_if_user_is_anon(current_user.user_id)
await authorization_verify_based_on_roles( ## 🔒 RBAC Utils ##
request, current_user.user_id, action, user["roles"], org_id
)
#### Security ####################################################

View file

@ -1,28 +0,0 @@
from typing import Optional
from pydantic import BaseModel
#### Classes ####################################################
class Organization(BaseModel):
name: str
description: str
email: str
slug: str
logo: Optional[str]
default: Optional[bool] = False
class OrganizationInDB(Organization):
org_id: str
class PublicOrganization(Organization):
name: str
description: str
email: str
slug: str
org_id: str
def __getitem__(self, item):
return getattr(self, item)

View file

@ -1,127 +1,141 @@
from typing import Literal from typing import Literal
from uuid import uuid4 from uuid import uuid4
from src.security.rbac.rbac import authorization_verify_if_user_is_anon from sqlmodel import Session, select
from src.services.roles.schemas.roles import Role, RoleInDB from src.security.rbac.rbac import (
from src.services.users.schemas.users import PublicUser authorization_verify_based_on_roles_and_authorship,
from fastapi import HTTPException, status, Request authorization_verify_if_user_is_anon,
)
from src.db.users import AnonymousUser, PublicUser
from src.db.roles import Role, RoleCreate, RoleRead, RoleUpdate
from fastapi import HTTPException, Request
from datetime import datetime from datetime import datetime
async def create_role(request: Request, role_object: Role, current_user: PublicUser): async def create_role(
roles = request.app.db["roles"] request: Request,
db_session: Session,
role_object: RoleCreate,
current_user: PublicUser,
):
role = Role.from_orm(role_object)
await verify_user_permissions_on_roles(request, current_user, "create", None) # RBAC check
await rbac_check(request, current_user, "create", "role_xxx", db_session)
# create the role object in the database and return the object # Complete the role object
role_id = "role_" + str(uuid4()) role.role_uuid = f"role_{uuid4()}"
role.creation_date = str(datetime.now())
role.update_date = str(datetime.now())
role = RoleInDB( db_session.add(role)
role_id=role_id, db_session.commit()
created_at=str(datetime.now()), db_session.refresh(role)
updated_at=str(datetime.now()),
**role_object.dict()
)
await roles.insert_one(role.dict()) role = RoleRead(**role.dict())
return role return role
async def read_role(request: Request, role_id: str, current_user: PublicUser): async def read_role(
roles = request.app.db["roles"] request: Request, db_session: Session, role_id: str, current_user: PublicUser
):
statement = select(Role).where(Role.id == role_id)
result = db_session.exec(statement)
await verify_user_permissions_on_roles(request, current_user, "read", role_id) role = result.first()
role = RoleInDB(**await roles.find_one({"role_id": role_id})) if not role:
raise HTTPException(
status_code=404,
detail="Role not found",
)
# RBAC check
await rbac_check(request, current_user, "read", role.role_uuid, db_session)
role = RoleRead(**role.dict())
return role return role
async def update_role( async def update_role(
request: Request, role_id: str, role_object: Role, current_user: PublicUser request: Request,
db_session: Session,
role_object: RoleUpdate,
current_user: PublicUser,
): ):
roles = request.app.db["roles"] statement = select(Role).where(Role.id == role_object.role_id)
result = db_session.exec(statement)
await verify_user_permissions_on_roles(request, current_user, "update", role_id) role = result.first()
role_object.updated_at = datetime.now() if not role:
raise HTTPException(
# Update the role object in the database and return the object status_code=404,
updated_role = RoleInDB( detail="Role not found",
**await roles.find_one_and_update(
{"role_id": role_id}, {"$set": role_object.dict()}, return_document=True
) )
# RBAC check
await rbac_check(request, current_user, "update", role.role_uuid, db_session)
# Complete the role object
role.update_date = str(datetime.now())
# Remove the role_id from the role_object
del role_object.role_id
# Update only the fields that were passed in
for var, value in vars(role_object).items():
if value is not None:
setattr(role, var, value)
db_session.add(role)
db_session.commit()
db_session.refresh(role)
role = RoleRead(**role.dict())
return role
async def delete_role(
request: Request, db_session: Session, role_id: str, current_user: PublicUser
):
# RBAC check
await rbac_check(request, current_user, "delete", role_id, db_session)
statement = select(Role).where(Role.id == role_id)
result = db_session.exec(statement)
role = result.first()
if not role:
raise HTTPException(
status_code=404,
detail="Role not found",
)
db_session.delete(role)
db_session.commit()
return "Role deleted"
## 🔒 RBAC Utils ##
async def rbac_check(
request: Request,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
role_uuid: str,
db_session: Session,
):
await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, role_uuid, db_session
) )
return updated_role
## 🔒 RBAC Utils ##
async def delete_role(request: Request, role_id: str, current_user: PublicUser):
roles = request.app.db["roles"]
await verify_user_permissions_on_roles(request, current_user, "delete", role_id)
# Delete the role object in the database and return the object
deleted_role = RoleInDB(**await roles.find_one_and_delete({"role_id": role_id}))
return deleted_role
#### Security ####################################################
async def verify_user_permissions_on_roles(
request: Request,
current_user: PublicUser,
action: Literal["create", "read", "update", "delete"],
role_id: str | None,
):
request.app.db["users"]
roles = request.app.db["roles"]
# If current user is not authenticated
if not current_user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Roles : Not authenticated"
)
await authorization_verify_if_user_is_anon(current_user.user_id)
if action == "create":
if "owner" in [org.org_role for org in current_user.orgs]:
return True
if role_id is not None:
role = RoleInDB(**await roles.find_one({"role_id": role_id}))
if action == "read":
if "owner" in [org.org_role for org in current_user.orgs]:
return True
for org in current_user.orgs:
if org.org_id == role.org_id:
return True
if action == "update":
for org in current_user.orgs:
# If the user is an owner of the organization
if org.org_id == role.org_id:
if org.org_role == "owner" or org.org_role == "editor":
return True
# Can't update a global role
if role.org_id == "*":
return False
if action == "delete":
for org in current_user.orgs:
# If the user is an owner of the organization
if org.org_id == role.org_id:
if org.org_role == "owner":
return True
# Can't delete a global role
if role.org_id == "*":
return False
#### Security ####################################################

View file

@ -1,41 +0,0 @@
from typing import Literal
from pydantic import BaseModel
# Database Models
class Permission(BaseModel):
action_create: bool
action_read: bool
action_update: bool
action_delete: bool
def __getitem__(self, item):
return getattr(self, item)
class Elements(BaseModel):
courses: Permission
users: Permission
houses: Permission
collections: Permission
organizations: Permission
coursechapters: Permission
activities: Permission
def __getitem__(self, item):
return getattr(self, item)
class Role(BaseModel):
name: str
description: str
elements : Elements
org_id: str | Literal["*"]
class RoleInDB(Role):
role_id: str
created_at: str
updated_at: str

View file

@ -1,286 +1,423 @@
from datetime import datetime from datetime import datetime
from typing import List, Literal, Optional
from uuid import uuid4 from uuid import uuid4
from src.db.chapter_activities import ChapterActivity
from fastapi import HTTPException, Request, status from fastapi import HTTPException, Request, status
from pydantic import BaseModel from sqlmodel import Session, select
from src.services.courses.chapters import get_coursechapters_meta from src.db.activities import Activity
from src.services.orgs.orgs import PublicOrganization from src.db.courses import Course
from src.db.trail_runs import TrailRun, TrailRunRead
from src.services.users.users import PublicUser from src.db.trail_steps import TrailStep
from src.db.trails import Trail, TrailCreate, TrailRead
#### Classes #################################################### from src.db.users import PublicUser
class ActivityData(BaseModel): async def create_user_trail(
activity_id: str request: Request,
activity_type: str user: PublicUser,
data: Optional[dict] trail_object: TrailCreate,
db_session: Session,
class TrailCourse(BaseModel):
course_id: str
elements_type: Optional[Literal["course"]] = "course"
status: Optional[Literal["ongoing", "done", "closed"]] = "ongoing"
course_object: dict
masked: Optional[bool] = False
activities_marked_complete: Optional[List[str]]
activities_data: Optional[List[ActivityData]]
progress: Optional[int]
class Trail(BaseModel):
status: Optional[Literal["ongoing", "done", "closed"]] = "ongoing"
masked: Optional[bool] = False
courses: Optional[List[TrailCourse]]
class TrailInDB(Trail):
trail_id: str
org_id: str
user_id: str
creationDate: str = datetime.now().isoformat()
updateDate: str = datetime.now().isoformat()
#### Classes ####################################################
async def create_trail(
request: Request, user: PublicUser, org_id: str, trail_object: Trail
) -> Trail: ) -> Trail:
trails = request.app.db["trails"] statement = select(Trail).where(Trail.org_id == trail_object.org_id)
trail = db_session.exec(statement).first()
# get list of courses if trail:
if trail_object.courses: raise HTTPException(
courses = trail_object.courses status_code=status.HTTP_400_BAD_REQUEST,
# get course ids detail="Trail already exists",
course_ids = [course.course_id for course in courses]
# find if the user has already started the course
existing_trail = await trails.find_one(
{"user_id": user.user_id, "courses.course_id": {"$in": course_ids}}
) )
if existing_trail:
# update the status of the element with the matching course_id to "ongoing"
for element in existing_trail["courses"]:
if element["course_id"] in course_ids:
element["status"] = "ongoing"
# update the existing trail in the database
await trails.replace_one(
{"trail_id": existing_trail["trail_id"]}, existing_trail
)
# create trail id trail = Trail.from_orm(trail_object)
trail_id = f"trail_{uuid4()}"
trail.creation_date = str(datetime.now())
trail.update_date = str(datetime.now())
trail.org_id = trail_object.org_id
trail.trail_uuid = str(f"trail_{uuid4()}")
# create trail # create trail
trail = TrailInDB( db_session.add(trail)
**trail_object.dict(), trail_id=trail_id, user_id=user.user_id, org_id=org_id db_session.commit()
) db_session.refresh(trail)
await trails.insert_one(trail.dict())
return trail return trail
async def get_user_trail(request: Request, org_slug: str, user: PublicUser) -> Trail: async def get_user_trails(
trails = request.app.db["trails"] request: Request,
trail = await trails.find_one({"user_id": user.user_id}) user: PublicUser,
db_session: Session,
) -> TrailRead:
statement = select(Trail).where(Trail.user_id == user.id)
trail = db_session.exec(statement).first()
if not trail: if not trail:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found" status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found"
) )
for element in trail["courses"]:
course_id = element["course_id"]
chapters_meta = await get_coursechapters_meta(request, course_id, user)
activities = chapters_meta["activities"]
num_activities = len(activities)
num_completed_activities = len(element.get("activities_marked_complete", [])) statement = select(TrailRun).where(TrailRun.trail_id == trail.id)
element["progress"] = ( trail_runs = db_session.exec(statement).all()
round((num_completed_activities / num_activities) * 100, 2)
if num_activities > 0 trail_runs = [
else 0 TrailRunRead(**trail_run.__dict__, course={}, steps=[], course_total_steps=0)
for trail_run in trail_runs
]
# Add course object and total activities in a course to trail runs
for trail_run in trail_runs:
statement = select(Course).where(Course.id == trail_run.course_id)
course = db_session.exec(statement).first()
trail_run.course = course
# Add number of activities (steps) in a course
statement = select(ChapterActivity).where(
ChapterActivity.course_id == trail_run.course_id
) )
course_total_steps = db_session.exec(statement)
# count number of activities in a this list
trail_run.course_total_steps = len(course_total_steps.all())
return Trail(**trail) for trail_run in trail_runs:
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id)
trail_steps = db_session.exec(statement).all()
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
trail_run.steps = trail_steps
async def get_user_trail_with_orgslug( for trail_step in trail_steps:
request: Request, user: PublicUser, org_slug: str statement = select(Course).where(Course.id == trail_step.course_id)
) -> Trail: course = db_session.exec(statement).first()
trails = request.app.db["trails"] trail_step.data = dict(course=course)
orgs = request.app.db["organizations"]
courses_mongo = request.app.db["courses"]
# get org_id from orgslug trail_read = TrailRead(
org = await orgs.find_one({"slug": org_slug}) **trail.dict(),
runs=trail_runs,
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
if not trail:
return Trail(masked=False, courses=[])
course_ids = [course["course_id"] for course in trail["courses"]]
live_courses = await courses_mongo.find({"course_id": {"$in": course_ids}}).to_list(
length=None
) )
for course in trail["courses"]: return trail_read
course_id = course["course_id"]
if course_id not in [course["course_id"] for course in live_courses]:
course["masked"] = True
continue
chapters_meta = await get_coursechapters_meta(request, course_id, user) async def check_trail_presence(
activities = chapters_meta["activities"] org_id: int,
user_id: int,
request: Request,
user: PublicUser,
db_session: Session,
):
statement = select(Trail).where(Trail.org_id == org_id, Trail.user_id == user.id)
trail = db_session.exec(statement).first()
# get course object without _id if not trail:
course_object = await courses_mongo.find_one( trail = await create_user_trail(
{"course_id": course_id}, {"_id": 0} request,
user,
TrailCreate(
org_id=org_id,
user_id=user.id,
),
db_session,
) )
return trail
course["course_object"] = course_object return trail
num_activities = len(activities)
num_completed_activities = len(course.get("activities_marked_complete", []))
course["progress"] = ( async def get_user_trail_with_orgid(
round((num_completed_activities / num_activities) * 100, 2) request: Request, user: PublicUser, org_id: int, db_session: Session
if num_activities > 0 ) -> TrailRead:
else 0
trail = await check_trail_presence(
org_id=org_id,
user_id=user.id,
request=request,
user=user,
db_session=db_session,
)
statement = select(TrailRun).where(TrailRun.trail_id == trail.id)
trail_runs = db_session.exec(statement).all()
trail_runs = [
TrailRunRead(**trail_run.__dict__, course={}, steps=[], course_total_steps=0)
for trail_run in trail_runs
]
# Add course object and total activities in a course to trail runs
for trail_run in trail_runs:
statement = select(Course).where(Course.id == trail_run.course_id)
course = db_session.exec(statement).first()
trail_run.course = course
# Add number of activities (steps) in a course
statement = select(ChapterActivity).where(
ChapterActivity.course_id == trail_run.course_id
) )
course_total_steps = db_session.exec(statement)
# count number of activities in a this list
trail_run.course_total_steps = len(course_total_steps.all())
return Trail(**trail) for trail_run in trail_runs:
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id)
trail_steps = db_session.exec(statement).all()
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
trail_run.steps = trail_steps
for trail_step in trail_steps:
statement = select(Course).where(Course.id == trail_step.course_id)
course = db_session.exec(statement).first()
trail_step.data = dict(course=course)
trail_read = TrailRead(
**trail.dict(),
runs=trail_runs,
)
return trail_read
async def add_activity_to_trail( async def add_activity_to_trail(
request: Request, user: PublicUser, course_id: str, org_slug: str, activity_id: str request: Request,
) -> Trail: user: PublicUser,
trails = request.app.db["trails"] activity_uuid: str,
orgs = request.app.db["organizations"] db_session: Session,
courseid = "course_" + course_id ) -> TrailRead:
activityid = "activity_" + activity_id # Look for the activity
statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
activity = db_session.exec(statement).first()
# get org_id from orgslug if not activity:
org = await orgs.find_one({"slug": org_slug})
org_id = org["org_id"]
# find a trail with the user_id and course_id in the courses array
trail = await trails.find_one(
{"user_id": user.user_id, "courses.course_id": courseid, "org_id": org_id}
)
if user.user_id == "anonymous":
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found"
detail="Anonymous users cannot add activity to trail",
) )
if not trail: statement = select(Course).where(Course.id == activity.course_id)
return Trail(masked=False, courses=[]) course = db_session.exec(statement).first()
# if a trail has course_id in the courses array, then add the activity_id to the activities_marked_complete array if not course:
for element in trail["courses"]: raise HTTPException(
if element["course_id"] == courseid: status_code=status.HTTP_404_NOT_FOUND, detail="Course not found"
if "activities_marked_complete" in element: )
# check if activity_id is already in the array
if activityid not in element["activities_marked_complete"]:
element["activities_marked_complete"].append(activityid)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Activity already marked complete",
)
else:
element["activities_marked_complete"] = [activity_id]
# modify trail object trail = await check_trail_presence(
await trails.replace_one({"trail_id": trail["trail_id"]}, trail) org_id=course.org_id,
user_id=user.id,
request=request,
user=user,
db_session=db_session,
)
return Trail(**trail) statement = select(TrailRun).where(
TrailRun.trail_id == trail.id, TrailRun.course_id == course.id
)
trailrun = db_session.exec(statement).first()
if not trailrun:
trailrun = TrailRun(
trail_id=trail.id if trail.id is not None else 0,
course_id=course.id if course.id is not None else 0,
org_id=course.org_id,
user_id=user.id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
db_session.add(trailrun)
db_session.commit()
db_session.refresh(trailrun)
statement = select(TrailStep).where(
TrailStep.trailrun_id == trailrun.id, TrailStep.activity_id == activity.id
)
trailstep = db_session.exec(statement).first()
if not trailstep:
trailstep = TrailStep(
trailrun_id=trailrun.id if trailrun.id is not None else 0,
activity_id=activity.id if activity.id is not None else 0,
course_id=course.id if course.id is not None else 0,
trail_id=trail.id if trail.id is not None else 0,
org_id=course.org_id,
complete=False,
teacher_verified=False,
grade="",
user_id=user.id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
db_session.add(trailstep)
db_session.commit()
db_session.refresh(trailstep)
statement = select(TrailRun).where(TrailRun.trail_id == trail.id)
trail_runs = db_session.exec(statement).all()
trail_runs = [
TrailRunRead(**trail_run.__dict__, course={}, steps=[], course_total_steps=0)
for trail_run in trail_runs
]
for trail_run in trail_runs:
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id)
trail_steps = db_session.exec(statement).all()
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
trail_run.steps = trail_steps
for trail_step in trail_steps:
statement = select(Course).where(Course.id == trail_step.course_id)
course = db_session.exec(statement).first()
trail_step.data = dict(course=course)
trail_read = TrailRead(
**trail.dict(),
runs=trail_runs,
)
return trail_read
async def add_course_to_trail( async def add_course_to_trail(
request: Request, user: PublicUser, orgslug: str, course_id: str request: Request,
) -> Trail: user: PublicUser,
trails = request.app.db["trails"] course_uuid: str,
orgs = request.app.db["organizations"] db_session: Session,
) -> TrailRead:
statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
if user.user_id == "anonymous": if not course:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_404_NOT_FOUND, detail="Course not found"
detail="Anonymous users cannot add activity to trail",
) )
org = await orgs.find_one({"slug": orgslug}) # check if run already exists
statement = select(TrailRun).where(TrailRun.course_id == course.id)
trailrun = db_session.exec(statement).first()
org = PublicOrganization(**org) if trailrun:
raise HTTPException(
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]}) status_code=status.HTTP_400_BAD_REQUEST, detail="TrailRun already exists"
if not trail:
trail_to_insert = TrailInDB(
trail_id=f"trail_{uuid4()}",
user_id=user.user_id,
org_id=org["org_id"],
courses=[],
) )
trail_to_insert = await trails.insert_one(trail_to_insert.dict())
trail = await trails.find_one({"_id": trail_to_insert.inserted_id}) statement = select(Trail).where(
Trail.org_id == course.org_id, Trail.user_id == user.id
# check if course is already present in the trail
for element in trail["courses"]:
if element["course_id"] == course_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Course already present in the trail",
)
updated_trail = TrailCourse(
course_id=course_id,
activities_data=[],
activities_marked_complete=[],
progress=0,
course_object={},
status="ongoing",
masked=False,
) )
trail["courses"].append(updated_trail.dict()) trail = db_session.exec(statement).first()
await trails.replace_one({"trail_id": trail["trail_id"]}, trail)
return Trail(**trail)
async def remove_course_from_trail(
request: Request, user: PublicUser, orgslug: str, course_id: str
) -> Trail:
trails = request.app.db["trails"]
orgs = request.app.db["organizations"]
if user.user_id == "anonymous":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Anonymous users cannot add activity to trail",
)
org = await orgs.find_one({"slug": orgslug})
org = PublicOrganization(**org)
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
if not trail: if not trail:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found" status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found"
) )
# check if course is already present in the trail statement = select(TrailRun).where(
TrailRun.trail_id == trail.id, TrailRun.course_id == course.id
)
trail_run = db_session.exec(statement).first()
for element in trail["courses"]: if not trail_run:
if element["course_id"] == course_id: trail_run = TrailRun(
trail["courses"].remove(element) trail_id=trail.id if trail.id is not None else 0,
break course_id=course.id if course.id is not None else 0,
org_id=course.org_id,
user_id=user.id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
db_session.add(trail_run)
db_session.commit()
db_session.refresh(trail_run)
await trails.replace_one({"trail_id": trail["trail_id"]}, trail) statement = select(TrailRun).where(TrailRun.trail_id == trail.id)
return Trail(**trail) trail_runs = db_session.exec(statement).all()
trail_runs = [
TrailRunRead(**trail_run.__dict__, course={}, steps=[], course_total_steps=0)
for trail_run in trail_runs
]
for trail_run in trail_runs:
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id)
trail_steps = db_session.exec(statement).all()
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
trail_run.steps = trail_steps
for trail_step in trail_steps:
statement = select(Course).where(Course.id == trail_step.course_id)
course = db_session.exec(statement).first()
trail_step.data = dict(course=course)
trail_read = TrailRead(
**trail.dict(),
runs=trail_runs,
)
return trail_read
async def remove_course_from_trail(
request: Request,
user: PublicUser,
course_uuid: str,
db_session: Session,
) -> TrailRead:
statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
if not course:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Course not found"
)
statement = select(Trail).where(
Trail.org_id == course.org_id, Trail.user_id == user.id
)
trail = db_session.exec(statement).first()
if not trail:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found"
)
statement = select(TrailRun).where(
TrailRun.trail_id == trail.id, TrailRun.course_id == course.id
)
trail_run = db_session.exec(statement).first()
if trail_run:
db_session.delete(trail_run)
db_session.commit()
# Delete all trail steps for this course
statement = select(TrailStep).where(TrailStep.course_id == course.id)
trail_steps = db_session.exec(statement).all()
for trail_step in trail_steps:
db_session.delete(trail_step)
db_session.commit()
statement = select(TrailRun).where(TrailRun.trail_id == trail.id)
trail_runs = db_session.exec(statement).all()
trail_runs = [
TrailRunRead(**trail_run.__dict__, course={}, steps=[], course_total_steps=0)
for trail_run in trail_runs
]
for trail_run in trail_runs:
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id)
trail_steps = db_session.exec(statement).all()
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
trail_run.steps = trail_steps
for trail_step in trail_steps:
statement = select(Course).where(Course.id == trail_step.course_id)
course = db_session.exec(statement).first()
trail_step.data = dict(course=course)
trail_read = TrailRead(
**trail.dict(),
runs=trail_runs,
)
return trail_read

View file

@ -1,70 +0,0 @@
from typing import Literal
from pydantic import BaseModel
class UserOrganization(BaseModel):
org_id: str
org_role: Literal['owner', 'editor', 'member']
def __getitem__(self, item):
return getattr(self, item)
class UserRolesInOrganization(BaseModel):
org_id: str
role_id: str
def __getitem__(self, item):
return getattr(self, item)
class User(BaseModel):
username: str
email: str
full_name: str | None = None
avatar_url: str | None = None
bio: str | None = None
class UserWithPassword(User):
password: str
class UserInDB(User):
user_id: str
password: str
verified: bool | None = False
disabled: bool | None = False
orgs: list[UserOrganization] = []
roles: list[UserRolesInOrganization] = []
creation_date: str
update_date: str
def __getitem__(self, item):
return getattr(self, item)
class PublicUser(User):
user_id: str
orgs: list[UserOrganization] = []
roles: list[UserRolesInOrganization] = []
creation_date: str
update_date: str
class AnonymousUser(BaseModel):
user_id: str = "anonymous"
username: str = "anonymous"
roles: list[UserRolesInOrganization] = [
UserRolesInOrganization(org_id="anonymous", role_id="role_anonymous")
]
# Forms ####################################################
class PasswordChangeForm(BaseModel):
old_password: str
new_password: str

View file

@ -2,214 +2,347 @@ from datetime import datetime
from typing import Literal 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 src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles, authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
) )
from src.security.security import security_hash_password, security_verify_password from src.db.organizations import Organization
from src.services.users.schemas.users import ( from src.db.users import (
PasswordChangeForm, AnonymousUser,
PublicUser, PublicUser,
User, User,
UserOrganization, UserCreate,
UserRolesInOrganization, UserRead,
UserWithPassword, UserUpdate,
UserInDB, UserUpdatePassword,
) )
from src.db.user_organizations import UserOrganization
from src.security.security import security_hash_password, security_verify_password
async def create_user( async def create_user(
request: Request, request: Request,
current_user: PublicUser | None, db_session: Session,
user_object: UserWithPassword, current_user: PublicUser | AnonymousUser,
org_slug: str, user_object: UserCreate,
org_id: int,
): ):
users = request.app.db["users"] user = User.from_orm(user_object)
isUsernameAvailable = await users.find_one({"username": user_object.username}) # RBAC check
isEmailAvailable = await users.find_one({"email": user_object.email}) await rbac_check(request, current_user, "create", "user_x", db_session)
if isUsernameAvailable: # Complete the user object
user.user_uuid = f"user_{uuid4()}"
user.password = await security_hash_password(user_object.password)
user.email_verified = False
user.creation_date = str(datetime.now())
user.update_date = str(datetime.now())
# Verifications
# Check if Organization exists
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
if not result.first():
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Username already exists" status_code=400,
detail="Organization does not exist",
) )
if isEmailAvailable: # Username
statement = select(User).where(User.username == user.username)
result = db_session.exec(statement)
if result.first():
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Email already exists" status_code=400,
detail="Username already exists",
) )
# Generate user_id with uuid4 # Email
user_id = str(f"user_{uuid4()}") statement = select(User).where(User.email == user.email)
result = db_session.exec(statement)
# Check if the requesting user is authenticated if result.first():
if current_user is not None:
# Verify rights
await verify_user_rights_on_user(request, current_user, "create", user_id)
# Set the username & hash the password
user_object.username = user_object.username.lower()
user_object.password = await security_hash_password(user_object.password)
# Get org_id from org_slug
orgs = request.app.db["organizations"]
# Check if the org exists
isOrgExists = await orgs.find_one({"slug": org_slug})
# If the org does not exist, raise an error
if not isOrgExists and (org_slug != "None"):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, status_code=400,
detail="You are trying to create a user in an organization that does not exist", detail="Email already exists",
) )
org_id = isOrgExists["org_id"] if org_slug != "None" else '' # Exclude unset values
user_data = user.dict(exclude_unset=True)
for key, value in user_data.items():
setattr(user, key, value)
# Create initial orgs list with the org_id passed in # Add user to database
orgs = ( db_session.add(user)
[UserOrganization(org_id=org_id, org_role="member")] db_session.commit()
if org_slug != "None" db_session.refresh(user)
else []
)
# Give role # Link user and organization
roles = ( user_organization = UserOrganization(
[UserRolesInOrganization(role_id="role_member", org_id=org_id)] user_id=user.id if user.id else 0,
if org_slug != "None" org_id=int(org_id),
else [] role_id=3,
)
# Create the user
user = UserInDB(
user_id=user_id,
creation_date=str(datetime.now()), creation_date=str(datetime.now()),
update_date=str(datetime.now()), update_date=str(datetime.now()),
orgs=orgs,
roles=roles,
**user_object.dict(),
) )
# Insert the user into the database db_session.add(user_organization)
await users.insert_one(user.dict()) db_session.commit()
db_session.refresh(user_organization)
return User(**user.dict()) user = UserRead.from_orm(user)
return user
async def read_user(request: Request, current_user: PublicUser, user_id: str): async def create_user_without_org(
users = request.app.db["users"] request: Request,
db_session: Session,
current_user: PublicUser | AnonymousUser,
user_object: UserCreate,
):
user = User.from_orm(user_object)
# Check if the user exists # RBAC check
isUserExists = await users.find_one({"user_id": user_id}) await rbac_check(request, current_user, "create", "user_x", db_session)
# Verify rights # Complete the user object
await verify_user_rights_on_user(request, current_user, "read", user_id) user.user_uuid = f"user_{uuid4()}"
user.password = await security_hash_password(user_object.password)
user.email_verified = False
user.creation_date = str(datetime.now())
user.update_date = str(datetime.now())
# If the user does not exist, raise an error # Verifications
if not isUserExists:
# Username
statement = select(User).where(User.username == user.username)
result = db_session.exec(statement)
if result.first():
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist" status_code=400,
detail="Username already exists",
) )
return User(**isUserExists) # Email
statement = select(User).where(User.email == user.email)
result = db_session.exec(statement)
if result.first():
raise HTTPException(
status_code=400,
detail="Email already exists",
)
# Exclude unset values
user_data = user.dict(exclude_unset=True)
for key, value in user_data.items():
setattr(user, key, value)
# Add user to database
db_session.add(user)
db_session.commit()
db_session.refresh(user)
user = UserRead.from_orm(user)
return user
async def update_user( async def update_user(
request: Request, user_id: str, user_object: User, current_user: PublicUser request: Request,
db_session: Session,
user_id: int,
current_user: PublicUser | AnonymousUser,
user_object: UserUpdate,
): ):
users = request.app.db["users"] # Get user
statement = select(User).where(User.id == user_id)
user = db_session.exec(statement).first()
# Verify rights if not user:
await verify_user_rights_on_user(request, current_user, "update", user_id)
isUserExists = await users.find_one({"user_id": user_id})
isUsernameAvailable = await users.find_one({"username": user_object.username})
isEmailAvailable = await users.find_one({"email": user_object.email})
if not isUserExists:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist" status_code=400,
detail="User does not exist",
) )
# okay if username is not changed # RBAC check
if isUserExists["username"] == user_object.username: await rbac_check(request, current_user, "update", user.user_uuid, db_session)
user_object.username = user_object.username.lower()
else: # Update user
if isUsernameAvailable: user_data = user_object.dict(exclude_unset=True)
raise HTTPException( for key, value in user_data.items():
status_code=status.HTTP_409_CONFLICT, detail="Username already used" setattr(user, key, value)
)
if isEmailAvailable: user.update_date = str(datetime.now())
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Email already used"
)
updated_user = {"$set": user_object.dict()} # Update user in database
users.update_one({"user_id": user_id}, updated_user) db_session.add(user)
db_session.commit()
db_session.refresh(user)
return User(**user_object.dict()) user = UserRead.from_orm(user)
return user
async def update_user_password( async def update_user_password(
request: Request, request: Request,
current_user: PublicUser, db_session: Session,
user_id: str, current_user: PublicUser | AnonymousUser,
password_change_form: PasswordChangeForm, user_id: int,
form: UserUpdatePassword,
): ):
users = request.app.db["users"] # Get user
statement = select(User).where(User.id == user_id)
user = db_session.exec(statement).first()
isUserExists = await users.find_one({"user_id": user_id}) if not user:
# Verify rights
await verify_user_rights_on_user(request, current_user, "update", user_id)
if not isUserExists:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist" status_code=400,
detail="User does not exist",
) )
if not await security_verify_password( # RBAC check
password_change_form.old_password, isUserExists["password"] await rbac_check(request, current_user, "update", user.user_uuid, db_session)
):
if not await security_verify_password(form.old_password, user.password):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Wrong password" status_code=status.HTTP_401_UNAUTHORIZED, detail="Wrong password"
) )
new_password = await security_hash_password(password_change_form.new_password) # Update user
user.password = await security_hash_password(form.new_password)
user.update_date = str(datetime.now())
updated_user = {"$set": {"password": new_password}} # Update user in database
await users.update_one({"user_id": user_id}, updated_user) db_session.add(user)
db_session.commit()
db_session.refresh(user)
return {"detail": "Password updated"} user = UserRead.from_orm(user)
return user
async def delete_user(request: Request, current_user: PublicUser, user_id: str): async def read_user_by_id(
users = request.app.db["users"] request: Request,
db_session: Session,
current_user: PublicUser | AnonymousUser,
user_id: int,
):
# Get user
statement = select(User).where(User.id == user_id)
user = db_session.exec(statement).first()
isUserExists = await users.find_one({"user_id": user_id}) if not user:
# Verify is user has permission to delete the user
await verify_user_rights_on_user(request, current_user, "delete", user_id)
if not isUserExists:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist" status_code=400,
detail="User does not exist",
) )
await users.delete_one({"user_id": user_id}) # RBAC check
await rbac_check(request, current_user, "read", user.user_uuid, db_session)
return {"detail": "User deleted"} user = UserRead.from_orm(user)
return user
async def read_user_by_uuid(
request: Request,
db_session: Session,
current_user: PublicUser | AnonymousUser,
user_uuid: str,
):
# Get user
statement = select(User).where(User.user_uuid == user_uuid)
user = db_session.exec(statement).first()
if not user:
raise HTTPException(
status_code=400,
detail="User does not exist",
)
# RBAC check
await rbac_check(request, current_user, "read", user.user_uuid, db_session)
user = UserRead.from_orm(user)
return user
async def authorize_user_action(
request: Request,
db_session: Session,
current_user: PublicUser | AnonymousUser,
ressource_uuid: str,
action: Literal["create", "read", "update", "delete"],
):
# 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",
)
# RBAC check
authorized = await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, ressource_uuid, db_session
)
if authorized:
return True
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You are not authorized to perform this action",
)
async def delete_user_by_id(
request: Request,
db_session: Session,
current_user: PublicUser | AnonymousUser,
user_id: int,
):
# Get user
statement = select(User).where(User.id == user_id)
user = db_session.exec(statement).first()
if not user:
raise HTTPException(
status_code=400,
detail="User does not exist",
)
# RBAC check
await rbac_check(request, current_user, "delete", user.user_uuid, db_session)
# Delete user
db_session.delete(user)
db_session.commit()
return "User deleted"
# Utils & Security functions # Utils & Security functions
async def security_get_user(request: Request, email: str): async def security_get_user(request: Request, db_session: Session, email: str) -> User:
users = request.app.db["users"] # Check if user exists
statement = select(User).where(User.email == email)
user = await users.find_one({"email": email}) user = db_session.exec(statement).first()
if not user: if not user:
raise HTTPException( raise HTTPException(
@ -217,105 +350,39 @@ async def security_get_user(request: Request, email: str):
detail="User with Email does not exist", detail="User with Email does not exist",
) )
return UserInDB(**user) user = User(**user.dict())
async def get_userid_by_username(request: Request, username: str):
users = request.app.db["users"]
user = await users.find_one({"username": username})
if not user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
)
return user["user_id"]
async def get_user_by_userid(request: Request, user_id: str):
users = request.app.db["users"]
user = await users.find_one({"user_id": user_id})
if not user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
)
user = User(**user)
return user return user
async def get_profile_metadata(request: Request, user): ## 🔒 RBAC Utils ##
users = request.app.db["users"]
request.app.db["roles"]
user = await users.find_one({"user_id": user["user_id"]})
if not user: async def rbac_check(
raise HTTPException( request: Request,
status_code=status.HTTP_409_CONFLICT, detail="User does not exist" current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
user_uuid: str,
db_session: Session,
):
if action == "create":
if current_user.id == 0: # if user is anonymous
return True
else:
await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, "create", "user_x", db_session
)
else:
await authorization_verify_if_user_is_anon(current_user.id)
# if user is the same as the one being read
if current_user.user_uuid == user_uuid:
return True
await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, user_uuid, db_session
) )
return {"user_object": PublicUser(**user), "roles": "random"}
## 🔒 RBAC Utils ##
# Verification of the user's permissions on the roles
async def verify_user_rights_on_user(
request: Request,
current_user: PublicUser,
action: Literal["create", "read", "update", "delete"],
user_id: str,
):
users = request.app.db["users"]
user = UserInDB(**await users.find_one({"user_id": user_id}))
if action == "create":
return True
if action == "read":
await authorization_verify_if_user_is_anon(current_user.user_id)
if current_user.user_id == user_id:
return True
for org in current_user.orgs:
if org.org_id in [org.org_id for org in user.orgs]:
return True
return False
if action == "update":
await authorization_verify_if_user_is_anon(current_user.user_id)
if current_user.user_id == user_id:
return True
for org in current_user.orgs:
if org.org_id in [org.org_id for org in user.orgs]:
if org.org_role == "owner":
return True
await authorization_verify_based_on_roles(
request, current_user.user_id, "update", user["roles"], user_id
)
return False
if action == "delete":
await authorization_verify_if_user_is_anon(current_user.user_id)
if current_user.user_id == user_id:
return True
for org in current_user.orgs:
if org.org_id in [org.org_id for org in user.orgs]:
if org.org_role == "owner":
return True
await authorization_verify_based_on_roles(
request, current_user.user_id, "update", user["roles"], user_id
)

View file

@ -6,7 +6,7 @@ from config.config import get_learnhouse_config
async def upload_content( async def upload_content(
directory: str, org_id: str, file_binary: bytes, file_and_format: str directory: str, org_uuid: str, file_binary: bytes, file_and_format: str
): ):
# Get Learnhouse Config # Get Learnhouse Config
learnhouse_config = get_learnhouse_config() learnhouse_config = get_learnhouse_config()
@ -16,12 +16,12 @@ async def upload_content(
if content_delivery == "filesystem": if content_delivery == "filesystem":
# create folder for activity # create folder for activity
if not os.path.exists(f"content/{org_id}/{directory}"): if not os.path.exists(f"content/{org_uuid}/{directory}"):
# create folder for activity # create folder for activity
os.makedirs(f"content/{org_id}/{directory}") os.makedirs(f"content/{org_uuid}/{directory}")
# upload file to server # upload file to server
with open( with open(
f"content/{org_id}/{directory}/{file_and_format}", f"content/{org_uuid}/{directory}/{file_and_format}",
"wb", "wb",
) as f: ) as f:
f.write(file_binary) f.write(file_binary)
@ -37,13 +37,13 @@ async def upload_content(
) )
# Create folder for activity # Create folder for activity
if not os.path.exists(f"content/{org_id}/{directory}"): if not os.path.exists(f"content/{org_uuid}/{directory}"):
# create folder for activity # create folder for activity
os.makedirs(f"content/{org_id}/{directory}") os.makedirs(f"content/{org_uuid}/{directory}")
# Upload file to server # Upload file to server
with open( with open(
f"content/{org_id}/{directory}/{file_and_format}", f"content/{org_uuid}/{directory}/{file_and_format}",
"wb", "wb",
) as f: ) as f:
f.write(file_binary) f.write(file_binary)
@ -52,9 +52,9 @@ async def upload_content(
print("Uploading to s3 using boto3...") print("Uploading to s3 using boto3...")
try: try:
s3.upload_file( s3.upload_file(
f"content/{org_id}/{directory}/{file_and_format}", f"content/{org_uuid}/{directory}/{file_and_format}",
"learnhouse-media", "learnhouse-media",
f"content/{org_id}/{directory}/{file_and_format}", f"content/{org_uuid}/{directory}/{file_and_format}",
) )
except ClientError as e: except ClientError as e:
print(e) print(e)
@ -63,7 +63,7 @@ async def upload_content(
try: try:
s3.head_object( s3.head_object(
Bucket="learnhouse-media", Bucket="learnhouse-media",
Key=f"content/{org_id}/{directory}/{file_and_format}", Key=f"content/{org_uuid}/{directory}/{file_and_format}",
) )
print("File upload successful!") print("File upload successful!")
except Exception as e: except Exception as e:

View file

@ -1,11 +1,12 @@
import { default as React, } from "react"; import { default as React, } from "react";
import AuthProvider from "@components/Security/AuthProvider"; 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";
import { Metadata } from "next"; 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";
type MetadataProps = { type MetadataProps = {
params: { orgslug: string, courseid: string, activityid: string }; params: { orgslug: string, courseid: string, activityid: string };
@ -21,26 +22,25 @@ export async function generateMetadata(
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
return { return {
title: `Edit - ${course_meta.course.name} Activity`, title: `Edit - ${course_meta.name} Activity`,
description: course_meta.course.mini_description, description: course_meta.mini_description,
}; };
} }
const EditActivity = async (params: any) => { const EditActivity = async (params: any) => {
const cookieStore = cookies(); const cookieStore = cookies();
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const activityid = params.params.activityid; const activityuuid = params.params.activityuuid;
const courseid = params.params.courseid; const courseid = params.params.courseid;
const orgslug = params.params.orgslug;
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(activityid, { 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'] });
console.log('courseInfo', courseInfo )
return ( return (
<div> <div>
<AuthProvider> <AuthProvider>
<EditorWrapper orgslug={orgslug} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper> <EditorWrapper org={org} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>
</AuthProvider> </AuthProvider>
</div> </div>
); );

View file

@ -5,7 +5,7 @@ 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/AuthProvider"; 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)

View file

@ -48,6 +48,7 @@ export async function generateMetadata(
const CollectionPage = async (params: any) => { const CollectionPage = async (params: any) => {
const cookieStore = cookies(); const cookieStore = cookies();
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const org = await getOrganizationContextInfo(params.params.orgslug, { revalidate: 1800, tags: ['organizations'] });
const orgslug = params.params.orgslug; const orgslug = params.params.orgslug;
const col = await getCollectionByIdWithAuthHeader(params.params.collectionid, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] }); const col = await getCollectionByIdWithAuthHeader(params.params.collectionid, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] });
@ -62,9 +63,9 @@ const CollectionPage = async (params: any) => {
<br /> <br />
<div className="home_courses flex flex-wrap"> <div className="home_courses flex flex-wrap">
{col.courses.map((course: any) => ( {col.courses.map((course: any) => (
<div className="pr-8" key={course.course_id}> <div className="pr-8" key={course.course_uuid}>
<Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_id))}> <Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_uuid))}>
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(course.org_id, course.course_id, course.thumbnail)})` }}> <div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org.org_uuid, course.course_uuid, course.thumbnail_image)})` }}>
</div> </div>
</Link> </Link>
<h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2> <h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2>

View file

@ -41,7 +41,7 @@ function NewCollection(params: any) {
description: description, description: description,
courses: selectedCourses, courses: selectedCourses,
public: true, public: true,
org_id: org.org_id, org_id: org.id,
}; };
await createCollection(collection); await createCollection(collection);
await revalidateTags(["collections"], orgslug); await revalidateTags(["collections"], orgslug);
@ -69,26 +69,29 @@ function NewCollection(params: any) {
) : ( ) : (
<div> <div>
{courses.map((course: any) => ( {courses.map((course: any) => (
<div key={course.course_id} className="flex items-center mb-2"> <div key={course.course_uuid} className="flex items-center mb-2">
<input
type="checkbox" <input
id={course.course_id}
name={course.course_id} type="checkbox"
value={course.course_id} id={course.id}
checked={selectedCourses.includes(course.course_id)} name={course.name}
onChange={(e) => { value={course.id}
const courseId = e.target.value; // id is an integer, not a string
setSelectedCourses((prevSelectedCourses: string[]) => {
if (e.target.checked) { onChange={(e) => {
return [...prevSelectedCourses, courseId]; if (e.target.checked) {
} else { setSelectedCourses([...selectedCourses, course.id]);
return prevSelectedCourses.filter((selectedCourse) => selectedCourse !== courseId); }
} else {
}); setSelectedCourses(selectedCourses.filter((course_uuid: any) => course_uuid !== course.course_uuid));
}} }
className="mr-2 focus:outline-none focus:ring-2 focus:ring-blue-500" }
/> }
<label htmlFor={course.course_id} className="text-sm">{course.name}</label> className="mr-2"
/>
<label htmlFor={course.course_uuid} className="text-sm">{course.name}</label>
</div> </div>
))} ))}

View file

@ -8,7 +8,7 @@ import { Metadata } from "next";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import Link from "next/link"; import Link from "next/link";
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth"; import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
import CollectionThumbnail from "@components/Objects/Other/CollectionThumbnail"; import CollectionThumbnail from "@components/Objects/Thumbnails/CollectionThumbnail";
import NewCollectionButton from "@components/StyledElements/Buttons/NewCollectionButton"; import NewCollectionButton from "@components/StyledElements/Buttons/NewCollectionButton";
type MetadataProps = { type MetadataProps = {
@ -49,14 +49,17 @@ const CollectionsPage = async (params: any) => {
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const orgslug = params.params.orgslug; const orgslug = params.params.orgslug;
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
const org_id = org.org_id; const org_id = org.id;
const collections = await getOrgCollectionsWithAuthHeader(org_id, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] }); const collections = await getOrgCollectionsWithAuthHeader(org_id, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] });
return ( return (
<GeneralWrapperStyled> <GeneralWrapperStyled>
<div className="flex justify-between" > <div className="flex justify-between" >
<TypeOfContentTitle title="Collections" type="col" /> <TypeOfContentTitle title="Collections" type="col" />
<AuthenticatedClientElement checkMethod='roles' orgId={org_id}> <AuthenticatedClientElement
ressourceType="collection"
action="create"
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")}>
<NewCollectionButton /> <NewCollectionButton />
</Link> </Link>
@ -64,7 +67,7 @@ const CollectionsPage = async (params: any) => {
</div> </div>
<div className="home_collections flex flex-wrap"> <div className="home_collections flex flex-wrap">
{collections.map((collection: any) => ( {collections.map((collection: any) => (
<div className="flex flex-col py-1 px-3" key={collection.collection_id}> <div className="flex flex-col py-1 px-3" key={collection.collection_uuid}>
<CollectionThumbnail collection={collection} orgslug={orgslug} org_id={org_id} /> <CollectionThumbnail collection={collection} orgslug={orgslug} org_id={org_id} />
</div> </div>
))} ))}
@ -81,7 +84,10 @@ const CollectionsPage = async (params: any) => {
<h1 className="text-3xl font-bold text-gray-600">No collections yet</h1> <h1 className="text-3xl font-bold text-gray-600">No collections yet</h1>
<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' orgId={org_id}> <AuthenticatedClientElement checkMethod='roles'
ressourceType="collection"
action="create"
orgId={org_id}>
<Link href={getUriWithOrg(orgslug, "/collections/new")}> <Link href={getUriWithOrg(orgslug, "/collections/new")}>
<NewCollectionButton /> <NewCollectionButton />
</Link> </Link>

View file

@ -1,129 +0,0 @@
"use client";
import Link from "next/link";
import { getUriWithOrg } from "@services/config/config";
import Canva from "@components/Objects/Activities/DynamicCanva/DynamicCanva";
import VideoActivity from "@components/Objects/Activities/Video/Video";
import { Check } from "lucide-react";
import { markActivityAsComplete } from "@services/courses/activity";
import DocumentPdfActivity from "@components/Objects/Activities/DocumentPdf/DocumentPdf";
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { useRouter } from "next/navigation";
import AuthenticatedClientElement from "@components/Security/AuthenticatedClientElement";
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
interface ActivityClientProps {
activityid: string;
courseid: string;
orgslug: string;
activity: any;
course: any;
}
function ActivityClient(props: ActivityClientProps) {
const activityid = props.activityid;
const courseid = props.courseid;
const orgslug = props.orgslug;
const activity = props.activity;
const course = props.course;
function getChapterName(chapterId: string) {
let chapterName = "";
course.chapters.forEach((chapter: any) => {
if (chapter.id === chapterId) {
chapterName = chapter.name;
}
});
return chapterName;
}
return (
<>
<GeneralWrapperStyled>
<div className="space-y-4 pt-4">
<div className="flex space-x-6">
<div className="flex">
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}`}>
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(course.course.org_id, course.course.course_id, course.course.thumbnail)}`} alt="" />
</Link>
</div>
<div className="flex flex-col -space-y-1">
<p className="font-bold text-gray-700 text-md">Course </p>
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{course.course.name}</h1>
</div>
</div>
<ActivityIndicators course_id={courseid} current_activity={activityid} orgslug={orgslug} course={course} />
<div className="flex justify-between items-center">
<div className="flex flex-col -space-y-1">
<p className="font-bold text-gray-700 text-md">Chapter : {getChapterName(activity.coursechapter_id)}</p>
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{activity.name}</h1>
</div>
<div className="flex space-x-2">
<AuthenticatedClientElement checkMethod="authentication">
<MarkStatus activityid={activityid} course={course} orgslug={orgslug} courseid={courseid} />
</AuthenticatedClientElement>
</div>
</div>
{activity ? (
<div className={`p-7 pt-4 drop-shadow-sm rounded-lg ${activity.type == 'dynamic' ? 'bg-white' : 'bg-zinc-950'}`}>
<div>
{activity.type == "dynamic" && <Canva content={activity.content} activity={activity} />}
{/* todo : use apis & streams instead of this */}
{activity.type == "video" && <VideoActivity course={course} activity={activity} />}
{activity.type == "documentpdf" && <DocumentPdfActivity course={course} activity={activity} />}
</div>
</div>
) : (<div></div>)}
{<div style={{ height: "100px" }}></div>}
</div>
</GeneralWrapperStyled>
</>
);
}
export function MarkStatus(props: { activityid: string, course: any, orgslug: string, courseid: string }) {
const router = useRouter();
async function markActivityAsCompleteFront() {
const trail = await markActivityAsComplete(props.orgslug, props.courseid, props.activityid);
router.refresh();
// refresh page (FIX for Next.js BUG)
//window.location.reload();
}
return (
<>{props.course.trail.activities_marked_complete &&
props.course.trail.activities_marked_complete.includes("activity_" + props.activityid) &&
props.course.trail.status == "ongoing" ? (
<div className="bg-teal-600 rounded-md drop-shadow-md flex flex-col p-3 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" >
<i>
<Check size={15}></Check>
</i>{" "}
Already completed
</div>
) : (
<div className="bg-zinc-600 rounded-md drop-shadow-md flex flex-col p-3 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" onClick={markActivityAsCompleteFront}>
{" "}
<i>
<Check size={15}></Check>
</i>{" "}
Mark as complete
</div>
)}</>
)
}
export default ActivityClient;

View file

@ -1,152 +0,0 @@
"use client";
import React, { FC, use, useEffect, useReducer } from 'react'
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
import useSWR, { mutate } from 'swr';
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
import Link from 'next/link';
import CourseEdition from '../subpages/CourseEdition';
import CourseContentEdition from '../subpages/CourseContentEdition';
import ErrorUI from '@components/StyledElements/Error/Error';
import { updateChaptersMetadata } from '@services/courses/chapters';
import { Check, SaveAllIcon, Timer } from 'lucide-react';
import Loading from '../../loading';
import { updateCourse } from '@services/courses/courses';
import { useRouter } from 'next/navigation';
function CourseEditClient({ courseid, subpage, params }: { courseid: string, subpage: string, params: any }) {
const { data: chapters_meta, error: chapters_meta_error, isLoading: chapters_meta_isloading } = useSWR(`${getAPIUrl()}chapters/meta/course_${courseid}`, swrFetcher);
const { data: course, error: course_error, isLoading: course_isloading } = useSWR(`${getAPIUrl()}courses/course_${courseid}`, swrFetcher);
const [courseChaptersMetadata, dispatchCourseChaptersMetadata] = useReducer(courseChaptersReducer, {});
const [courseState, dispatchCourseMetadata] = useReducer(courseReducer, {});
const [savedContent, dispatchSavedContent] = useReducer(savedContentReducer, true);
const router = useRouter();
function courseChaptersReducer(state: any, action: any) {
switch (action.type) {
case 'updated_chapter':
// action will contain the entire state, just update the entire state
return action.payload;
default:
throw new Error();
}
}
function courseReducer(state: any, action: any) {
switch (action.type) {
case 'updated_course':
// action will contain the entire state, just update the entire state
return action.payload;
default:
throw new Error();
}
}
function savedContentReducer(state: any, action: any) {
switch (action.type) {
case 'saved_content':
return true;
case 'unsaved_content':
return false;
default:
throw new Error();
}
}
async function saveCourse() {
if (subpage.toString() === 'content') {
await updateChaptersMetadata(courseid, courseChaptersMetadata)
dispatchSavedContent({ type: 'saved_content' })
await mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`)
await revalidateTags(['courses'], params.params.orgslug)
router.refresh()
}
else if (subpage.toString() === 'general') {
await updateCourse(courseid, courseState)
dispatchSavedContent({ type: 'saved_content' })
await mutate(`${getAPIUrl()}courses/course_${courseid}`)
await revalidateTags(['courses'], params.params.orgslug)
router.refresh()
}
}
useEffect(() => {
if (chapters_meta) {
dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: chapters_meta })
dispatchSavedContent({ type: 'saved_content' })
}
if (course) {
dispatchCourseMetadata({ type: 'updated_course', payload: course })
dispatchSavedContent({ type: 'saved_content' })
}
}, [chapters_meta, course])
return (
<>
<div className='bg-white shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'>
<div className='max-w-screen-2xl mx-auto px-16 pt-5 tracking-tight'>
{course_isloading && <div className='text-sm text-gray-500'>Loading...</div>}
{course && <>
<div className='flex items-center'><div className='info flex space-x-5 items-center grow'>
<div className='flex'>
<Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseid}`}>
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(course.org_id, "course_" + courseid, course.thumbnail)}`} alt="" />
</Link>
</div>
<div className="flex flex-col ">
<div className='text-sm text-gray-500'>Edit Course</div>
<div className='text-2xl font-bold first-letter:uppercase'>{course.name}</div>
</div>
</div>
<div className='flex space-x-5 items-center'>
{savedContent ? <></> : <div className='text-gray-600 flex space-x-2 items-center antialiased'>
<Timer size={15} />
<div>
Unsaved changes
</div>
</div>}
<div className={`' px-4 py-2 rounded-lg drop-shadow-md cursor-pointer flex space-x-2 items-center font-bold antialiased transition-all ease-linear ` + (savedContent ? 'bg-gray-600 text-white' : 'bg-black text-white border hover:bg-gray-900 ')
} onClick={saveCourse}>
{savedContent ? <Check size={20} /> : <SaveAllIcon size={20} />}
{savedContent ? <div className=''>Saved</div> : <div className=''>Save</div>}
</div>
</div>
</div>
</>}
<div className='flex space-x-5 pt-3 font-black text-sm'>
<Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseid}/edit/general`}>
<div className={`py-2 w-16 text-center border-black transition-all ease-linear ${subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>General</div>
</Link>
<Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseid}/edit/content`}>
<div className={`py-2 w-16 text-center border-black transition-all ease-linear ${subpage.toString() === 'content' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>Content</div>
</Link>
</div>
</div>
</div>
<CoursePageViewer dispatchSavedContent={dispatchSavedContent} courseState={courseState} courseChaptersMetadata={courseChaptersMetadata} dispatchCourseMetadata={dispatchCourseMetadata} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} subpage={subpage} courseid={courseid} orgslug={params.params.orgslug} />
</>
)
}
const CoursePageViewer = ({ subpage, courseid, orgslug, dispatchCourseMetadata, dispatchCourseChaptersMetadata, courseChaptersMetadata, dispatchSavedContent, courseState }: { subpage: string, courseid: string, orgslug: string, dispatchCourseChaptersMetadata: React.Dispatch<any>, dispatchCourseMetadata: React.Dispatch<any>, dispatchSavedContent: React.Dispatch<any>, courseChaptersMetadata: any, courseState: any }) => {
if (subpage.toString() === 'general' && Object.keys(courseState).length !== 0) {
return <CourseEdition data={courseState} dispatchCourseMetadata={dispatchCourseMetadata} dispatchSavedContent={dispatchSavedContent} />
}
else if (subpage.toString() === 'content' && Object.keys(courseChaptersMetadata).length !== 0) {
return <CourseContentEdition data={courseChaptersMetadata} dispatchSavedContent={dispatchSavedContent} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} courseid={courseid} orgslug={orgslug} />
}
else if (subpage.toString() === 'content' || subpage.toString() === 'general') {
return <Loading />
}
else {
return <ErrorUI />
}
}
export default CourseEditClient

View file

@ -1,41 +0,0 @@
import { getOrganizationContextInfo } from "@services/organizations/orgs";
import CourseEditClient from "./edit";
import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
import { cookies } from "next/headers";
import { Metadata } from 'next';
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
type MetadataProps = {
params: { orgslug: string, courseid: string };
searchParams: { [key: string]: string | string[] | undefined };
};
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
const cookieStore = cookies();
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
return {
title: `Edit Course - ` + course_meta.course.name,
description: course_meta.course.mini_description,
};
}
function CourseEdit(params: any) {
let subpage = params.params.subpage ? params.params.subpage : 'general';
return (
<>
<CourseEditClient params={params} subpage={subpage} courseid={params.params.courseid} />
</>
);
}
export default CourseEdit;

View file

@ -1,320 +0,0 @@
"use client";
import React from "react";
import { useState, useEffect } from "react";
import { DragDropContext, Droppable } from "react-beautiful-dnd";
import Chapter from "@components/Pages/CourseEdit/Draggables/Chapter";
import { createChapter, deleteChapter, getCourseChaptersMetadata, updateChaptersMetadata } from "@services/courses/chapters";
import { useRouter } from "next/navigation";
import NewChapterModal from "@components/Objects/Modals/Chapters/NewChapter";
import NewActivityModal from "@components/Objects/Modals/Activities/Create/NewActivity";
import { createActivity, createFileActivity, createExternalVideoActivity } from "@services/courses/activities";
import { getOrganizationContextInfo, getOrganizationContextInfoWithoutCredentials } from "@services/organizations/orgs";
import Modal from "@components/StyledElements/Modal/Modal";
import { denyAccessToUser } from "@services/utils/react/middlewares/views";
import { Folders, Hexagon, SaveIcon } from "lucide-react";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
import { mutate } from "swr";
import { getAPIUrl } from "@services/config/config";
function CourseContentEdition(props: any) {
const router = useRouter();
// Initial Course State
const data = props.data;
// New Chapter Modal State
const [newChapterModal, setNewChapterModal] = useState(false) as any;
// New Activity Modal State
const [newActivityModal, setNewActivityModal] = useState(false) as any;
const [newActivityModalData, setNewActivityModalData] = useState("") as any;
// Check window availability
const [winReady, setwinReady] = useState(false);
const courseid = props.courseid;
const orgslug = props.orgslug;
useEffect(() => {
setwinReady(true);
}, [courseid, orgslug]);
// get a list of chapters order by chapter order
const getChapters = () => {
const chapterOrder = data.chapterOrder ? data.chapterOrder : [];
return chapterOrder.map((chapterId: any) => {
const chapter = data.chapters[chapterId];
let activities = [];
if (data.activities) {
activities = chapter.activityIds.map((activityId: any) => data.activities[activityId])
? chapter.activityIds.map((activityId: any) => data.activities[activityId])
: [];
}
return {
list: {
chapter: chapter,
activities: activities,
},
};
});
};
// Submit new chapter
const submitChapter = async (chapter: any) => {
await createChapter(chapter, courseid);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
// await getCourseChapters();
await revalidateTags(['courses'], orgslug);
router.refresh();
setNewChapterModal(false);
};
// Submit new activity
const submitActivity = async (activity: any) => {
let org = await getOrganizationContextInfoWithoutCredentials(orgslug, { revalidate: 1800 });
await updateChaptersMetadata(courseid, data);
await createActivity(activity, activity.chapterId, org.org_id);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
// await getCourseChapters();
setNewActivityModal(false);
await revalidateTags(['courses'], orgslug);
router.refresh();
};
// Submit File Upload
const submitFileActivity = async (file: any, type: any, activity: any, chapterId: string) => {
await updateChaptersMetadata(courseid, data);
await createFileActivity(file, type, activity, chapterId);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
// await getCourseChapters();
setNewActivityModal(false);
await revalidateTags(['courses'], orgslug);
router.refresh();
};
// Submit YouTube Video Upload
const submitExternalVideo = async (external_video_data: any, activity: any, chapterId: string) => {
await updateChaptersMetadata(courseid, data);
await createExternalVideoActivity(external_video_data, activity, chapterId);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
// await getCourseChapters();
setNewActivityModal(false);
await revalidateTags(['courses'], orgslug);
router.refresh();
};
const deleteChapterUI = async (chapterId: any) => {
await deleteChapter(chapterId);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
// await getCourseChapters();
await revalidateTags(['courses'], orgslug);
router.refresh();
};
const updateChapters = () => {
updateChaptersMetadata(courseid, data);
revalidateTags(['courses'], orgslug);
router.refresh();
};
/*
Modals
*/
const openNewActivityModal = async (chapterId: any) => {
setNewActivityModal(true);
setNewActivityModalData(chapterId);
};
// Close new chapter modal
const closeNewChapterModal = () => {
setNewChapterModal(false);
};
const closeNewActivityModal = () => {
setNewActivityModal(false);
};
/*
Drag and drop functions
*/
const onDragEnd = async (result: any) => {
const { destination, source, draggableId, type } = result;
// check if the activity is dropped outside the droppable area
if (!destination) {
return;
}
// check if the activity is dropped in the same place
if (destination.droppableId === source.droppableId && destination.index === source.index) {
return;
}
//////////////////////////// CHAPTERS ////////////////////////////
if (type === "chapter") {
const newChapterOrder = Array.from(data.chapterOrder);
newChapterOrder.splice(source.index, 1);
newChapterOrder.splice(destination.index, 0, draggableId);
const newState = {
...data,
chapterOrder: newChapterOrder,
};
props.dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: newState })
props.dispatchSavedContent({ type: 'unsaved_content' })
//setData(newState);
return;
}
//////////////////////// ACTIVITIES IN SAME CHAPTERS ////////////////////////////
// check if the activity is dropped in the same chapter
const start = data.chapters[source.droppableId];
const finish = data.chapters[destination.droppableId];
// check if the activity is dropped in the same chapter
if (start === finish) {
// create new arrays for chapters and activities
const chapter = data.chapters[source.droppableId];
const newActivityIds = Array.from(chapter.activityIds);
// remove the activity from the old position
newActivityIds.splice(source.index, 1);
// add the activity to the new position
newActivityIds.splice(destination.index, 0, draggableId);
const newChapter = {
...chapter,
activityIds: newActivityIds,
};
const newState = {
...data,
chapters: {
...data.chapters,
[newChapter.id]: newChapter,
},
};
props.dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: newState })
props.dispatchSavedContent({ type: 'unsaved_content' })
//setData(newState);
return;
}
//////////////////////// ACTIVITIES IN DIFF CHAPTERS ////////////////////////////
// check if the activity is dropped in a different chapter
if (start !== finish) {
// create new arrays for chapters and activities
const startChapterActivityIds = Array.from(start.activityIds);
// remove the activity from the old position
startChapterActivityIds.splice(source.index, 1);
const newStart = {
...start,
activityIds: startChapterActivityIds,
};
// add the activity to the new position within the chapter
const finishChapterActivityIds = Array.from(finish.activityIds);
finishChapterActivityIds.splice(destination.index, 0, draggableId);
const newFinish = {
...finish,
activityIds: finishChapterActivityIds,
};
const newState = {
...data,
chapters: {
...data.chapters,
[newStart.id]: newStart,
[newFinish.id]: newFinish,
},
};
props.dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: newState })
props.dispatchSavedContent({ type: 'unsaved_content' })
//setData(newState);
return;
}
};
return (
<>
<div className=""
>
<GeneralWrapperStyled>
<Modal
isDialogOpen={newActivityModal}
onOpenChange={setNewActivityModal}
minHeight="no-min"
addDefCloseButton={false}
dialogContent={<NewActivityModal
closeModal={closeNewActivityModal}
submitFileActivity={submitFileActivity}
submitExternalVideo={submitExternalVideo}
submitActivity={submitActivity}
chapterId={newActivityModalData}
></NewActivityModal>}
dialogTitle="Create Activity"
dialogDescription="Choose between types of activities to add to the course"
/>
{winReady && (
<div className="flex flex-col">
<DragDropContext onDragEnd={onDragEnd}>
<Droppable key="chapters" droppableId="chapters" type="chapter">
{(provided) => (
<>
<div key={"chapters"} {...provided.droppableProps} ref={provided.innerRef}>
{getChapters().map((info: any, index: any) => (
<>
<Chapter
orgslug={orgslug}
courseid={courseid}
openNewActivityModal={openNewActivityModal}
deleteChapter={deleteChapterUI}
key={index}
info={info}
index={index}
></Chapter>
</>
))}
{provided.placeholder}
</div>
</>
)}
</Droppable>
</DragDropContext>
<Modal
isDialogOpen={newChapterModal}
onOpenChange={setNewChapterModal}
minHeight="sm"
dialogContent={<NewChapterModal
closeModal={closeNewChapterModal}
submitChapter={submitChapter}
></NewChapterModal>}
dialogTitle="Create chapter"
dialogDescription="Add a new chapter to the course"
dialogTrigger={
<div className="flex max-w-7xl bg-black text-sm shadow rounded-md items-center text-white justify-center mx-auto space-x-2 p-3 w-72 hover:bg-gray-900 hover:cursor-pointer">
<Hexagon size={16} />
<div>Add chapter +</div>
</div>
}
/>
</div>
)}
</GeneralWrapperStyled >
</div>
</>
);
}
export default CourseContentEdition;

View file

@ -1,116 +0,0 @@
"use client";
import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input, Textarea } from '@components/StyledElements/Form/Form'
import * as Form from '@radix-ui/react-form';
import { useFormik } from 'formik';
import { AlertTriangle } from "lucide-react";
import React from "react";
const validate = (values: any) => {
const errors: any = {};
if (!values.name) {
errors.name = 'Required';
}
if (values.name.length > 100) {
errors.name = 'Must be 80 characters or less';
}
if (!values.mini_description) {
errors.mini_description = 'Required';
}
if (values.mini_description.length > 200) {
errors.mini_description = 'Must be 200 characters or less';
}
if (!values.description) {
errors.description = 'Required';
}
if (values.description.length > 1000) {
errors.description = 'Must be 1000 characters or less';
}
if (!values.learnings) {
errors.learnings = 'Required';
}
return errors;
};
function CourseEdition(props: any) {
const [error, setError] = React.useState('');
const formik = useFormik({
initialValues: {
name: String(props.data.name),
mini_description: String(props.data.mini_description),
description: String(props.data.description),
learnings: String(props.data.learnings),
},
validate,
onSubmit: async values => {
},
});
React.useEffect(() => {
// This code will run whenever form values are updated
if (formik.values !== formik.initialValues) {
props.dispatchSavedContent({ type: 'unsaved_content' });
const updatedCourse = {
...props.data,
name: formik.values.name,
mini_description: formik.values.mini_description,
description: formik.values.description,
learnings: formik.values.learnings.split(", "),
};
props.dispatchCourseMetadata({ type: 'updated_course', payload: updatedCourse });
}
}, [formik.values, formik.initialValues]);
return (
<div className='max-w-screen-2xl mx-auto px-16 pt-5 tracking-tight'>
<div className="login-form">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
<AlertTriangle size={18} />
<div className="font-bold text-sm">{error}</div>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="name">
<FormLabelAndMessage label='Name' message={formik.errors.name} />
<Form.Control asChild>
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.name} type="text" required />
</Form.Control>
</FormField>
<FormField name="mini_description">
<FormLabelAndMessage label='Mini description' message={formik.errors.mini_description} />
<Form.Control asChild>
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.mini_description} type="text" required />
</Form.Control>
</FormField>
<FormField name="description">
<FormLabelAndMessage label='Description' message={formik.errors.description} />
<Form.Control asChild>
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.description} required />
</Form.Control>
</FormField>
<FormField name="learnings">
<FormLabelAndMessage label='Learnings (Separated by , )' message={formik.errors.learnings} />
<Form.Control asChild>
<Textarea placeholder='Science, Design, Architecture' style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.learnings} required />
</Form.Control>
</FormField>
</FormLayout>
</div>
</div>
)
}
export default CourseEdition

View file

@ -0,0 +1,137 @@
"use client";
import Link from "next/link";
import { getUriWithOrg } from "@services/config/config";
import Canva from "@components/Objects/Activities/DynamicCanva/DynamicCanva";
import VideoActivity from "@components/Objects/Activities/Video/Video";
import { Check } from "lucide-react";
import { markActivityAsComplete } from "@services/courses/activity";
import DocumentPdfActivity from "@components/Objects/Activities/DocumentPdf/DocumentPdf";
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { useRouter } from "next/navigation";
import AuthenticatedClientElement from "@components/Security/AuthenticatedClientElement";
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
import { useOrg } from "@components/Contexts/OrgContext";
import { CourseProvider } from "@components/Contexts/CourseContext";
interface ActivityClientProps {
activityid: string;
courseuuid: string;
orgslug: string;
activity: any;
course: any;
}
function ActivityClient(props: ActivityClientProps) {
const activityid = props.activityid;
const courseuuid = props.courseuuid;
const orgslug = props.orgslug;
const activity = props.activity;
const course = props.course;
const org = useOrg() as any;
function getChapterName(chapterId: string) {
let chapterName = "";
course.chapters.forEach((chapter: any) => {
if (chapter.id === chapterId) {
chapterName = chapter.name;
}
});
return chapterName;
}
return (
<>
<CourseProvider courseuuid={course?.course_uuid}>
<GeneralWrapperStyled>
<div className="space-y-4 pt-4">
<div className="flex space-x-6">
<div className="flex">
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}`}>
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(org?.org_uuid, course.course_uuid, course.thumbnail_image)}`} alt="" />
</Link>
</div>
<div className="flex flex-col -space-y-1">
<p className="font-bold text-gray-700 text-md">Course </p>
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{course.name}</h1>
</div>
</div>
<ActivityIndicators course_uuid={courseuuid} current_activity={activityid} orgslug={orgslug} course={course} />
<div className="flex justify-between items-center">
<div className="flex flex-col -space-y-1">
<p className="font-bold text-gray-700 text-md">Chapter : {getChapterName(activity.coursechapter_id)}</p>
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{activity.name}</h1>
</div>
<div className="flex space-x-2">
<AuthenticatedClientElement checkMethod="authentication">
<MarkStatus activity={activity} activityid={activityid} course={course} orgslug={orgslug} />
</AuthenticatedClientElement>
</div>
</div>
{activity ? (
<div className={`p-7 pt-4 drop-shadow-sm rounded-lg ${activity.activity_type == 'TYPE_DYNAMIC' ? 'bg-white' : 'bg-zinc-950'}`}>
<div>
{activity.activity_type == "TYPE_DYNAMIC" && <Canva content={activity.content} activity={activity} />}
{/* todo : use apis & streams instead of this */}
{activity.activity_type == "TYPE_VIDEO" && <VideoActivity course={course} activity={activity} />}
{activity.activity_type == "TYPE_DOCUMENT" && <DocumentPdfActivity course={course} activity={activity} />}
</div>
</div>
) : (<div></div>)}
{<div style={{ height: "100px" }}></div>}
</div>
</GeneralWrapperStyled>
</CourseProvider>
</>
);
}
export function MarkStatus(props: { activity: any, activityid: string, course: any, orgslug: string }) {
const router = useRouter();
console.log(props.course.trail)
async function markActivityAsCompleteFront() {
const trail = await markActivityAsComplete(props.orgslug, props.course.course_uuid, 'activity_' + props.activityid);
router.refresh();
}
const isActivityCompleted = () => {
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 == props.activity.id);
}
}
console.log('isActivityCompleted', isActivityCompleted());
return (
<>{isActivityCompleted() ? (
<div className="bg-teal-600 rounded-md drop-shadow-md flex flex-col p-3 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" >
<i>
<Check size={15}></Check>
</i>{" "}
Already completed
</div>
) : (
<div className="bg-zinc-600 rounded-md drop-shadow-md flex flex-col p-3 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" onClick={markActivityAsCompleteFront}>
{" "}
<i>
<Check size={15}></Check>
</i>{" "}
Mark as complete
</div>
)}</>
)
}
export default ActivityClient;

View file

@ -8,7 +8,7 @@ import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshToke
type MetadataProps = { type MetadataProps = {
params: { orgslug: string, courseid: string, activityid: string }; params: { orgslug: string, courseuuid: string, activityid: string };
searchParams: { [key: string]: string | string[] | undefined }; searchParams: { [key: string]: string | string[] | undefined };
}; };
@ -20,14 +20,14 @@ export async function generateMetadata(
// Get Org context information // Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] }); const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) const course_meta = await getCourseMetadataWithAuthHeader(params.courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
const activity = await getActivityWithAuthHeader(params.activityid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null) const activity = await getActivityWithAuthHeader(params.activityid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null)
// SEO // SEO
return { return {
title: activity.name + `${course_meta.course.name} Course`, title: activity.name + `${course_meta.name} Course`,
description: course_meta.course.mini_description, description: course_meta.description,
keywords: course_meta.course.learnings, keywords: course_meta.learnings,
robots: { robots: {
index: true, index: true,
follow: true, follow: true,
@ -39,11 +39,10 @@ export async function generateMetadata(
} }
}, },
openGraph: { openGraph: {
title: activity.name + `${course_meta.course.name} Course`, title: activity.name + `${course_meta.name} Course`,
description: course_meta.course.mini_description, description: course_meta.description,
type: activity.type === 'video' ? 'video.other' : 'article', publishedTime: course_meta.creation_date,
publishedTime: course_meta.course.creationDate, tags: course_meta.learnings,
tags: course_meta.course.learnings,
}, },
}; };
} }
@ -52,16 +51,16 @@ const ActivityPage = async (params: any) => {
const cookieStore = cookies(); const cookieStore = cookies();
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const activityid = params.params.activityid; const activityid = params.params.activityid;
const courseid = params.params.courseid; const courseuuid = params.params.courseuuid;
const orgslug = params.params.orgslug; const orgslug = params.params.orgslug;
const course_meta = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) const course_meta = await getCourseMetadataWithAuthHeader(courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
const activity = await getActivityWithAuthHeader(activityid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null) const activity = await getActivityWithAuthHeader(activityid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null)
return ( return (
<> <>
<ActivityClient <ActivityClient
activityid={activityid} activityid={activityid}
courseid={courseid} courseuuid={courseuuid}
orgslug={orgslug} orgslug={orgslug}
activity={activity} activity={activity}
course={course_meta} course={course_meta}

View file

@ -12,26 +12,28 @@ import { getCourseThumbnailMediaDirectory } from "@services/media/media";
import { ArrowRight, Check, File, Sparkles, Star, Video } from "lucide-react"; import { ArrowRight, Check, File, Sparkles, Star, Video } from "lucide-react";
import Avvvatars from "avvvatars-react"; import Avvvatars from "avvvatars-react";
import { getUser } from "@services/users/users"; import { getUser } from "@services/users/users";
import { useOrg } from "@components/Contexts/OrgContext";
const CourseClient = (props: any) => { const CourseClient = (props: any) => {
const [user, setUser] = useState<any>({}); const [user, setUser] = useState<any>({});
const courseid = props.courseid; const [learnings, setLearnings] = useState<any>([]);
const courseuuid = props.courseuuid;
const orgslug = props.orgslug; const orgslug = props.orgslug;
const course = props.course; const course = props.course;
const org = useOrg() as any;
const router = useRouter(); const router = useRouter();
function getLearningTags() {
// create array of learnings from a string object (comma separated)
let learnings = course.learnings.split(",");
setLearnings(learnings);
async function getUserUI() {
let user_id = course.course.authors[0];
const user = await getUser(user_id);
setUser(user);
console.log(user);
} }
async function startCourseUI() { async function startCourseUI() {
// Create activity // Create activity
await startCourse("course_" + courseid, orgslug); await startCourse("course_" + courseuuid, orgslug);
await revalidateTags(['courses'], orgslug); await revalidateTags(['courses'], orgslug);
router.refresh(); router.refresh();
@ -39,18 +41,24 @@ const CourseClient = (props: any) => {
// window.location.reload(); // window.location.reload();
} }
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");
}
async function quitCourse() { async function quitCourse() {
// Close activity // Close activity
let activity = await removeCourse("course_" + courseid, orgslug); let activity = await removeCourse("course_" + courseuuid, orgslug);
// Mutate course // Mutate course
await revalidateTags(['courses'], orgslug); await revalidateTags(['courses'], orgslug);
router.refresh(); router.refresh();
} }
useEffect(() => { useEffect(() => {
getUserUI();
} }
, []); , [org]);
return ( return (
<> <>
@ -61,26 +69,26 @@ const CourseClient = (props: any) => {
<div className="pb-3"> <div className="pb-3">
<p className="text-md font-bold text-gray-400 pb-2">Course</p> <p className="text-md font-bold text-gray-400 pb-2">Course</p>
<h1 className="text-3xl -mt-3 font-bold"> <h1 className="text-3xl -mt-3 font-bold">
{course.course.name} {course.name}
</h1> </h1>
</div> </div>
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[300px] bg-cover bg-center mb-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(course.course.org_id, course.course.course_id, course.course.thumbnail)})` }}> <div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[300px] bg-cover bg-center mb-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org?.org_uuid, course.course_uuid, course.thumbnail_image)})` }}>
</div> </div>
<ActivityIndicators course_id={props.course.course.course_id} orgslug={orgslug} course={course} /> <ActivityIndicators course_uuid={props.course.course_uuid} orgslug={orgslug} course={course} />
<div className="flex flex-row pt-10"> <div className="flex flex-row pt-10">
<div className="course_metadata_left grow space-y-2"> <div className="course_metadata_left grow space-y-2">
<h2 className="py-3 text-2xl font-bold">Description</h2> <h2 className="py-3 text-2xl font-bold">Description</h2>
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden"> <div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
<p className="py-5 px-5">{course.course.description}</p> <p className="py-5 px-5">{course.description}</p>
</div> </div>
<h2 className="py-3 text-2xl font-bold">What you will learn</h2> <h2 className="py-3 text-2xl font-bold">What you will learn</h2>
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden px-5 py-5 space-y-2"> <div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden px-5 py-5 space-y-2">
{course.course.learnings.map((learning: any) => { {learnings.map((learning: any) => {
return ( return (
<div key={learning} <div key={learning}
className="flex space-x-2 items-center font-semibold text-gray-500 capitalize"> className="flex space-x-2 items-center font-semibold text-gray-500 capitalize">
@ -118,48 +126,48 @@ const CourseClient = (props: any) => {
</p> </p>
<div className="flex space-x-1 py-2 px-4 items-center"> <div className="flex space-x-1 py-2 px-4 items-center">
<div className="courseicon items-center flex space-x-2 text-neutral-400"> <div className="courseicon items-center flex space-x-2 text-neutral-400">
{activity.type === "dynamic" && {activity.activity_type === "TYPE_DYNAMIC" &&
<div className="bg-gray-100 px-2 py-2 rounded-full"> <div className="bg-gray-100 px-2 py-2 rounded-full">
<Sparkles className="text-gray-400" size={13} /> <Sparkles className="text-gray-400" size={13} />
</div> </div>
} }
{activity.type === "video" && {activity.activity_type === "TYPE_VIDEO" &&
<div className="bg-gray-100 px-2 py-2 rounded-full"> <div className="bg-gray-100 px-2 py-2 rounded-full">
<Video className="text-gray-400" size={13} /> <Video className="text-gray-400" size={13} />
</div> </div>
} }
{activity.type === "documentpdf" && {activity.activity_type === "TYPE_DOCUMENT" &&
<div className="bg-gray-100 px-2 py-2 rounded-full"> <div className="bg-gray-100 px-2 py-2 rounded-full">
<File className="text-gray-400" size={13} /> <File className="text-gray-400" size={13} />
</div> </div>
} }
</div> </div>
<Link className="flex font-semibold grow pl-2 text-neutral-500" href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`} rel="noopener noreferrer"> <Link className="flex font-semibold grow pl-2 text-neutral-500" href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}/activity/${activity.activity_uuid.replace("activity_", "")}`} rel="noopener noreferrer">
<p>{activity.name}</p> <p>{activity.name}</p>
</Link> </Link>
<div className="flex "> <div className="flex ">
{activity.type === "dynamic" && {activity.activity_type === "TYPE_DYNAMIC" &&
<> <>
<Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`} rel="noopener noreferrer"> <Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}/activity/${activity.activity_uuid.replace("activity_", "")}`} rel="noopener noreferrer">
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center"> <div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
<p>Page</p> <p>Page</p>
<ArrowRight size={13} /></div> <ArrowRight size={13} /></div>
</Link> </Link>
</> </>
} }
{activity.type === "video" && {activity.activity_type === "TYPE_VIDEO" &&
<> <>
<Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`} rel="noopener noreferrer"> <Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}/activity/${activity.activity_uuid.replace("activity_", "")}`} rel="noopener noreferrer">
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center"> <div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
<p>Video</p> <p>Video</p>
<ArrowRight size={13} /></div> <ArrowRight size={13} /></div>
</Link> </Link>
</> </>
} }
{activity.type === "documentpdf" && {activity.activity_type === "TYPE_DOCUMENT" &&
<> <>
<Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`} rel="noopener noreferrer"> <Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}/activity/${activity.activity_uuid.replace("activity_", "")}`} rel="noopener noreferrer">
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center"> <div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
<p>Document</p> <p>Document</p>
<ArrowRight size={13} /></div> <ArrowRight size={13} /></div>
@ -178,19 +186,20 @@ const CourseClient = (props: any) => {
</div> </div>
<div className="course_metadata_right space-y-3 w-64 antialiased flex flex-col ml-10 h-fit p-3 py-5 bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden"> <div className="course_metadata_right space-y-3 w-64 antialiased flex flex-col ml-10 h-fit p-3 py-5 bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
{ user && {user &&
<div className="flex mx-auto space-x-3 px-2 py-2 items-center"> <div className="flex mx-auto space-x-3 px-2 py-2 items-center">
<div className=""> <div className="">
<Avvvatars border borderSize={5} borderColor="white" size={50} shadow value={course.course.authors[0]} style='shape' /> <Avvvatars border borderSize={5} borderColor="white" size={50} shadow value={course.authors[0].username} style='shape' />
</div>
<div className="-space-y-2 ">
<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>
</div> </div>
<div className="-space-y-2 ">
<div className="text-[12px] text-neutral-400 font-semibold">Author</div>
<div className="text-xl font-bold text-neutral-800">{user.full_name}</div>
</div>
</div>
} }
{console.log(course)}
{course.trail.status == "ongoing" ? ( {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}>
Quit Course Quit Course
</button> </button>

View file

@ -7,7 +7,7 @@ import { Metadata } from 'next';
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from '@services/auth/auth'; import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from '@services/auth/auth';
type MetadataProps = { type MetadataProps = {
params: { orgslug: string, courseid: string }; params: { orgslug: string, courseuuid: string };
searchParams: { [key: string]: string | string[] | undefined }; searchParams: { [key: string]: string | string[] | undefined };
}; };
@ -19,14 +19,14 @@ export async function generateMetadata(
// Get Org context information // Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] }); const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) const course_meta = await getCourseMetadataWithAuthHeader(params.courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
// SEO // SEO
return { return {
title: course_meta.course.name + `${org.name}`, title: course_meta.name + `${org.name}`,
description: course_meta.course.mini_description, description: course_meta.description,
keywords: course_meta.course.learnings, keywords: course_meta.learnings,
robots: { robots: {
index: true, index: true,
follow: true, follow: true,
@ -38,11 +38,11 @@ export async function generateMetadata(
} }
}, },
openGraph: { openGraph: {
title: course_meta.course.name + `${org.name}`, title: course_meta.name + `${org.name}`,
description: course_meta.course.mini_description, description: course_meta.description ? course_meta.description : '',
type: 'article', type: 'article',
publishedTime: course_meta.course.creationDate, publishedTime: course_meta.creation_date ? course_meta.creation_date : '',
tags: course_meta.course.learnings, tags: course_meta.learnings ? course_meta.learnings : [],
}, },
}; };
} }
@ -50,14 +50,14 @@ export async function generateMetadata(
const CoursePage = async (params: any) => { const CoursePage = async (params: any) => {
const cookieStore = cookies(); const cookieStore = cookies();
const courseid = params.params.courseid const courseuuid = params.params.courseuuid
const orgslug = params.params.orgslug; const orgslug = params.params.orgslug;
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const course_meta = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) const course_meta = await getCourseMetadataWithAuthHeader(courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
return ( return (
<div> <div>
<CourseClient courseid={courseid} orgslug={orgslug} course={course_meta} /> <CourseClient courseuuid={courseuuid} orgslug={orgslug} course={course_meta} />
</div> </div>
) )
} }

View file

@ -6,7 +6,7 @@ import { useSearchParams } from 'next/navigation';
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'; import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper';
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle'; import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle';
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'; import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
import CourseThumbnail from '@components/Objects/Other/CourseThumbnail'; import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton'; import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
interface CourseProps { interface CourseProps {
@ -32,7 +32,10 @@ function Courses(props: CourseProps) {
<div className='flex flex-wrap justify-between'> <div className='flex flex-wrap justify-between'>
<TypeOfContentTitle title="Courses" type="cou" /> <TypeOfContentTitle title="Courses" type="cou" />
<AuthenticatedClientElement checkMethod='roles' orgId={props.org_id}> <AuthenticatedClientElement checkMethod='roles'
action='create'
ressourceType='course'
orgId={props.org_id}>
<Modal <Modal
isDialogOpen={newCourseModal} isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal} onOpenChange={setNewCourseModal}
@ -56,7 +59,7 @@ function Courses(props: CourseProps) {
<div className="flex flex-wrap"> <div className="flex flex-wrap">
{courses.map((course: any) => ( {courses.map((course: any) => (
<div className="px-3" key={course.course_id}> <div className="px-3" key={course.course_uuid}>
<CourseThumbnail course={course} orgslug={orgslug} /> <CourseThumbnail course={course} orgslug={orgslug} />
</div> </div>
))} ))}
@ -73,7 +76,10 @@ function Courses(props: CourseProps) {
<h1 className="text-3xl font-bold text-gray-600">No courses yet</h1> <h1 className="text-3xl font-bold text-gray-600">No courses yet</h1>
<p className="text-lg text-gray-400">Create a course to add content</p> <p className="text-lg text-gray-400">Create a course to add content</p>
</div> </div>
<AuthenticatedClientElement checkMethod='roles' orgId={props.org_id}> <AuthenticatedClientElement
action='create'
ressourceType='course'
checkMethod='roles' orgId={props.org_id}>
<Modal <Modal
isDialogOpen={newCourseModal} isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal} onOpenChange={setNewCourseModal}

View file

@ -1,6 +1,6 @@
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/AuthProvider"; import AuthProvider from "@components/Security/AuthProviderDepreceated";
export default function RootLayout({ children, params }: { children: React.ReactNode , params :any}) { export default function RootLayout({ children, params }: { children: React.ReactNode , params :any}) {
return ( return (

View file

@ -9,8 +9,8 @@ import { cookies } from 'next/headers';
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'; import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper';
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle'; import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle';
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'; import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth';
import CourseThumbnail from '@components/Objects/Other/CourseThumbnail'; import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
import CollectionThumbnail from '@components/Objects/Other/CollectionThumbnail'; import CollectionThumbnail from '@components/Objects/Thumbnails/CollectionThumbnail';
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'; import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
import { Plus, PlusCircle } from 'lucide-react'; import { Plus, PlusCircle } from 'lucide-react';
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton'; import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
@ -56,8 +56,8 @@ const OrgHomePage = async (params: any) => {
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null); const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null);
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
const org_id = org.org_id; const org_id = org.id;
const collections = await getOrgCollectionsWithAuthHeader(org.org_id, access_token ? access_token : null, { revalidate: 0, tags: ['courses'] }); const collections = await getOrgCollectionsWithAuthHeader(org.id, access_token ? access_token : null, { revalidate: 0, tags: ['courses'] });
return ( return (
<div> <div>
@ -67,7 +67,11 @@ const OrgHomePage = async (params: any) => {
<div className='flex grow'> <div className='flex grow'>
<TypeOfContentTitle title="Collections" type="col" /> <TypeOfContentTitle title="Collections" type="col" />
</div> </div>
<AuthenticatedClientElement checkMethod='roles' orgId={org_id}> <AuthenticatedClientElement
checkMethod='roles'
ressourceType='collection'
action='create'
orgId={org_id}>
<Link href={getUriWithOrg(orgslug, "/collections/new")}> <Link href={getUriWithOrg(orgslug, "/collections/new")}>
<NewCollectionButton /> <NewCollectionButton />
</Link> </Link>
@ -105,7 +109,11 @@ const OrgHomePage = async (params: any) => {
<div className='flex grow'> <div className='flex grow'>
<TypeOfContentTitle title="Courses" type="cou" /> <TypeOfContentTitle title="Courses" type="cou" />
</div> </div>
<AuthenticatedClientElement checkMethod='roles' orgId={org_id}> <AuthenticatedClientElement
ressourceType='course'
action='create'
checkMethod='roles'
orgId={org_id}>
<Link href={getUriWithOrg(orgslug, "/courses?new=true")}> <Link href={getUriWithOrg(orgslug, "/courses?new=true")}>
<NewCourseButton /> <NewCourseButton />
</Link> </Link>
@ -113,7 +121,7 @@ const OrgHomePage = async (params: any) => {
</div> </div>
<div className="home_courses flex flex-wrap"> <div className="home_courses flex flex-wrap">
{courses.map((course: any) => ( {courses.map((course: any) => (
<div className="py-3 px-3" key={course.course_id}> <div className="py-3 px-3" key={course.course_uuid}>
<CourseThumbnail course={course} orgslug={orgslug} /> <CourseThumbnail course={course} orgslug={orgslug} />
</div> </div>
))} ))}

View file

@ -1,4 +1,5 @@
"use client"; "use client";
import { useOrg } from "@components/Contexts/OrgContext";
import PageLoading from "@components/Objects/Loaders/PageLoading"; import PageLoading from "@components/Objects/Loaders/PageLoading";
import TrailCourseElement from "@components/Pages/Trail/TrailCourseElement"; import TrailCourseElement from "@components/Pages/Trail/TrailCourseElement";
import TypeOfContentTitle from "@components/StyledElements/Titles/TypeOfContentTitle"; import TypeOfContentTitle from "@components/StyledElements/Titles/TypeOfContentTitle";
@ -6,13 +7,18 @@ import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWra
import { getAPIUrl } from "@services/config/config"; import { getAPIUrl } from "@services/config/config";
import { removeCourse } from "@services/courses/activity"; import { removeCourse } from "@services/courses/activity";
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests"; import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
import React from "react"; import React, { useEffect } from "react";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
function Trail(params: any) { function Trail(params: any) {
let orgslug = params.orgslug; let orgslug = params.orgslug;
const { data: trail, error: error } = useSWR(`${getAPIUrl()}trail/org_slug/${orgslug}/trail`, swrFetcher); const org = useOrg() as any;
const orgID = org?.id;
const { data: trail, error: error } = useSWR(`${getAPIUrl()}trail/org/${orgID}/trail`, swrFetcher);
useEffect(() => {
}
, [trail,org]);
return ( return (
<GeneralWrapperStyled> <GeneralWrapperStyled>
@ -21,12 +27,10 @@ function Trail(params: any) {
<PageLoading></PageLoading> <PageLoading></PageLoading>
) : ( ) : (
<div className="space-y-6"> <div className="space-y-6">
{trail.courses.map((course: any) => ( {trail.runs.map((run: any) => (
!course.masked ? ( <>
<TrailCourseElement key={trail.trail_id} orgslug={orgslug} course={course} /> <TrailCourseElement run={run} course={run.course} orgslug={orgslug} />
) : ( </>
<></>
)
))} ))}

View file

@ -0,0 +1,109 @@
'use client';
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse';
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
import Modal from '@components/StyledElements/Modal/Modal';
import Link from 'next/link'
import { useSearchParams } from 'next/navigation';
import React from 'react'
type CourseProps = {
orgslug: string;
courses: any;
org_id: string;
}
function CoursesHome(params: CourseProps) {
const searchParams = useSearchParams();
const isCreatingCourse = searchParams.get('new') ? true : false;
const [newCourseModal, setNewCourseModal] = React.useState(isCreatingCourse);
const orgslug = params.orgslug;
const courses = params.courses;
async function closeNewCourseModal() {
setNewCourseModal(false);
}
return (
<div className='h-full w-full bg-[#f8f8f8]'>
<div >
<div className='pl-10 mr-10 tracking-tighter'>
<BreadCrumbs type='courses' />
<div className='w-100 flex justify-between'>
<div className='pt-3 flex font-bold text-4xl'>Courses</div>
<AuthenticatedClientElement checkMethod='roles'
action='create'
ressourceType='course'
orgId={params.org_id}>
<Modal
isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal}
minHeight="md"
dialogContent={<CreateCourseModal
closeModal={closeNewCourseModal}
orgslug={orgslug}
></CreateCourseModal>}
dialogTitle="Create Course"
dialogDescription="Create a new course"
dialogTrigger={
<button>
<NewCourseButton />
</button>}
/>
</AuthenticatedClientElement>
</div>
</div>
</div>
<div className="flex flex-wrap mx-8 mt-7">
{courses.map((course: any) => (
<div className="px-3" key={course.course_uuid}>
<CourseThumbnail course={course} orgslug={orgslug} />
</div>
))}
{courses.length == 0 &&
<div className="flex mx-auto h-[400px]">
<div className="flex flex-col justify-center text-center items-center space-y-5">
<div className='mx-auto'>
<svg width="120" height="120" viewBox="0 0 295 295" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.51" x="10" y="10" width="275" height="275" rx="75" stroke="#4B5564" strokeOpacity="0.15" strokeWidth="20" />
<path d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z" fill="#4B5564" fillOpacity="0.08" />
</svg>
</div>
<div className="space-y-0">
<h1 className="text-3xl font-bold text-gray-600">No courses yet</h1>
<p className="text-lg text-gray-400">Create a course to add content</p>
</div>
<AuthenticatedClientElement
action='create'
ressourceType='course'
checkMethod='roles' orgId={params.org_id}>
<Modal
isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal}
minHeight="md"
dialogContent={<CreateCourseModal
closeModal={closeNewCourseModal}
orgslug={orgslug}
></CreateCourseModal>}
dialogTitle="Create Course"
dialogDescription="Create a new course"
dialogTrigger={
<button>
<NewCourseButton />
</button>}
/>
</AuthenticatedClientElement>
</div>
</div>
}
</div>
</div>
)
}
export default CoursesHome

View file

@ -0,0 +1,79 @@
'use client';
import EditCourseStructure from '../../../../../../../../components/Dashboard/Course/EditCourseStructure/EditCourseStructure'
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import PageLoading from '@components/Objects/Loaders/PageLoading';
import ClientComponentSkeleton from '@components/Utils/ClientComp';
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
import { swrFetcher } from '@services/utils/ts/requests';
import React, { createContext, use, useEffect, useState } from 'react'
import useSWR from 'swr';
import { CourseProvider, useCourse } from '../../../../../../../../components/Contexts/CourseContext';
import SaveState from '@components/Dashboard/UI/SaveState';
import Link from 'next/link';
import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop';
import { CSSTransition } from 'react-transition-group';
import { motion } from 'framer-motion';
import EditCourseGeneral from '@components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral';
import { GalleryVertical, GalleryVerticalEnd, Info } from 'lucide-react';
export type CourseOverviewParams = {
orgslug: string,
courseuuid: string,
subpage: string
}
function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
function getEntireCourseUUID(courseuuid: string) {
// add course_ to uuid
return `course_${courseuuid}`
}
return (
<div className='h-full w-full bg-[#f8f8f8]'>
<CourseProvider courseuuid={getEntireCourseUUID(params.courseuuid)}>
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'>
<CourseOverviewTop params={params} />
<div className='flex space-x-5 font-black text-sm'>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/courses/course/${params.courseuuid}/general`}>
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
<div className='flex items-center space-x-2.5 mx-2'>
<Info size={16} />
<div>General</div>
</div>
</div>
</Link>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/courses/course/${params.courseuuid}/content`}>
<div className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'content' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
<div className='flex items-center space-x-2.5 mx-2'>
<GalleryVerticalEnd size={16} />
<div>Content</div>
</div>
</div>
</Link>
</div>
</div>
<div className='h-6'></div>
<motion.div
initial={{ opacity: 0, }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
>
{params.subpage == 'content' ? <EditCourseStructure orgslug={params.orgslug} /> : ''}
{params.subpage == 'general' ? <EditCourseGeneral orgslug={params.orgslug} /> : ''}
</motion.div>
</CourseProvider>
</div>
)
}
export default CourseOverviewPage

View file

@ -0,0 +1,57 @@
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth';
import { getOrgCoursesWithAuthHeader } from '@services/courses/courses';
import { getOrganizationContextInfo } from '@services/organizations/orgs';
import { Metadata } from 'next';
import { cookies } from 'next/headers';
import React from 'react'
import CoursesHome from './client';
type MetadataProps = {
params: { orgslug: string };
searchParams: { [key: string]: string | string[] | undefined };
};
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
// SEO
return {
title: "Courses — " + org.name,
description: org.description,
keywords: `${org.name}, ${org.description}, courses, learning, education, online learning, edu, online courses, ${org.name} courses`,
robots: {
index: true,
follow: true,
nocache: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
}
},
openGraph: {
title: "Courses — " + org.name,
description: org.description,
type: 'website',
},
};
}
async function CoursesPage(params: any) {
const orgslug = params.params.orgslug;
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
const cookieStore = cookies();
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null);
return (
<CoursesHome org_id={org.org_id} orgslug={orgslug} courses={courses} />
)
}
export default CoursesPage

View file

@ -0,0 +1,20 @@
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>
<div className='flex'>
<LeftMenu/>
<div className='flex w-full'>
{children}
</div>
</div>
</AuthProvider>
</>
)
}
export default DashboardLayout

View file

@ -0,0 +1,51 @@
'use client';
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import { getUriWithOrg } from '@services/config/config'
import { Info } from 'lucide-react'
import Link from 'next/link'
import React from 'react'
import { motion } from 'framer-motion'
import OrgEditGeneral from '@components/Dashboard/Org/OrgEditGeneral/OrgEditGeneral'
export type OrgParams = {
subpage: string
orgslug: string
}
function OrgPage({ params }: { params: OrgParams }) {
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='org' ></BreadCrumbs>
<div className='my-2 tracking-tighter'>
<div className='w-100 flex justify-between'>
<div className='pt-3 flex font-bold text-4xl'>Organization Settings</div>
</div>
</div>
<div className='flex space-x-5 font-black text-sm'>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/org/settings/general`}>
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
<div className='flex items-center space-x-2.5 mx-2'>
<Info size={16} />
<div>General</div>
</div>
</div>
</Link>
</div>
</div>
<div className='h-6'></div>
<motion.div
initial={{ opacity: 0, }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
>
{params.subpage == 'general' ? <OrgEditGeneral /> : ''}
</motion.div>
</div>
)
}
export default OrgPage

View file

@ -0,0 +1,13 @@
import PageLoading from '@components/Objects/Loaders/PageLoading'
import React from 'react'
function DashboardHome() {
return (
<div className="flex items-center justify-center mx-auto min-h-screen flex-col space-x-3">
<PageLoading />
<div className='text-neutral-400 font-bold animate-pulse text-2xl'>This page is work in progress</div>
</div>
)
}
export default DashboardHome

View file

@ -0,0 +1,69 @@
'use client';
import React, { useEffect } from 'react'
import { motion } from 'framer-motion';
import UserEditGeneral from '@components/Dashboard/User/UserEditGeneral/UserEditGeneral';
import UserEditPassword from '@components/Dashboard/User/UserEditPassword/UserEditPassword';
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';
export type SettingsParams = {
subpage: string
orgslug: string
}
function SettingsPage({ params }: { params: SettingsParams }) {
const auth = useAuth() as any;
useEffect(() => {
}
, [auth])
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>
<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>
</div>
</div>
<div className='flex space-x-5 font-black text-sm'>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/user/settings/general`}>
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
<div className='flex items-center space-x-2.5 mx-2'>
<Info size={16} />
<div>General</div>
</div>
</div>
</Link>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/user/settings/security`}>
<div className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'security' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
<div className='flex items-center space-x-2.5 mx-2'>
<Lock size={16} />
<div>Password</div>
</div>
</div>
</Link>
</div>
</div>
<div className='h-6'></div>
<motion.div
initial={{ opacity: 0, }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
>
{params.subpage == 'general' ? <UserEditGeneral /> : ''}
{params.subpage == 'security' ? <UserEditPassword /> : ''}
</motion.div>
</div>
)
}
export default SettingsPage

Some files were not shown because too many files have changed in this diff Show more