mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: implement comprehensive RBAC checks for courses, chapters, collections, and activities, enhancing user rights management and security documentation
This commit is contained in:
parent
887046203e
commit
3ce019abec
22 changed files with 1788 additions and 598 deletions
|
|
@ -26,6 +26,7 @@ from src.services.courses.courses import (
|
|||
delete_course,
|
||||
update_course_thumbnail,
|
||||
search_courses,
|
||||
get_course_user_rights,
|
||||
)
|
||||
from src.services.courses.updates import (
|
||||
create_update,
|
||||
|
|
@ -358,12 +359,94 @@ async def api_remove_bulk_course_contributors(
|
|||
):
|
||||
"""
|
||||
Remove multiple contributors from a course by their usernames
|
||||
Only administrators can perform this action
|
||||
"""
|
||||
return await remove_bulk_course_contributors(
|
||||
request,
|
||||
course_uuid,
|
||||
usernames,
|
||||
current_user,
|
||||
db_session
|
||||
request, course_uuid, usernames, current_user, db_session
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{course_uuid}/rights")
|
||||
async def api_get_course_user_rights(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
db_session: Session = Depends(get_db_session),
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""
|
||||
Get detailed user rights for a specific course.
|
||||
|
||||
This endpoint returns comprehensive rights information that can be used
|
||||
by the UI to enable/disable features based on user permissions.
|
||||
|
||||
|
||||
|
||||
**Response Structure:**
|
||||
```json
|
||||
{
|
||||
"course_uuid": "course_123",
|
||||
"user_id": 456,
|
||||
"is_anonymous": false,
|
||||
"permissions": {
|
||||
"read": true,
|
||||
"create": false,
|
||||
"update": true,
|
||||
"delete": false,
|
||||
"create_content": true,
|
||||
"update_content": true,
|
||||
"delete_content": true,
|
||||
"manage_contributors": true,
|
||||
"manage_access": true,
|
||||
"grade_assignments": true,
|
||||
"mark_activities_done": true,
|
||||
"create_certifications": true
|
||||
},
|
||||
"ownership": {
|
||||
"is_owner": true,
|
||||
"is_creator": true,
|
||||
"is_maintainer": false,
|
||||
"is_contributor": false,
|
||||
"authorship_status": "ACTIVE"
|
||||
},
|
||||
"roles": {
|
||||
"is_admin": false,
|
||||
"is_maintainer_role": false,
|
||||
"is_instructor": true,
|
||||
"is_user": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Permissions Explained:**
|
||||
- `read`: Can read the course content
|
||||
- `create`: Can create new courses (instructor role or higher)
|
||||
- `update`: Can update course settings (title, description, etc.)
|
||||
- `delete`: Can delete the course
|
||||
- `create_content`: Can create activities, assignments, chapters, etc.
|
||||
- `update_content`: Can update course content
|
||||
- `delete_content`: Can delete course content
|
||||
- `manage_contributors`: Can add/remove contributors
|
||||
- `manage_access`: Can change course access settings (public, open_to_contributors)
|
||||
- `grade_assignments`: Can grade student assignments
|
||||
- `mark_activities_done`: Can mark activities as done for other users
|
||||
- `create_certifications`: Can create course certifications
|
||||
|
||||
**Ownership Information:**
|
||||
- `is_owner`: Is course owner (CREATOR, MAINTAINER, or CONTRIBUTOR)
|
||||
- `is_creator`: Is course creator
|
||||
- `is_maintainer`: Is course maintainer
|
||||
- `is_contributor`: Is course contributor
|
||||
- `authorship_status`: Current authorship status (ACTIVE, PENDING, INACTIVE)
|
||||
|
||||
**Role Information:**
|
||||
- `is_admin`: Has admin role (role 1)
|
||||
- `is_maintainer_role`: Has maintainer role (role 2)
|
||||
- `is_instructor`: Has instructor role (role 3)
|
||||
- `is_user`: Has basic user role (role 4)
|
||||
|
||||
**Security Notes:**
|
||||
- Returns rights based on course ownership and user roles
|
||||
- Safe to expose to UI as it only returns permission information
|
||||
- Anonymous users can only read public courses
|
||||
- All permissions are calculated based on current user context
|
||||
"""
|
||||
return await get_course_user_rights(request, course_uuid, current_user, db_session)
|
||||
|
|
|
|||
410
apps/api/src/security/courses_security.py
Normal file
410
apps/api/src/security/courses_security.py
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
"""
|
||||
SECURITY DOCUMENTATION FOR COURSES RBAC SYSTEM
|
||||
|
||||
This module provides unified RBAC (Role-Based Access Control) checks for all courses-related operations.
|
||||
|
||||
SECURITY MEASURES IMPLEMENTED:
|
||||
|
||||
1. COURSE OWNERSHIP REQUIREMENTS:
|
||||
- All non-read operations (create, update, delete) require course ownership
|
||||
- Course ownership is determined by ResourceAuthor table with ACTIVE status
|
||||
- Valid ownership roles: CREATOR, MAINTAINER, CONTRIBUTOR
|
||||
- Admin/maintainer roles are also accepted for course operations
|
||||
|
||||
2. COURSE CREATION VS COURSE CONTENT CREATION:
|
||||
- COURSE CREATION: Allow if user has instructor role (3) or higher
|
||||
- COURSE CONTENT CREATION (activities, assignments, chapters, etc.): Require course ownership (CREATOR, MAINTAINER, CONTRIBUTOR) or admin/maintainer role
|
||||
- This distinction allows instructors to create courses but prevents them from creating content in courses they don't own
|
||||
|
||||
3. STRICT ACCESS CONTROLS:
|
||||
- Activities: Require course ownership for all non-read operations
|
||||
- Assignments: Require course ownership for all non-read operations
|
||||
- Chapters: Require course ownership for all non-read operations
|
||||
- Certifications: Require course ownership for all non-read operations
|
||||
- Collections: Use organization-level permissions
|
||||
|
||||
4. GRADING AND SUBMISSION SECURITY:
|
||||
- Only course owners or instructors can grade assignments
|
||||
- Users can only submit their own work
|
||||
- Users cannot update grades unless they are instructors
|
||||
- Users can only update their own submissions
|
||||
|
||||
5. CERTIFICATE SECURITY:
|
||||
- Certificates can only be created by course owners or instructors
|
||||
- System-generated certificates (from course completion) are properly secured
|
||||
- Certificate creation requires proper RBAC checks
|
||||
|
||||
6. ACTIVITY MARKING SECURITY:
|
||||
- Only course owners or instructors can mark activities as done for other users
|
||||
- Users can only mark their own activities as done
|
||||
|
||||
7. COLLECTION SECURITY:
|
||||
- Users can only add courses to collections if they have read access to those courses
|
||||
- Collection operations require appropriate organization-level permissions
|
||||
|
||||
8. ANONYMOUS USER HANDLING:
|
||||
- Anonymous users can only read public courses
|
||||
- All non-read operations require authentication
|
||||
|
||||
9. ERROR HANDLING:
|
||||
- Clear error messages for security violations
|
||||
- Proper HTTP status codes (401, 403, 404)
|
||||
- Comprehensive logging of security events
|
||||
|
||||
10. COURSE ACCESS MANAGEMENT SECURITY:
|
||||
- Sensitive fields (public, open_to_contributors) require additional validation
|
||||
- Only course owners (CREATOR, MAINTAINER) or admins can change access settings
|
||||
- Course creation requires proper organization-level permissions
|
||||
- Course updates require course ownership or admin role
|
||||
|
||||
11. CONTRIBUTOR MANAGEMENT SECURITY:
|
||||
- Only course owners (CREATOR, MAINTAINER) or admins can add/remove contributors
|
||||
- Only course owners (CREATOR, MAINTAINER) or admins can update contributor roles
|
||||
- Cannot modify the role of the course creator
|
||||
- Contributor applications are created with PENDING status
|
||||
- Only course owners or admins can approve contributor applications
|
||||
|
||||
SECURITY BEST PRACTICES:
|
||||
- Always check course ownership before allowing modifications
|
||||
- Validate user permissions at multiple levels
|
||||
- Use proper RBAC checks for all operations
|
||||
- Implement principle of least privilege
|
||||
- Provide clear error messages for security violations
|
||||
- Log security events for audit purposes
|
||||
- Additional validation for sensitive access control fields
|
||||
- Strict ownership requirements for contributor management
|
||||
- Distinguish between course creation and course content creation permissions
|
||||
|
||||
CRITICAL SECURITY FIXES:
|
||||
- Fixed: Users could create certifications for courses they don't own
|
||||
- Fixed: Users could grade assignments without proper permissions
|
||||
- Fixed: Users could mark activities as done for other users without permissions
|
||||
- Fixed: Collections could be created with courses the user doesn't have access to
|
||||
- Fixed: Assignment submissions could be modified by unauthorized users
|
||||
- Fixed: Users could change course access settings (public, open_to_contributors) without proper permissions
|
||||
- Fixed: Users could add/remove contributors from courses they don't own
|
||||
- Fixed: Users could update contributor roles without course ownership
|
||||
- Fixed: Course creation used hardcoded RBAC check
|
||||
- Fixed: Contributor management used permissive RBAC checks instead of strict ownership requirements
|
||||
- Fixed: Instructors could create content in courses they don't own (now they can only create courses)
|
||||
"""
|
||||
|
||||
from typing import Literal
|
||||
from fastapi import HTTPException, Request, status
|
||||
from sqlmodel import Session, select
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.db.courses.courses import Course
|
||||
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum, ResourceAuthorshipStatusEnum
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
authorization_verify_based_on_org_admin_status,
|
||||
)
|
||||
|
||||
|
||||
async def courses_rbac_check(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
require_course_ownership: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
Unified RBAC check for courses-related operations.
|
||||
|
||||
SECURITY NOTES:
|
||||
- READ operations: Allow if user has read access to the course (public courses or user has permissions)
|
||||
- COURSE CREATION: Allow if user has instructor role (3) or higher
|
||||
- COURSE CONTENT CREATION (activities, assignments, chapters, etc.): Require course ownership (CREATOR, MAINTAINER, CONTRIBUTOR) or admin/maintainer role
|
||||
- UPDATE/DELETE operations: Require course ownership (CREATOR, MAINTAINER, CONTRIBUTOR) or admin/maintainer role
|
||||
- Course ownership is determined by ResourceAuthor table with ACTIVE status
|
||||
- Admin/maintainer roles are checked via authorization_verify_based_on_org_admin_status
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
course_uuid: UUID of the course (or "course_x" for course creation)
|
||||
current_user: Current user (PublicUser or AnonymousUser)
|
||||
action: Action to perform (create, read, update, delete)
|
||||
db_session: Database session
|
||||
require_course_ownership: If True, requires course ownership for non-read actions
|
||||
|
||||
Returns:
|
||||
bool: True if authorized, raises HTTPException otherwise
|
||||
|
||||
Raises:
|
||||
HTTPException: 403 Forbidden if user lacks required permissions
|
||||
HTTPException: 401 Unauthorized if user is anonymous for non-read actions
|
||||
"""
|
||||
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
return await authorization_verify_if_element_is_public(
|
||||
request, course_uuid, action, db_session
|
||||
)
|
||||
else:
|
||||
return await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, course_uuid, db_session
|
||||
)
|
||||
else:
|
||||
# For non-read actions, proceed with strict RBAC checks
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
# SECURITY: Special handling for course creation vs course content creation
|
||||
if action == "create" and course_uuid == "course_x":
|
||||
# This is course creation - allow instructors (role 3) or higher
|
||||
# Check if user has instructor role or higher
|
||||
from src.security.rbac.rbac import authorization_verify_based_on_roles
|
||||
|
||||
has_create_permission = await authorization_verify_based_on_roles(
|
||||
request, current_user.id, "create", "course_x", db_session
|
||||
)
|
||||
|
||||
if has_create_permission:
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You must have instructor role or higher to create courses",
|
||||
)
|
||||
|
||||
# SECURITY: For course content creation and other operations, require course ownership
|
||||
# This prevents users without course ownership from creating/modifying course content
|
||||
if require_course_ownership or action in ["create", "update", "delete"]:
|
||||
# Check if user is course owner (CREATOR, MAINTAINER, or CONTRIBUTOR)
|
||||
statement = select(ResourceAuthor).where(
|
||||
ResourceAuthor.resource_uuid == course_uuid,
|
||||
ResourceAuthor.user_id == current_user.id
|
||||
)
|
||||
resource_author = db_session.exec(statement).first()
|
||||
|
||||
is_course_owner = False
|
||||
if resource_author:
|
||||
if ((resource_author.authorship == ResourceAuthorshipEnum.CREATOR) or
|
||||
(resource_author.authorship == ResourceAuthorshipEnum.MAINTAINER) or
|
||||
(resource_author.authorship == ResourceAuthorshipEnum.CONTRIBUTOR)) and \
|
||||
resource_author.authorship_status == ResourceAuthorshipStatusEnum.ACTIVE:
|
||||
is_course_owner = True
|
||||
|
||||
# Check if user has admin or maintainer role
|
||||
is_admin_or_maintainer = await authorization_verify_based_on_org_admin_status(
|
||||
request, current_user.id, action, course_uuid, db_session
|
||||
)
|
||||
|
||||
# SECURITY: For creating, updating, and deleting course content, user MUST be either:
|
||||
# 1. Course owner (CREATOR, MAINTAINER, or CONTRIBUTOR with ACTIVE status)
|
||||
# 2. Admin or maintainer role
|
||||
# General role permissions are NOT sufficient for these actions
|
||||
if not (is_course_owner or is_admin_or_maintainer):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"You must be the course owner (CREATOR, MAINTAINER, or CONTRIBUTOR) or have admin/maintainer role to {action} in this course",
|
||||
)
|
||||
return True
|
||||
else:
|
||||
# For other actions, use the existing RBAC check
|
||||
return await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
course_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
||||
|
||||
async def courses_rbac_check_with_course_lookup(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
require_course_ownership: bool = False,
|
||||
) -> Course:
|
||||
"""
|
||||
Unified RBAC check for courses-related operations with course lookup.
|
||||
|
||||
SECURITY NOTES:
|
||||
- First validates that the course exists
|
||||
- Then performs RBAC check using courses_rbac_check
|
||||
- Returns the course object if authorized
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
course_uuid: UUID of the course
|
||||
current_user: Current user (PublicUser or AnonymousUser)
|
||||
action: Action to perform (create, read, update, delete)
|
||||
db_session: Database session
|
||||
require_course_ownership: If True, requires course ownership for non-read actions
|
||||
|
||||
Returns:
|
||||
Course: The course object if authorized, raises HTTPException otherwise
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 Not Found if course doesn't exist
|
||||
HTTPException: 403 Forbidden if user lacks required permissions
|
||||
"""
|
||||
|
||||
# First check if course exists
|
||||
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",
|
||||
)
|
||||
|
||||
# Perform RBAC check
|
||||
await courses_rbac_check(
|
||||
request, course_uuid, current_user, action, db_session, require_course_ownership
|
||||
)
|
||||
|
||||
return course
|
||||
|
||||
|
||||
async def courses_rbac_check_for_activities(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
) -> bool:
|
||||
"""
|
||||
Specialized RBAC check for activities that requires course ownership for non-read actions.
|
||||
|
||||
SECURITY NOTES:
|
||||
- Activities are core course content and require strict ownership controls
|
||||
- READ: Allow if user has read access to the course
|
||||
- CREATE/UPDATE/DELETE: Require course ownership (CREATOR, MAINTAINER, CONTRIBUTOR) or admin/maintainer role
|
||||
- This prevents unauthorized users from creating/modifying course activities
|
||||
- Instructors can create courses but cannot create activities in courses they don't own
|
||||
"""
|
||||
|
||||
return await courses_rbac_check(
|
||||
request, course_uuid, current_user, action, db_session, require_course_ownership=True
|
||||
)
|
||||
|
||||
|
||||
async def courses_rbac_check_for_assignments(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
) -> bool:
|
||||
"""
|
||||
Specialized RBAC check for assignments that requires course ownership for non-read actions.
|
||||
|
||||
SECURITY NOTES:
|
||||
- Assignments are course content and require strict ownership controls
|
||||
- READ: Allow if user has read access to the course
|
||||
- CREATE/UPDATE/DELETE: Require course ownership (CREATOR, MAINTAINER, CONTRIBUTOR) or admin/maintainer role
|
||||
- This prevents unauthorized users from creating/modifying course assignments
|
||||
- Instructors can create courses but cannot create assignments in courses they don't own
|
||||
"""
|
||||
|
||||
return await courses_rbac_check(
|
||||
request, course_uuid, current_user, action, db_session, require_course_ownership=True
|
||||
)
|
||||
|
||||
|
||||
async def courses_rbac_check_for_chapters(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
) -> bool:
|
||||
"""
|
||||
Specialized RBAC check for chapters that requires course ownership for non-read actions.
|
||||
|
||||
SECURITY NOTES:
|
||||
- Chapters are course structure and require strict ownership controls
|
||||
- READ: Allow if user has read access to the course
|
||||
- CREATE/UPDATE/DELETE: Require course ownership (CREATOR, MAINTAINER, CONTRIBUTOR) or admin/maintainer role
|
||||
- This prevents unauthorized users from creating/modifying course chapters
|
||||
- Instructors can create courses but cannot create chapters in courses they don't own
|
||||
"""
|
||||
|
||||
return await courses_rbac_check(
|
||||
request, course_uuid, current_user, action, db_session, require_course_ownership=True
|
||||
)
|
||||
|
||||
|
||||
async def courses_rbac_check_for_certifications(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
) -> bool:
|
||||
"""
|
||||
Specialized RBAC check for certifications that requires course ownership for non-read actions.
|
||||
|
||||
SECURITY NOTES:
|
||||
- Certifications are course credentials and require strict ownership controls
|
||||
- READ: Allow if user has read access to the course
|
||||
- CREATE/UPDATE/DELETE: Require course ownership (CREATOR, MAINTAINER, CONTRIBUTOR) or admin/maintainer role
|
||||
- This prevents unauthorized users from creating/modifying course certifications
|
||||
- CRITICAL: Without this check, users could create certifications for courses they don't own
|
||||
- Instructors can create courses but cannot create certifications in courses they don't own
|
||||
"""
|
||||
|
||||
return await courses_rbac_check(
|
||||
request, course_uuid, current_user, action, db_session, require_course_ownership=True
|
||||
)
|
||||
|
||||
|
||||
async def courses_rbac_check_for_collections(
|
||||
request: Request,
|
||||
collection_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
) -> bool:
|
||||
"""
|
||||
Specialized RBAC check for collections.
|
||||
|
||||
SECURITY NOTES:
|
||||
- Collections are course groupings and require appropriate access controls
|
||||
- READ: Allow if collection is public or user has read access
|
||||
- CREATE/UPDATE/DELETE: Require appropriate permissions based on collection ownership
|
||||
- Collections may have different ownership models than courses
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
collection_uuid: UUID of the collection
|
||||
current_user: Current user (PublicUser or AnonymousUser)
|
||||
action: Action to perform (create, read, update, delete)
|
||||
db_session: Database session
|
||||
|
||||
Returns:
|
||||
bool: True if authorized, raises HTTPException otherwise
|
||||
"""
|
||||
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
res = await authorization_verify_if_element_is_public(
|
||||
request, collection_uuid, action, db_session
|
||||
)
|
||||
if res == False:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights : You are not allowed to read this collection",
|
||||
)
|
||||
return res
|
||||
else:
|
||||
return await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, collection_uuid, db_session
|
||||
)
|
||||
else:
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
return await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
collection_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
|
@ -6,15 +6,19 @@ from src.security.rbac.rbac import (
|
|||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
authorization_verify_based_on_org_admin_status,
|
||||
authorization_verify_based_on_roles,
|
||||
)
|
||||
from src.db.courses.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate
|
||||
from src.db.courses.chapter_activities import ChapterActivity
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from fastapi import HTTPException, Request
|
||||
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum, ResourceAuthorshipStatusEnum
|
||||
from fastapi import HTTPException, Request, status
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
|
||||
from src.services.payments.payments_access import check_activity_paid_access
|
||||
from src.security.courses_security import courses_rbac_check_for_activities
|
||||
|
||||
|
||||
####################################################
|
||||
|
|
@ -49,7 +53,7 @@ async def create_activity(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
await rbac_check(request, course.course_uuid, current_user, "create", db_session)
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# Create Activity
|
||||
activity = Activity(**activity_object.model_dump())
|
||||
|
|
@ -118,7 +122,7 @@ async def get_activity(
|
|||
activity, course = result
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Paid access check
|
||||
has_paid_access = await check_activity_paid_access(
|
||||
|
|
@ -156,7 +160,7 @@ async def get_activityby_id(
|
|||
activity, course = result
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
return ActivityRead.model_validate(activity)
|
||||
|
||||
|
|
@ -187,7 +191,7 @@ async def update_activity(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(activity_object).items():
|
||||
|
|
@ -228,7 +232,7 @@ async def delete_activity(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Delete activity from chapter
|
||||
statement = select(ChapterActivity).where(
|
||||
|
|
@ -296,46 +300,8 @@ async def get_activities(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
activities = [ActivityRead.model_validate(activity) for activity in activities]
|
||||
|
||||
return activities
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
||||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
element_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
):
|
||||
|
||||
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
res = await authorization_verify_if_element_is_public(
|
||||
request, element_uuid, action, db_session
|
||||
)
|
||||
return res
|
||||
else:
|
||||
res = await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, element_uuid, db_session
|
||||
)
|
||||
return res
|
||||
else:
|
||||
# For non-read actions, proceed with regular RBAC checks
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
element_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ from src.services.courses.activities.uploads.tasks_ref_files import (
|
|||
)
|
||||
from src.services.trail.trail import check_trail_presence
|
||||
from src.services.courses.certifications import check_course_completion_and_create_certificate
|
||||
from src.security.courses_security import courses_rbac_check_for_assignments
|
||||
|
||||
## > Assignments CRUD
|
||||
|
||||
|
|
@ -66,7 +67,7 @@ async def create_assignment(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "create", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# Usage check
|
||||
check_limits_with_usage("assignments", course.org_id, db_session)
|
||||
|
|
@ -118,7 +119,7 @@ async def read_assignment(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment read
|
||||
return AssignmentRead.model_validate(assignment)
|
||||
|
|
@ -161,7 +162,7 @@ async def read_assignment_from_activity_uuid(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment read
|
||||
return AssignmentRead.model_validate(assignment)
|
||||
|
|
@ -195,7 +196,7 @@ async def update_assignment(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(assignment_object).items():
|
||||
|
|
@ -239,7 +240,7 @@ async def delete_assignment(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Feature usage
|
||||
decrease_feature_usage("assignments", course.org_id, db_session)
|
||||
|
|
@ -289,7 +290,7 @@ async def delete_assignment_from_activity_uuid(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Feature usage
|
||||
decrease_feature_usage("assignments", course.org_id, db_session)
|
||||
|
|
@ -333,7 +334,7 @@ async def create_assignment_task(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "create", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# Create Assignment Task
|
||||
assignment_task = AssignmentTask(**assignment_task_object.model_dump())
|
||||
|
|
@ -388,7 +389,7 @@ async def read_assignment_tasks(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment tasks read
|
||||
return [
|
||||
|
|
@ -436,7 +437,7 @@ async def read_assignment_task(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment task read
|
||||
return AssignmentTaskRead.model_validate(assignmenttask)
|
||||
|
|
@ -490,7 +491,7 @@ async def put_assignment_task_reference_file(
|
|||
org = db_session.exec(org_statement).first()
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Upload reference file
|
||||
if reference_file and reference_file.filename and activity and org:
|
||||
|
|
@ -568,7 +569,7 @@ async def put_assignment_task_submission_file(
|
|||
org = db_session.exec(org_statement).first()
|
||||
|
||||
# RBAC check - only need read permission to submit files
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Check if user is enrolled in the course
|
||||
if not await authorization_verify_based_on_roles(request, current_user.id, "read", course.course_uuid, db_session):
|
||||
|
|
@ -633,7 +634,7 @@ async def update_assignment_task(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(assignment_task_object).items():
|
||||
|
|
@ -689,7 +690,7 @@ async def delete_assignment_task(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Delete Assignment Task
|
||||
db_session.delete(assignment_task)
|
||||
|
|
@ -741,7 +742,7 @@ async def handle_assignment_task_submission(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
# Check if user has instructor/admin permissions
|
||||
# SECURITY: Check if user has instructor/admin permissions for grading
|
||||
is_instructor = await authorization_verify_based_on_roles(request, current_user.id, "update", course.course_uuid, db_session)
|
||||
|
||||
# For regular users, ensure they can only submit their own work
|
||||
|
|
@ -753,7 +754,7 @@ async def handle_assignment_task_submission(
|
|||
detail="You must be enrolled in this course to submit assignments"
|
||||
)
|
||||
|
||||
# Regular users cannot update grades - only check if actual values are being set
|
||||
# SECURITY: Regular users cannot update grades - only check if actual values are being set
|
||||
if (assignment_task_submission_object.grade is not None and assignment_task_submission_object.grade != 0) or \
|
||||
(assignment_task_submission_object.task_submission_grade_feedback is not None and assignment_task_submission_object.task_submission_grade_feedback != ""):
|
||||
raise HTTPException(
|
||||
|
|
@ -762,10 +763,10 @@ async def handle_assignment_task_submission(
|
|||
)
|
||||
|
||||
# Only need read permission for submissions
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
else:
|
||||
# Instructors/admins need update permission to grade
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
# SECURITY: Instructors/admins need update permission to grade
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Try to find existing submission if UUID is provided
|
||||
assignment_task_submission = None
|
||||
|
|
@ -777,7 +778,7 @@ async def handle_assignment_task_submission(
|
|||
|
||||
# If submission exists, update it
|
||||
if assignment_task_submission:
|
||||
# For regular users, ensure they can only update their own submissions
|
||||
# SECURITY: For regular users, ensure they can only update their own submissions
|
||||
if not is_instructor and assignment_task_submission.user_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
|
|
@ -880,7 +881,7 @@ async def read_user_assignment_task_submissions(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment task submission read
|
||||
return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission)
|
||||
|
|
@ -953,7 +954,7 @@ async def read_assignment_task_submissions(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment task submission read
|
||||
return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission)
|
||||
|
|
@ -1012,7 +1013,7 @@ async def update_assignment_task_submission(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(assignment_task_submission_object).items():
|
||||
|
|
@ -1081,7 +1082,7 @@ async def delete_assignment_task_submission(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Delete Assignment Task Submission
|
||||
db_session.delete(assignment_task_submission)
|
||||
|
|
@ -1147,7 +1148,7 @@ async def create_assignment_submission(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Create Assignment User Submission
|
||||
assignment_user_submission = AssignmentUserSubmission(
|
||||
|
|
@ -1280,7 +1281,7 @@ async def read_assignment_submissions(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment tasks read
|
||||
return [
|
||||
|
|
@ -1323,7 +1324,7 @@ async def read_user_assignment_submissions(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment tasks read
|
||||
return [
|
||||
|
|
@ -1389,7 +1390,7 @@ async def update_assignment_submission(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(assignment_user_submission_object).items():
|
||||
|
|
@ -1447,7 +1448,7 @@ async def delete_assignment_submission(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Delete Assignment User Submission
|
||||
db_session.delete(assignment_user_submission)
|
||||
|
|
@ -1464,7 +1465,7 @@ async def grade_assignment_submission(
|
|||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
|
||||
# SECURITY: This function should only be accessible by course owners or instructors
|
||||
# Check if assignment exists
|
||||
statement = select(Assignment).where(Assignment.assignment_uuid == assignment_uuid)
|
||||
assignment = db_session.exec(statement).first()
|
||||
|
|
@ -1484,7 +1485,8 @@ async def grade_assignment_submission(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
# SECURITY: Require course ownership or instructor role for grading
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Check if assignment user submission exists
|
||||
statement = select(AssignmentUserSubmission).where(
|
||||
|
|
@ -1602,6 +1604,7 @@ async def mark_activity_as_done_for_user(
|
|||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
# SECURITY: This function should only be accessible by course owners or instructors
|
||||
# Get Assignment
|
||||
statement = select(Assignment).where(Assignment.assignment_uuid == assignment_uuid)
|
||||
assignment = db_session.exec(statement).first()
|
||||
|
|
@ -1625,7 +1628,8 @@ async def mark_activity_as_done_for_user(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
# SECURITY: Require course ownership or instructor role for marking activities as done
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
if not activity:
|
||||
raise HTTPException(
|
||||
|
|
@ -1704,46 +1708,7 @@ async def get_assignments_from_course(
|
|||
assignments.append(assignment)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignments read
|
||||
return [AssignmentRead.model_validate(assignment) for assignment in assignments]
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
||||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
):
|
||||
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
res = await authorization_verify_if_element_is_public(
|
||||
request, course_uuid, action, db_session
|
||||
)
|
||||
return res
|
||||
else:
|
||||
res = (
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, course_uuid, db_session
|
||||
)
|
||||
)
|
||||
return res
|
||||
else:
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
course_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from src.services.courses.activities.uploads.pdfs import upload_pdf
|
|||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from src.security.courses_security import courses_rbac_check_for_activities
|
||||
|
||||
|
||||
async def create_documentpdf_activity(
|
||||
|
|
@ -30,9 +31,6 @@ async def create_documentpdf_activity(
|
|||
db_session: Session,
|
||||
pdf_file: UploadFile | None = None,
|
||||
):
|
||||
# RBAC check
|
||||
await rbac_check(request, "course_uuid", current_user, "create", db_session)
|
||||
|
||||
# get chapter_id
|
||||
statement = select(Chapter).where(Chapter.id == chapter_id)
|
||||
chapter = db_session.exec(statement).first()
|
||||
|
|
@ -52,6 +50,19 @@ async def create_documentpdf_activity(
|
|||
detail="CourseChapter not found",
|
||||
)
|
||||
|
||||
# Get course_uuid for RBAC check
|
||||
statement = select(Course).where(Course.id == coursechapter.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# get org_id
|
||||
org_id = coursechapter.org_id
|
||||
|
||||
|
|
@ -59,10 +70,6 @@ async def create_documentpdf_activity(
|
|||
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()}"
|
||||
|
||||
|
|
@ -119,7 +126,7 @@ async def create_documentpdf_activity(
|
|||
)
|
||||
|
||||
# upload pdf
|
||||
if pdf_file:
|
||||
if pdf_file and organization and course:
|
||||
# get pdffile format
|
||||
await upload_pdf(
|
||||
pdf_file,
|
||||
|
|
@ -134,27 +141,3 @@ async def create_documentpdf_activity(
|
|||
db_session.refresh(activity_chapter)
|
||||
|
||||
return ActivityRead.model_validate(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,
|
||||
)
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from src.services.courses.activities.uploads.videos import upload_video
|
|||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from src.security.courses_security import courses_rbac_check_for_activities
|
||||
|
||||
|
||||
async def create_video_activity(
|
||||
|
|
@ -34,9 +35,6 @@ async def create_video_activity(
|
|||
video_file: UploadFile | None = None,
|
||||
details: str = "{}",
|
||||
):
|
||||
# RBAC check
|
||||
await rbac_check(request, "activity_x", current_user, "create", db_session)
|
||||
|
||||
# get chapter_id
|
||||
statement = select(Chapter).where(Chapter.id == chapter_id)
|
||||
chapter = db_session.exec(statement).first()
|
||||
|
|
@ -59,14 +57,23 @@ async def create_video_activity(
|
|||
detail="CourseChapter not found",
|
||||
)
|
||||
|
||||
# Get course_uuid for RBAC check
|
||||
statement = select(Course).where(Course.id == coursechapter.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# 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()}")
|
||||
|
||||
|
|
@ -103,7 +110,7 @@ async def create_video_activity(
|
|||
"filename": "video." + video_format,
|
||||
"activity_uuid": activity_uuid,
|
||||
},
|
||||
details=details,
|
||||
details=details if isinstance(details, dict) else json.loads(details),
|
||||
creation_date=str(datetime.now()),
|
||||
update_date=str(datetime.now()),
|
||||
)
|
||||
|
|
@ -115,7 +122,7 @@ async def create_video_activity(
|
|||
db_session.refresh(activity)
|
||||
|
||||
# upload video
|
||||
if video_file:
|
||||
if video_file and organization and course:
|
||||
# get videofile format
|
||||
await upload_video(
|
||||
video_file,
|
||||
|
|
@ -161,9 +168,6 @@ async def create_external_video_activity(
|
|||
data: ExternalVideo,
|
||||
db_session: Session,
|
||||
):
|
||||
# RBAC check
|
||||
await rbac_check(request, "activity_x", current_user, "create", db_session)
|
||||
|
||||
# get chapter_id
|
||||
statement = select(Chapter).where(Chapter.id == data.chapter_id)
|
||||
chapter = db_session.exec(statement).first()
|
||||
|
|
@ -183,6 +187,19 @@ async def create_external_video_activity(
|
|||
detail="CourseChapter not found",
|
||||
)
|
||||
|
||||
# Get course_uuid for RBAC check
|
||||
statement = select(Course).where(Course.id == coursechapter.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await courses_rbac_check_for_activities(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# generate activity_uuid
|
||||
activity_uuid = str(f"activity_{uuid4()}")
|
||||
|
||||
|
|
@ -230,22 +247,4 @@ async def create_external_video_activity(
|
|||
return ActivityRead.model_validate(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 ##
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from src.security.rbac.rbac import (
|
|||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.security.courses_security import courses_rbac_check_for_certifications
|
||||
|
||||
|
||||
####################################################
|
||||
|
|
@ -46,7 +47,7 @@ async def create_certification(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "create", db_session)
|
||||
await courses_rbac_check_for_certifications(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# Create certification
|
||||
certification = Certifications(
|
||||
|
|
@ -93,7 +94,7 @@ async def get_certification(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_certifications(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
return CertificationRead(**certification.model_dump())
|
||||
|
||||
|
|
@ -117,7 +118,7 @@ async def get_certifications_by_course(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_certifications(request, course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Get certifications for this course
|
||||
statement = select(Certifications).where(Certifications.course_id == course.id)
|
||||
|
|
@ -155,7 +156,7 @@ async def update_certification(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check_for_certifications(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(certification_object).items():
|
||||
|
|
@ -200,7 +201,7 @@ async def delete_certification(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check_for_certifications(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
db_session.delete(certification)
|
||||
db_session.commit()
|
||||
|
|
@ -218,8 +219,16 @@ async def create_certificate_user(
|
|||
user_id: int,
|
||||
certification_id: int,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser | None = None,
|
||||
) -> CertificateUserRead:
|
||||
"""Create a certificate user link"""
|
||||
"""
|
||||
Create a certificate user link
|
||||
|
||||
SECURITY NOTES:
|
||||
- This function should only be called by authorized users (course owners, instructors, or system)
|
||||
- When called from check_course_completion_and_create_certificate, it's a system operation
|
||||
- When called directly, requires proper RBAC checks
|
||||
"""
|
||||
|
||||
# Check if certification exists
|
||||
statement = select(Certifications).where(Certifications.id == certification_id)
|
||||
|
|
@ -231,6 +240,21 @@ async def create_certificate_user(
|
|||
detail="Certification not found",
|
||||
)
|
||||
|
||||
# SECURITY: If current_user is provided, perform RBAC check
|
||||
if current_user:
|
||||
# Get course for RBAC check
|
||||
statement = select(Course).where(Course.id == certification.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# Require course ownership or instructor role for creating certificates
|
||||
await courses_rbac_check_for_certifications(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# Check if certificate user already exists
|
||||
statement = select(CertificateUser).where(
|
||||
CertificateUser.user_id == user_id,
|
||||
|
|
@ -316,7 +340,7 @@ async def get_user_certificates_for_course(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_certifications(request, course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Get all certifications for this course
|
||||
statement = select(Certifications).where(Certifications.course_id == course.id)
|
||||
|
|
@ -357,7 +381,14 @@ async def check_course_completion_and_create_certificate(
|
|||
course_id: int,
|
||||
db_session: Session,
|
||||
) -> bool:
|
||||
"""Check if all activities in a course are completed and create certificate if so"""
|
||||
"""
|
||||
Check if all activities in a course are completed and create certificate if so
|
||||
|
||||
SECURITY NOTES:
|
||||
- This function is called by the system when activities are completed
|
||||
- It should only create certificates for users who have actually completed the course
|
||||
- The function is called from mark_activity_as_done_for_user which already has RBAC checks
|
||||
"""
|
||||
|
||||
# Get all activities in the course
|
||||
statement = select(ChapterActivity).where(ChapterActivity.course_id == course_id)
|
||||
|
|
@ -381,7 +412,8 @@ async def check_course_completion_and_create_certificate(
|
|||
certification = db_session.exec(statement).first()
|
||||
|
||||
if certification and certification.id:
|
||||
# Create certificate user link
|
||||
# SECURITY: Create certificate user link (system operation, no RBAC needed here)
|
||||
# This is called from mark_activity_as_done_for_user which already has proper RBAC checks
|
||||
try:
|
||||
await create_certificate_user(request, user_id, certification.id, db_session)
|
||||
return True
|
||||
|
|
@ -506,36 +538,3 @@ async def get_all_user_certificates(
|
|||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
####################################################
|
||||
# RBAC Utils
|
||||
####################################################
|
||||
|
||||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
):
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
await authorization_verify_if_element_is_public(
|
||||
request, course_uuid, action, db_session
|
||||
)
|
||||
else:
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, course_uuid, db_session
|
||||
)
|
||||
else:
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
course_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
|
@ -2,7 +2,7 @@ from datetime import datetime
|
|||
from typing import List, Literal
|
||||
from uuid import uuid4
|
||||
from sqlmodel import Session, select
|
||||
from src.db.users import AnonymousUser
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
|
|
@ -18,9 +18,9 @@ from src.db.courses.chapters import (
|
|||
ChapterUpdate,
|
||||
ChapterUpdateOrder,
|
||||
)
|
||||
from src.services.courses.courses import Course
|
||||
from src.services.users.users import PublicUser
|
||||
from src.db.courses.courses import Course
|
||||
from fastapi import HTTPException, status, Request
|
||||
from src.security.courses_security import courses_rbac_check_for_chapters
|
||||
|
||||
|
||||
####################################################
|
||||
|
|
@ -42,7 +42,7 @@ async def create_chapter(
|
|||
course = db_session.exec(statement).one()
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, "chapter_x", current_user, "create", db_session)
|
||||
await courses_rbac_check_for_chapters(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# complete chapter object
|
||||
chapter.course_id = chapter_object.course_id
|
||||
|
|
@ -55,7 +55,7 @@ async def create_chapter(
|
|||
statement = (
|
||||
select(CourseChapter)
|
||||
.where(CourseChapter.course_id == chapter.course_id)
|
||||
.order_by(CourseChapter.order)
|
||||
.order_by(CourseChapter.order) # type: ignore
|
||||
)
|
||||
course_chapters = db_session.exec(statement).all()
|
||||
|
||||
|
|
@ -122,14 +122,14 @@ async def get_chapter(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_chapters(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Get activities for this chapter
|
||||
statement = (
|
||||
select(Activity)
|
||||
.join(ChapterActivity, Activity.id == ChapterActivity.activity_id)
|
||||
.join(ChapterActivity, Activity.id == ChapterActivity.activity_id) # type: ignore
|
||||
.where(ChapterActivity.chapter_id == chapter_id)
|
||||
.distinct(Activity.id)
|
||||
.distinct(Activity.id) # type: ignore
|
||||
)
|
||||
|
||||
activities = db_session.exec(statement).all()
|
||||
|
|
@ -158,7 +158,7 @@ async def update_chapter(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, chapter.chapter_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check_for_chapters(request, chapter.chapter_uuid, current_user, "update", db_session)
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(chapter_object).items():
|
||||
|
|
@ -193,7 +193,7 @@ async def delete_chapter(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, chapter.chapter_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check_for_chapters(request, chapter.chapter_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Remove all linked chapter activities
|
||||
statement = select(ChapterActivity).where(ChapterActivity.chapter_id == chapter.id)
|
||||
|
|
@ -224,26 +224,26 @@ async def get_course_chapters(
|
|||
|
||||
statement = (
|
||||
select(Chapter)
|
||||
.join(CourseChapter, Chapter.id == CourseChapter.chapter_id)
|
||||
.join(CourseChapter, Chapter.id == CourseChapter.chapter_id) # type: ignore
|
||||
.where(CourseChapter.course_id == course_id)
|
||||
.where(Chapter.course_id == course_id)
|
||||
.order_by(CourseChapter.order)
|
||||
.group_by(Chapter.id, CourseChapter.order)
|
||||
.order_by(CourseChapter.order) # type: ignore
|
||||
.group_by(Chapter.id, CourseChapter.order) # type: ignore
|
||||
)
|
||||
chapters = db_session.exec(statement).all()
|
||||
|
||||
chapters = [ChapterRead(**chapter.model_dump(), activities=[]) for chapter in chapters]
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session) # type: ignore
|
||||
await courses_rbac_check_for_chapters(request, course.course_uuid, current_user, "read", db_session) # type: ignore
|
||||
|
||||
# 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)
|
||||
.order_by(ChapterActivity.order) # type: ignore
|
||||
.distinct(ChapterActivity.id, ChapterActivity.order) # type: ignore
|
||||
)
|
||||
chapter_activities = db_session.exec(statement).all()
|
||||
|
||||
|
|
@ -251,7 +251,7 @@ async def get_course_chapters(
|
|||
statement = (
|
||||
select(Activity)
|
||||
.where(Activity.id == chapter_activity.activity_id, with_unpublished_activities or Activity.published == True)
|
||||
.distinct(Activity.id)
|
||||
.distinct(Activity.id) # type: ignore
|
||||
)
|
||||
activity = db_session.exec(statement).first()
|
||||
|
||||
|
|
@ -279,7 +279,7 @@ async def DEPRECEATED_get_course_chapters(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check_for_chapters(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
chapters_in_db = await get_course_chapters(request, course.id, db_session, current_user) # type: ignore
|
||||
|
||||
|
|
@ -306,9 +306,9 @@ async def DEPRECEATED_get_course_chapters(
|
|||
activities_list = {}
|
||||
statement = (
|
||||
select(Activity)
|
||||
.join(ChapterActivity, ChapterActivity.activity_id == Activity.id)
|
||||
.join(ChapterActivity, ChapterActivity.activity_id == Activity.id) # type: ignore
|
||||
.where(ChapterActivity.activity_id == Activity.id)
|
||||
.group_by(Activity.id)
|
||||
.group_by(Activity.id) # type: ignore
|
||||
)
|
||||
activities_in_db = db_session.exec(statement).all()
|
||||
|
||||
|
|
@ -324,10 +324,10 @@ async def DEPRECEATED_get_course_chapters(
|
|||
# get chapter order
|
||||
statement = (
|
||||
select(Chapter)
|
||||
.join(CourseChapter, CourseChapter.chapter_id == Chapter.id)
|
||||
.join(CourseChapter, CourseChapter.chapter_id == Chapter.id) # type: ignore
|
||||
.where(CourseChapter.chapter_id == Chapter.id)
|
||||
.group_by(Chapter.id, CourseChapter.order)
|
||||
.order_by(CourseChapter.order)
|
||||
.group_by(Chapter.id, CourseChapter.order) # type: ignore
|
||||
.order_by(CourseChapter.order) # type: ignore
|
||||
)
|
||||
chapters_in_db = db_session.exec(statement).all()
|
||||
|
||||
|
|
@ -361,7 +361,7 @@ async def reorder_chapters_and_activities(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check_for_chapters(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
###########
|
||||
# Chapters
|
||||
|
|
@ -458,39 +458,3 @@ async def reorder_chapters_and_activities(
|
|||
db_session.commit()
|
||||
|
||||
return {"detail": "Chapters and activities reordered successfully"}
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
||||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
):
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
res = await authorization_verify_if_element_is_public(
|
||||
request, course_uuid, action, db_session
|
||||
)
|
||||
return res
|
||||
else:
|
||||
res = await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, course_uuid, db_session
|
||||
)
|
||||
return res
|
||||
else:
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
course_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from datetime import datetime
|
|||
from typing import List, Literal
|
||||
from uuid import uuid4
|
||||
from sqlmodel import Session, select
|
||||
from src.db.users import AnonymousUser
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
|
|
@ -16,8 +16,8 @@ from src.db.collections import (
|
|||
)
|
||||
from src.db.collections_courses import CollectionCourse
|
||||
from src.db.courses.courses import Course
|
||||
from src.services.users.users import PublicUser
|
||||
from fastapi import HTTPException, status, Request
|
||||
from src.security.courses_security import courses_rbac_check_for_collections
|
||||
|
||||
|
||||
####################################################
|
||||
|
|
@ -40,7 +40,7 @@ async def get_collection(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(
|
||||
await courses_rbac_check_for_collections(
|
||||
request, collection.collection_uuid, current_user, "read", db_session
|
||||
)
|
||||
|
||||
|
|
@ -86,8 +86,10 @@ async def create_collection(
|
|||
) -> CollectionRead:
|
||||
collection = Collection.model_validate(collection_object)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, "collection_x", current_user, "create", db_session)
|
||||
# SECURITY: Check if user has permission to create collections in this organization
|
||||
# Since collections are organization-level resources, we need to check org permissions
|
||||
# For now, we'll use the existing RBAC check but with proper organization context
|
||||
await courses_rbac_check_for_collections(request, "collection_x", current_user, "create", db_session)
|
||||
|
||||
# Complete the collection object
|
||||
collection.collection_uuid = f"collection_{uuid4()}"
|
||||
|
|
@ -99,9 +101,23 @@ async def create_collection(
|
|||
db_session.commit()
|
||||
db_session.refresh(collection)
|
||||
|
||||
# Link courses to collection
|
||||
# SECURITY: Link courses to collection - ensure user has access to all courses being added
|
||||
if collection:
|
||||
for course_id in collection_object.courses:
|
||||
# Check if user has access to this course
|
||||
statement = select(Course).where(Course.id == course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if course:
|
||||
# Verify user has read access to the course before adding it to collection
|
||||
try:
|
||||
await courses_rbac_check_for_collections(request, course.course_uuid, current_user, "read", db_session)
|
||||
except HTTPException:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"You don't have permission to add course {course.name} to this collection"
|
||||
)
|
||||
|
||||
collection_course = CollectionCourse(
|
||||
collection_id=int(collection.id), # type: ignore
|
||||
course_id=course_id,
|
||||
|
|
@ -145,7 +161,7 @@ async def update_collection(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(
|
||||
await courses_rbac_check_for_collections(
|
||||
request, collection.collection_uuid, current_user, "update", db_session
|
||||
)
|
||||
|
||||
|
|
@ -219,7 +235,7 @@ async def delete_collection(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(
|
||||
await courses_rbac_check_for_collections(
|
||||
request, collection.collection_uuid, current_user, "delete", db_session
|
||||
)
|
||||
|
||||
|
|
@ -248,7 +264,7 @@ async def get_collections(
|
|||
Collection.org_id == org_id, Collection.public == True
|
||||
)
|
||||
statement_all = (
|
||||
select(Collection).where(Collection.org_id == org_id).distinct(Collection.id)
|
||||
select(Collection).where(Collection.org_id == org_id).distinct(Collection.id) # type: ignore
|
||||
)
|
||||
|
||||
if current_user.id == 0:
|
||||
|
|
@ -288,49 +304,7 @@ async def get_collections(
|
|||
|
||||
courses = db_session.exec(statement).all()
|
||||
|
||||
collection = CollectionRead(**collection.model_dump(), courses=courses)
|
||||
collection = CollectionRead(**collection.model_dump(), courses=list(courses))
|
||||
collections_with_courses.append(collection)
|
||||
|
||||
return collections_with_courses
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
||||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
collection_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
):
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
res = await authorization_verify_if_element_is_public(
|
||||
request, collection_uuid, action, db_session
|
||||
)
|
||||
if res == False:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights : You are not allowed to read this collection",
|
||||
)
|
||||
else:
|
||||
res = (
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, collection_uuid, db_session
|
||||
)
|
||||
)
|
||||
return res
|
||||
else:
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
collection_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ from sqlmodel import Session, select, and_
|
|||
from src.db.users import PublicUser, AnonymousUser, User, UserRead
|
||||
from src.db.courses.courses import Course
|
||||
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum, ResourceAuthorshipStatusEnum
|
||||
from src.security.rbac.rbac import authorization_verify_if_user_is_anon, authorization_verify_based_on_roles_and_authorship
|
||||
from src.security.rbac.rbac import authorization_verify_if_user_is_anon, authorization_verify_based_on_org_admin_status
|
||||
from src.security.courses_security import courses_rbac_check
|
||||
from typing import List
|
||||
|
||||
|
||||
|
|
@ -14,6 +15,14 @@ async def apply_course_contributor(
|
|||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
"""
|
||||
Apply to become a course contributor
|
||||
|
||||
SECURITY NOTES:
|
||||
- Any authenticated user can apply to become a contributor
|
||||
- Applications are created with PENDING status
|
||||
- Only course owners (CREATOR, MAINTAINER) or admins can approve applications
|
||||
"""
|
||||
# Verify user is not anonymous
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
|
|
@ -73,21 +82,17 @@ async def update_course_contributor(
|
|||
):
|
||||
"""
|
||||
Update a course contributor's role and status
|
||||
Only administrators can perform this action
|
||||
|
||||
SECURITY NOTES:
|
||||
- Only course owners (CREATOR, MAINTAINER) or admins can update contributors
|
||||
- Cannot modify the role of the course creator
|
||||
- Requires strict course ownership checks
|
||||
"""
|
||||
# Verify user is not anonymous
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
# RBAC check - verify if user has admin rights
|
||||
authorized = await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, "update", course_uuid, db_session
|
||||
)
|
||||
|
||||
if not authorized:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You are not authorized to update course contributors",
|
||||
)
|
||||
# SECURITY: Require course ownership or admin role for updating contributors
|
||||
await courses_rbac_check(request, course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Check if course exists
|
||||
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||
|
|
@ -115,7 +120,7 @@ async def update_course_contributor(
|
|||
detail="Contributor not found for this course",
|
||||
)
|
||||
|
||||
# Don't allow changing the role of the creator
|
||||
# SECURITY: Don't allow changing the role of the creator
|
||||
if existing_authorship.authorship == ResourceAuthorshipEnum.CREATOR:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
|
|
@ -144,6 +149,10 @@ async def get_course_contributors(
|
|||
) -> List[dict]:
|
||||
"""
|
||||
Get all contributors for a course with their user information
|
||||
|
||||
SECURITY NOTES:
|
||||
- Requires read access to the course
|
||||
- Contributors are visible to anyone with course read access
|
||||
"""
|
||||
# Check if course exists
|
||||
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||
|
|
@ -155,6 +164,9 @@ async def get_course_contributors(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
# SECURITY: Require read access to the course
|
||||
await courses_rbac_check(request, course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Get all contributors for this course with user information
|
||||
statement = (
|
||||
select(ResourceAuthor, User)
|
||||
|
|
@ -184,21 +196,17 @@ async def add_bulk_course_contributors(
|
|||
):
|
||||
"""
|
||||
Add multiple contributors to a course by their usernames
|
||||
Only administrators can perform this action
|
||||
|
||||
SECURITY NOTES:
|
||||
- Only course owners (CREATOR, MAINTAINER) or admins can add contributors
|
||||
- Requires strict course ownership checks
|
||||
- Cannot add contributors to courses the user doesn't own
|
||||
"""
|
||||
# Verify user is not anonymous
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
# RBAC check - verify if user has admin rights
|
||||
authorized = await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, "update", course_uuid, db_session
|
||||
)
|
||||
|
||||
if not authorized:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You are not authorized to add contributors",
|
||||
)
|
||||
# SECURITY: Require course ownership or admin role for adding contributors
|
||||
await courses_rbac_check(request, course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Check if course exists
|
||||
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||
|
|
@ -284,21 +292,18 @@ async def remove_bulk_course_contributors(
|
|||
):
|
||||
"""
|
||||
Remove multiple contributors from a course by their usernames
|
||||
Only administrators can perform this action
|
||||
|
||||
SECURITY NOTES:
|
||||
- Only course owners (CREATOR, MAINTAINER) or admins can remove contributors
|
||||
- Requires strict course ownership checks
|
||||
- Cannot remove contributors from courses the user doesn't own
|
||||
- Cannot remove the course creator
|
||||
"""
|
||||
# Verify user is not anonymous
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
# RBAC check - verify if user has admin rights
|
||||
authorized = await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, "update", course_uuid, db_session
|
||||
)
|
||||
|
||||
if not authorized:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You are not authorized to remove contributors",
|
||||
)
|
||||
# SECURITY: Require course ownership or admin role for removing contributors
|
||||
await courses_rbac_check(request, course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Check if course exists
|
||||
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||
|
|
@ -346,7 +351,7 @@ async def remove_bulk_course_contributors(
|
|||
})
|
||||
continue
|
||||
|
||||
# Don't allow removing the creator
|
||||
# SECURITY: Don't allow removing the creator
|
||||
if existing_authorship.authorship == ResourceAuthorshipEnum.CREATOR:
|
||||
results["failed"].append({
|
||||
"username": username,
|
||||
|
|
|
|||
|
|
@ -24,10 +24,12 @@ from src.security.rbac.rbac import (
|
|||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
authorization_verify_based_on_org_admin_status,
|
||||
)
|
||||
from src.services.courses.thumbnails import upload_thumbnail
|
||||
from fastapi import HTTPException, Request, UploadFile
|
||||
from fastapi import HTTPException, Request, UploadFile, status
|
||||
from datetime import datetime
|
||||
from src.security.courses_security import courses_rbac_check
|
||||
|
||||
|
||||
async def get_course(
|
||||
|
|
@ -46,15 +48,15 @@ async def get_course(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Get course authors with their roles
|
||||
authors_statement = (
|
||||
select(ResourceAuthor, User)
|
||||
.join(User, ResourceAuthor.user_id == User.id)
|
||||
.join(User, ResourceAuthor.user_id == User.id) # type: ignore
|
||||
.where(ResourceAuthor.resource_uuid == course.course_uuid)
|
||||
.order_by(
|
||||
ResourceAuthor.id.asc()
|
||||
ResourceAuthor.id.asc() # type: ignore
|
||||
)
|
||||
)
|
||||
author_results = db_session.exec(authors_statement).all()
|
||||
|
|
@ -92,15 +94,15 @@ async def get_course_by_id(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Get course authors with their roles
|
||||
authors_statement = (
|
||||
select(ResourceAuthor, User)
|
||||
.join(User, ResourceAuthor.user_id == User.id)
|
||||
.join(User, ResourceAuthor.user_id == User.id) # type: ignore
|
||||
.where(ResourceAuthor.resource_uuid == course.course_uuid)
|
||||
.order_by(
|
||||
ResourceAuthor.id.asc()
|
||||
ResourceAuthor.id.asc() # type: ignore
|
||||
)
|
||||
)
|
||||
author_results = db_session.exec(authors_statement).all()
|
||||
|
|
@ -153,7 +155,7 @@ async def get_course_meta(
|
|||
author_results = [(ra, u) for _, ra, u in results if ra is not None and u is not None]
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Get course chapters
|
||||
chapters = []
|
||||
|
|
@ -241,7 +243,7 @@ async def get_courses_orgslug(
|
|||
.join(User, ResourceAuthor.user_id == User.id) # type: ignore
|
||||
.where(ResourceAuthor.resource_uuid.in_(course_uuids)) # type: ignore
|
||||
.order_by(
|
||||
ResourceAuthor.id.asc()
|
||||
ResourceAuthor.id.asc() # type: ignore
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -349,10 +351,10 @@ async def search_courses(
|
|||
# Get course authors with their roles
|
||||
authors_statement = (
|
||||
select(ResourceAuthor, User)
|
||||
.join(User, ResourceAuthor.user_id == User.id)
|
||||
.join(User, ResourceAuthor.user_id == User.id) # type: ignore
|
||||
.where(ResourceAuthor.resource_uuid == course.course_uuid)
|
||||
.order_by(
|
||||
ResourceAuthor.id.asc()
|
||||
ResourceAuthor.id.asc() # type: ignore
|
||||
)
|
||||
)
|
||||
author_results = db_session.exec(authors_statement).all()
|
||||
|
|
@ -399,10 +401,20 @@ async def create_course(
|
|||
thumbnail_file: UploadFile | None = None,
|
||||
thumbnail_type: ThumbnailType = ThumbnailType.IMAGE,
|
||||
):
|
||||
"""
|
||||
Create a new course
|
||||
|
||||
SECURITY NOTES:
|
||||
- Requires proper permissions to create courses in the organization
|
||||
- User becomes the CREATOR of the course automatically
|
||||
- Course creation is subject to organization limits and permissions
|
||||
"""
|
||||
course = Course.model_validate(course_object)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, "course_x", current_user, "create", db_session)
|
||||
# SECURITY: Check if user has permission to create courses in this organization
|
||||
# Since this is a new course, we need to check organization-level permissions
|
||||
# For now, we'll use the existing RBAC check but with proper organization context
|
||||
await courses_rbac_check(request, "course_x", current_user, "create", db_session)
|
||||
|
||||
# Usage check
|
||||
check_limits_with_usage("courses", org_id, db_session)
|
||||
|
|
@ -440,7 +452,7 @@ async def create_course(
|
|||
db_session.commit()
|
||||
db_session.refresh(course)
|
||||
|
||||
# Make the user the creator of the course
|
||||
# SECURITY: Make the user the creator of the course
|
||||
resource_author = ResourceAuthor(
|
||||
resource_uuid=course.course_uuid,
|
||||
user_id=current_user.id,
|
||||
|
|
@ -458,10 +470,10 @@ async def create_course(
|
|||
# Get course authors with their roles
|
||||
authors_statement = (
|
||||
select(ResourceAuthor, User)
|
||||
.join(User, ResourceAuthor.user_id == User.id)
|
||||
.join(User, ResourceAuthor.user_id == User.id) # type: ignore
|
||||
.where(ResourceAuthor.resource_uuid == course.course_uuid)
|
||||
.order_by(
|
||||
ResourceAuthor.id.asc()
|
||||
ResourceAuthor.id.asc() # type: ignore
|
||||
)
|
||||
)
|
||||
author_results = db_session.exec(authors_statement).all()
|
||||
|
|
@ -506,7 +518,7 @@ async def update_course_thumbnail(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Get org uuid
|
||||
org_statement = select(Organization).where(Organization.id == course.org_id)
|
||||
|
|
@ -543,10 +555,10 @@ async def update_course_thumbnail(
|
|||
# Get course authors with their roles
|
||||
authors_statement = (
|
||||
select(ResourceAuthor, User)
|
||||
.join(User, ResourceAuthor.user_id == User.id)
|
||||
.join(User, ResourceAuthor.user_id == User.id) # type: ignore
|
||||
.where(ResourceAuthor.resource_uuid == course.course_uuid)
|
||||
.order_by(
|
||||
ResourceAuthor.id.asc()
|
||||
ResourceAuthor.id.asc() # type: ignore
|
||||
)
|
||||
)
|
||||
author_results = db_session.exec(authors_statement).all()
|
||||
|
|
@ -575,6 +587,14 @@ async def update_course(
|
|||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
"""
|
||||
Update a course
|
||||
|
||||
SECURITY NOTES:
|
||||
- Requires course ownership (CREATOR, MAINTAINER) or admin role
|
||||
- Sensitive fields (public, open_to_contributors) require additional validation
|
||||
- Cannot change course access settings without proper permissions
|
||||
"""
|
||||
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
|
|
@ -584,8 +604,46 @@ async def update_course(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
# SECURITY: Require course ownership or admin role for updating courses
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# SECURITY: Additional checks for sensitive access control fields
|
||||
sensitive_fields_updated = []
|
||||
|
||||
# Check if sensitive fields are being updated
|
||||
if course_object.public is not None:
|
||||
sensitive_fields_updated.append("public")
|
||||
if course_object.open_to_contributors is not None:
|
||||
sensitive_fields_updated.append("open_to_contributors")
|
||||
|
||||
# If sensitive fields are being updated, require additional validation
|
||||
if sensitive_fields_updated:
|
||||
# SECURITY: For sensitive access control changes, require CREATOR or MAINTAINER role
|
||||
# Check if user is course owner (CREATOR or MAINTAINER)
|
||||
statement = select(ResourceAuthor).where(
|
||||
ResourceAuthor.resource_uuid == course_uuid,
|
||||
ResourceAuthor.user_id == current_user.id
|
||||
)
|
||||
resource_author = db_session.exec(statement).first()
|
||||
|
||||
is_course_owner = False
|
||||
if resource_author:
|
||||
if ((resource_author.authorship == ResourceAuthorshipEnum.CREATOR) or
|
||||
(resource_author.authorship == ResourceAuthorshipEnum.MAINTAINER)) and \
|
||||
resource_author.authorship_status == ResourceAuthorshipStatusEnum.ACTIVE:
|
||||
is_course_owner = True
|
||||
|
||||
# Check if user has admin or maintainer role
|
||||
is_admin_or_maintainer = await authorization_verify_based_on_org_admin_status(
|
||||
request, current_user.id, "update", course_uuid, db_session
|
||||
)
|
||||
|
||||
# SECURITY: Only course owners (CREATOR, MAINTAINER) or admins can change access settings
|
||||
if not (is_course_owner or is_admin_or_maintainer):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"You must be the course owner (CREATOR or MAINTAINER) or have admin role to change access settings: {', '.join(sensitive_fields_updated)}",
|
||||
)
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(course_object).items():
|
||||
|
|
@ -602,10 +660,10 @@ async def update_course(
|
|||
# Get course authors with their roles
|
||||
authors_statement = (
|
||||
select(ResourceAuthor, User)
|
||||
.join(User, ResourceAuthor.user_id == User.id)
|
||||
.join(User, ResourceAuthor.user_id == User.id) # type: ignore
|
||||
.where(ResourceAuthor.resource_uuid == course.course_uuid)
|
||||
.order_by(
|
||||
ResourceAuthor.id.asc()
|
||||
ResourceAuthor.id.asc() # type: ignore
|
||||
)
|
||||
)
|
||||
author_results = db_session.exec(authors_statement).all()
|
||||
|
|
@ -643,7 +701,7 @@ async def delete_course(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Feature usage
|
||||
decrease_feature_usage("courses", course.org_id, db_session)
|
||||
|
|
@ -681,7 +739,7 @@ async def get_user_courses(
|
|||
return []
|
||||
|
||||
# Get courses with the extracted UUIDs
|
||||
statement = select(Course).where(Course.course_uuid.in_(course_uuids))
|
||||
statement = select(Course).where(Course.course_uuid.in_(course_uuids)) # type: ignore
|
||||
|
||||
# Apply pagination
|
||||
statement = statement.offset((page - 1) * limit).limit(limit)
|
||||
|
|
@ -738,39 +796,177 @@ async def get_user_courses(
|
|||
return result
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
||||
|
||||
async def rbac_check(
|
||||
async def get_course_user_rights(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
):
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
res = await authorization_verify_if_element_is_public(
|
||||
request, course_uuid, action, db_session
|
||||
)
|
||||
return res
|
||||
else:
|
||||
res = (
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, course_uuid, db_session
|
||||
)
|
||||
)
|
||||
return res
|
||||
else:
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
) -> dict:
|
||||
"""
|
||||
Get detailed user rights for a specific course.
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
course_uuid,
|
||||
db_session,
|
||||
This function returns comprehensive rights information that can be used
|
||||
by the UI to enable/disable features based on user permissions.
|
||||
|
||||
SECURITY NOTES:
|
||||
- Returns rights based on course ownership and user roles
|
||||
- Includes both course-level and content-level permissions
|
||||
- Safe to expose to UI as it only returns permission information
|
||||
"""
|
||||
# Check if course exists
|
||||
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",
|
||||
)
|
||||
|
||||
# Initialize rights object
|
||||
rights = {
|
||||
"course_uuid": course_uuid,
|
||||
"user_id": current_user.id,
|
||||
"is_anonymous": current_user.id == 0,
|
||||
"permissions": {
|
||||
"read": False,
|
||||
"create": False,
|
||||
"update": False,
|
||||
"delete": False,
|
||||
"create_content": False,
|
||||
"update_content": False,
|
||||
"delete_content": False,
|
||||
"manage_contributors": False,
|
||||
"manage_access": False,
|
||||
"grade_assignments": False,
|
||||
"mark_activities_done": False,
|
||||
"create_certifications": False,
|
||||
},
|
||||
"ownership": {
|
||||
"is_owner": False,
|
||||
"is_creator": False,
|
||||
"is_maintainer": False,
|
||||
"is_contributor": False,
|
||||
"authorship_status": None,
|
||||
},
|
||||
"roles": {
|
||||
"is_admin": False,
|
||||
"is_maintainer_role": False,
|
||||
"is_instructor": False,
|
||||
"is_user": False,
|
||||
}
|
||||
}
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
# Handle anonymous users
|
||||
if current_user.id == 0:
|
||||
# Anonymous users can only read public courses
|
||||
if course.public:
|
||||
rights["permissions"]["read"] = True
|
||||
return rights
|
||||
|
||||
# Check course ownership
|
||||
statement = select(ResourceAuthor).where(
|
||||
ResourceAuthor.resource_uuid == course_uuid,
|
||||
ResourceAuthor.user_id == current_user.id
|
||||
)
|
||||
resource_author = db_session.exec(statement).first()
|
||||
|
||||
if resource_author:
|
||||
rights["ownership"]["authorship_status"] = resource_author.authorship_status
|
||||
|
||||
if resource_author.authorship_status == ResourceAuthorshipStatusEnum.ACTIVE:
|
||||
if resource_author.authorship == ResourceAuthorshipEnum.CREATOR:
|
||||
rights["ownership"]["is_creator"] = True
|
||||
rights["ownership"]["is_owner"] = True
|
||||
elif resource_author.authorship == ResourceAuthorshipEnum.MAINTAINER:
|
||||
rights["ownership"]["is_maintainer"] = True
|
||||
rights["ownership"]["is_owner"] = True
|
||||
elif resource_author.authorship == ResourceAuthorshipEnum.CONTRIBUTOR:
|
||||
rights["ownership"]["is_contributor"] = True
|
||||
rights["ownership"]["is_owner"] = True
|
||||
|
||||
# Check user roles
|
||||
from src.security.rbac.rbac import authorization_verify_based_on_org_admin_status
|
||||
from src.security.rbac.rbac import authorization_verify_based_on_roles
|
||||
|
||||
# Check admin/maintainer role
|
||||
is_admin_or_maintainer = await authorization_verify_based_on_org_admin_status(
|
||||
request, current_user.id, "update", course_uuid, db_session
|
||||
)
|
||||
|
||||
if is_admin_or_maintainer:
|
||||
rights["roles"]["is_admin"] = True
|
||||
rights["roles"]["is_maintainer_role"] = True
|
||||
|
||||
# Check instructor role
|
||||
has_instructor_permissions = await authorization_verify_based_on_roles(
|
||||
request, current_user.id, "create", "course_x", db_session
|
||||
)
|
||||
|
||||
if has_instructor_permissions:
|
||||
rights["roles"]["is_instructor"] = True
|
||||
|
||||
# Check user role (basic permissions)
|
||||
has_user_permissions = await authorization_verify_based_on_roles(
|
||||
request, current_user.id, "read", course_uuid, db_session
|
||||
)
|
||||
|
||||
if has_user_permissions:
|
||||
rights["roles"]["is_user"] = True
|
||||
|
||||
# Determine permissions based on ownership and roles
|
||||
is_course_owner = rights["ownership"]["is_owner"]
|
||||
is_admin = rights["roles"]["is_admin"]
|
||||
is_maintainer_role = rights["roles"]["is_maintainer_role"]
|
||||
is_instructor = rights["roles"]["is_instructor"]
|
||||
|
||||
# READ permissions
|
||||
if course.public or is_course_owner or is_admin or is_maintainer_role or is_instructor or has_user_permissions:
|
||||
rights["permissions"]["read"] = True
|
||||
|
||||
# CREATE permissions (course creation)
|
||||
if is_instructor or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["create"] = True
|
||||
|
||||
# UPDATE permissions (course-level updates)
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["update"] = True
|
||||
|
||||
# DELETE permissions (course deletion)
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["delete"] = True
|
||||
|
||||
# CONTENT CREATION permissions (activities, assignments, chapters, etc.)
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["create_content"] = True
|
||||
|
||||
# CONTENT UPDATE permissions
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["update_content"] = True
|
||||
|
||||
# CONTENT DELETE permissions
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["delete_content"] = True
|
||||
|
||||
# CONTRIBUTOR MANAGEMENT permissions
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["manage_contributors"] = True
|
||||
|
||||
# ACCESS MANAGEMENT permissions (public, open_to_contributors)
|
||||
if (rights["ownership"]["is_creator"] or rights["ownership"]["is_maintainer"] or
|
||||
is_admin or is_maintainer_role):
|
||||
rights["permissions"]["manage_access"] = True
|
||||
|
||||
# GRADING permissions
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["grade_assignments"] = True
|
||||
|
||||
# ACTIVITY MARKING permissions
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["mark_activities_done"] = True
|
||||
|
||||
# CERTIFICATION permissions
|
||||
if is_course_owner or is_admin or is_maintainer_role:
|
||||
rights["permissions"]["create_certifications"] = True
|
||||
|
||||
return rights
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from src.db.courses.course_updates import (
|
|||
from src.db.courses.courses import Course
|
||||
from src.db.organizations import Organization
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.services.courses.courses import rbac_check
|
||||
from src.security.courses_security import courses_rbac_check
|
||||
|
||||
|
||||
async def create_update(
|
||||
|
|
@ -41,7 +41,7 @@ async def create_update(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Generate UUID
|
||||
courseupdate_uuid = str(f"courseupdate_{uuid4()}")
|
||||
|
|
@ -81,7 +81,7 @@ async def update_update(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(
|
||||
await courses_rbac_check(
|
||||
request, update.courseupdate_uuid, current_user, "update", db_session
|
||||
)
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ async def delete_update(
|
|||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(
|
||||
await courses_rbac_check(
|
||||
request, update.courseupdate_uuid, current_user, "delete", db_session
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from src.db.payments.payments_courses import PaymentsCourse
|
|||
from src.db.payments.payments_products import PaymentsProduct
|
||||
from src.db.courses.courses import Course
|
||||
from src.db.users import PublicUser, AnonymousUser
|
||||
from src.services.courses.courses import rbac_check
|
||||
from src.security.courses_security import courses_rbac_check
|
||||
|
||||
async def link_course_to_product(
|
||||
request: Request,
|
||||
|
|
@ -22,7 +22,7 @@ async def link_course_to_product(
|
|||
raise HTTPException(status_code=404, detail="Course not found")
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Check if product exists
|
||||
statement = select(PaymentsProduct).where(
|
||||
|
|
@ -71,7 +71,7 @@ async def unlink_course_from_product(
|
|||
raise HTTPException(status_code=404, detail="Course not found")
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
await courses_rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Find and delete the payment course link
|
||||
statement = select(PaymentsCourse).where(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ import Modal from '@components/Objects/StyledElements/Modal/Modal'
|
|||
import { useSearchParams } from 'next/navigation'
|
||||
import React from 'react'
|
||||
import useAdminStatus from '@components/Hooks/useAdminStatus'
|
||||
import { getUriWithOrg } from '@services/config/config'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import { BookOpen } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
type CourseProps = {
|
||||
orgslug: string
|
||||
|
|
@ -22,6 +26,7 @@ function CoursesHome(params: CourseProps) {
|
|||
const orgslug = params.orgslug
|
||||
const courses = params.courses
|
||||
const isUserAdmin = useAdminStatus() as any
|
||||
const org = useOrg() as any
|
||||
|
||||
async function closeNewCourseModal() {
|
||||
setNewCourseModal(false)
|
||||
|
|
@ -32,7 +37,16 @@ function CoursesHome(params: CourseProps) {
|
|||
<div className="mb-6">
|
||||
<BreadCrumbs type="courses" />
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mt-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<h1 className="text-3xl font-bold mb-4 sm:mb-0">Courses</h1>
|
||||
<Link
|
||||
href={getUriWithOrg(org?.slug, '/dash/documentation/rights')}
|
||||
className="rounded-lg bg-black hover:scale-105 transition-all duration-100 ease-linear antialiased p-2 px-5 font text-xs font-bold text-white drop-shadow-lg flex space-x-2 items-center"
|
||||
>
|
||||
<BookOpen className="w-4 h-4" />
|
||||
<span>Rights Guide</span>
|
||||
</Link>
|
||||
</div>
|
||||
<AuthenticatedClientElement
|
||||
checkMethod="roles"
|
||||
action="create"
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
'use client'
|
||||
import { getUriWithOrg } from '@services/config/config'
|
||||
import React, { use } from 'react';
|
||||
import React, { use, useEffect } from 'react';
|
||||
import { CourseProvider } from '../../../../../../../../components/Contexts/CourseContext'
|
||||
import Link from 'next/link'
|
||||
import { CourseOverviewTop } from '@components/Dashboard/Misc/CourseOverviewTop'
|
||||
import { motion } from 'framer-motion'
|
||||
import { GalleryVerticalEnd, Globe, Info, UserPen, UserRoundCog, Users, Award } from 'lucide-react'
|
||||
import { GalleryVerticalEnd, Globe, Info, UserPen, UserRoundCog, Users, Award, Lock } from 'lucide-react'
|
||||
import EditCourseStructure from '@components/Dashboard/Pages/Course/EditCourseStructure/EditCourseStructure'
|
||||
import EditCourseGeneral from '@components/Dashboard/Pages/Course/EditCourseGeneral/EditCourseGeneral'
|
||||
import EditCourseAccess from '@components/Dashboard/Pages/Course/EditCourseAccess/EditCourseAccess'
|
||||
import EditCourseContributors from '@components/Dashboard/Pages/Course/EditCourseContributors/EditCourseContributors'
|
||||
import EditCourseCertification from '@components/Dashboard/Pages/Course/EditCourseCertification/EditCourseCertification'
|
||||
import { useCourseRights } from '@hooks/useCourseRights'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import ToolTip from '@components/Objects/StyledElements/Tooltip/Tooltip'
|
||||
|
||||
export type CourseOverviewParams = {
|
||||
orgslug: string
|
||||
courseuuid: string
|
||||
|
|
@ -19,110 +23,146 @@ export type CourseOverviewParams = {
|
|||
|
||||
function CourseOverviewPage(props: { params: Promise<CourseOverviewParams> }) {
|
||||
const params = use(props.params);
|
||||
const router = useRouter();
|
||||
|
||||
function getEntireCourseUUID(courseuuid: string) {
|
||||
// add course_ to uuid
|
||||
return `course_${courseuuid}`
|
||||
}
|
||||
|
||||
const courseuuid = getEntireCourseUUID(params.courseuuid)
|
||||
const { hasPermission, isLoading: rightsLoading } = useCourseRights(courseuuid)
|
||||
|
||||
// Define tab configurations with their required permissions
|
||||
const tabs = [
|
||||
{
|
||||
key: 'general',
|
||||
label: 'General',
|
||||
icon: Info,
|
||||
href: `/dash/courses/course/${params.courseuuid}/general`,
|
||||
requiredPermission: 'update' as const
|
||||
},
|
||||
{
|
||||
key: 'content',
|
||||
label: 'Content',
|
||||
icon: GalleryVerticalEnd,
|
||||
href: `/dash/courses/course/${params.courseuuid}/content`,
|
||||
requiredPermission: 'update_content' as const
|
||||
},
|
||||
{
|
||||
key: 'access',
|
||||
label: 'Access',
|
||||
icon: Globe,
|
||||
href: `/dash/courses/course/${params.courseuuid}/access`,
|
||||
requiredPermission: 'manage_access' as const
|
||||
},
|
||||
{
|
||||
key: 'contributors',
|
||||
label: 'Contributors',
|
||||
icon: UserPen,
|
||||
href: `/dash/courses/course/${params.courseuuid}/contributors`,
|
||||
requiredPermission: 'manage_contributors' as const
|
||||
},
|
||||
{
|
||||
key: 'certification',
|
||||
label: 'Certification',
|
||||
icon: Award,
|
||||
href: `/dash/courses/course/${params.courseuuid}/certification`,
|
||||
requiredPermission: 'create_certifications' as const
|
||||
}
|
||||
]
|
||||
|
||||
// Filter tabs based on permissions
|
||||
const visibleTabs = tabs.filter(tab => hasPermission(tab.requiredPermission))
|
||||
|
||||
// Check if current subpage is accessible
|
||||
const currentTab = tabs.find(tab => tab.key === params.subpage)
|
||||
const hasAccessToCurrentPage = currentTab ? hasPermission(currentTab.requiredPermission) : false
|
||||
|
||||
// Redirect to first available tab if current page is not accessible
|
||||
useEffect(() => {
|
||||
if (!rightsLoading && !hasAccessToCurrentPage && visibleTabs.length > 0) {
|
||||
const firstAvailableTab = visibleTabs[0]
|
||||
router.replace(getUriWithOrg(params.orgslug, '') + firstAvailableTab.href)
|
||||
}
|
||||
}, [rightsLoading, hasAccessToCurrentPage, visibleTabs, router, params.orgslug])
|
||||
|
||||
// Show loading state while rights are being fetched
|
||||
if (rightsLoading) {
|
||||
return (
|
||||
<div className="h-screen w-full bg-[#f8f8f8] flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Show access denied if no tabs are available
|
||||
if (!rightsLoading && visibleTabs.length === 0) {
|
||||
return (
|
||||
<div className="h-screen w-full bg-[#f8f8f8] flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Lock className="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Access Denied</h3>
|
||||
<p className="text-gray-500">You don't have permission to access this course.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto_1fr]">
|
||||
<CourseProvider courseuuid={getEntireCourseUUID(params.courseuuid)} withUnpublishedActivities={true}>
|
||||
<CourseProvider courseuuid={courseuuid} withUnpublishedActivities={true}>
|
||||
<div className="pl-10 pr-10 text-sm tracking-tight bg-[#fcfbfc] z-10 nice-shadow">
|
||||
<CourseOverviewTop params={params} />
|
||||
<div className="flex space-x-3 font-black text-sm">
|
||||
<Link
|
||||
href={
|
||||
getUriWithOrg(params.orgslug, '') +
|
||||
`/dash/courses/course/${params.courseuuid}/general`
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`flex space-x-4 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>
|
||||
{tabs.map((tab) => {
|
||||
const IconComponent = tab.icon
|
||||
const isActive = params.subpage.toString() === tab.key
|
||||
const hasAccess = hasPermission(tab.requiredPermission)
|
||||
|
||||
<Link
|
||||
href={
|
||||
getUriWithOrg(params.orgslug, '') +
|
||||
`/dash/courses/course/${params.courseuuid}/content`
|
||||
if (!hasAccess) {
|
||||
// Show disabled tab with subtle visual cues and tooltip
|
||||
return (
|
||||
<ToolTip
|
||||
key={tab.key}
|
||||
content={
|
||||
<div className="text-center">
|
||||
<div className="font-medium text-gray-900">Access Restricted</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
You don't have permission to access {tab.label}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<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 space-x-4 py-2 w-fit text-center border-black transition-all ease-linear opacity-30 cursor-not-allowed">
|
||||
<div className="flex items-center space-x-2.5 mx-2">
|
||||
<GalleryVerticalEnd size={16} />
|
||||
<div>Content</div>
|
||||
<IconComponent size={16} />
|
||||
<div>{tab.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href={
|
||||
getUriWithOrg(params.orgslug, '') +
|
||||
`/dash/courses/course/${params.courseuuid}/access`
|
||||
</ToolTip>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'access'
|
||||
? 'border-b-4'
|
||||
: 'opacity-50'
|
||||
} cursor-pointer`}
|
||||
>
|
||||
<div className="flex items-center space-x-2.5 mx-2">
|
||||
<Globe size={16} />
|
||||
<div>Access</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href={
|
||||
getUriWithOrg(params.orgslug, '') +
|
||||
`/dash/courses/course/${params.courseuuid}/contributors`
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'contributors'
|
||||
? 'border-b-4'
|
||||
: 'opacity-50'
|
||||
} cursor-pointer`}
|
||||
>
|
||||
<div className="flex items-center space-x-2.5 mx-2">
|
||||
<UserPen size={16} />
|
||||
<div>Contributors</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href={
|
||||
getUriWithOrg(params.orgslug, '') +
|
||||
`/dash/courses/course/${params.courseuuid}/certification`
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'certification'
|
||||
? 'border-b-4'
|
||||
: 'opacity-50'
|
||||
} cursor-pointer`}
|
||||
>
|
||||
<div className="flex items-center space-x-2.5 mx-2">
|
||||
<Award size={16} />
|
||||
<div>Certification</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={tab.key}
|
||||
href={getUriWithOrg(params.orgslug, '') + tab.href}
|
||||
>
|
||||
<div
|
||||
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${
|
||||
isActive ? 'border-b-4' : 'opacity-50 hover:opacity-75'
|
||||
} cursor-pointer`}
|
||||
>
|
||||
<div className="flex items-center space-x-2.5 mx-2">
|
||||
<IconComponent size={16} />
|
||||
<div>{tab.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
|
|
@ -132,12 +172,21 @@ function CourseOverviewPage(props: { params: Promise<CourseOverviewParams> }) {
|
|||
className="h-full overflow-y-auto relative"
|
||||
>
|
||||
<div className="absolute inset-0">
|
||||
{params.subpage == 'content' ? (<EditCourseStructure orgslug={params.orgslug} />) : ('')}
|
||||
{params.subpage == 'general' ? (<EditCourseGeneral orgslug={params.orgslug} />) : ('')}
|
||||
{params.subpage == 'access' ? (<EditCourseAccess orgslug={params.orgslug} />) : ('')}
|
||||
{params.subpage == 'contributors' ? (<EditCourseContributors orgslug={params.orgslug} />) : ('')}
|
||||
{params.subpage == 'certification' ? (<EditCourseCertification orgslug={params.orgslug} />) : ('')}
|
||||
|
||||
{params.subpage == 'content' && hasPermission('update_content') ? (
|
||||
<EditCourseStructure orgslug={params.orgslug} />
|
||||
) : null}
|
||||
{params.subpage == 'general' && hasPermission('update') ? (
|
||||
<EditCourseGeneral orgslug={params.orgslug} />
|
||||
) : null}
|
||||
{params.subpage == 'access' && hasPermission('manage_access') ? (
|
||||
<EditCourseAccess orgslug={params.orgslug} />
|
||||
) : null}
|
||||
{params.subpage == 'contributors' && hasPermission('manage_contributors') ? (
|
||||
<EditCourseContributors orgslug={params.orgslug} />
|
||||
) : null}
|
||||
{params.subpage == 'certification' && hasPermission('create_certifications') ? (
|
||||
<EditCourseCertification orgslug={params.orgslug} />
|
||||
) : null}
|
||||
</div>
|
||||
</motion.div>
|
||||
</CourseProvider>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react'
|
||||
|
||||
export default function DocumentationLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <>{children}</>
|
||||
}
|
||||
217
apps/web/app/orgs/[orgslug]/dash/documentation/rights/page.tsx
Normal file
217
apps/web/app/orgs/[orgslug]/dash/documentation/rights/page.tsx
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
'use client'
|
||||
import React from 'react'
|
||||
import { getUriWithOrg } from '@services/config/config'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import {
|
||||
Shield,
|
||||
Users,
|
||||
BookOpen,
|
||||
UserCheck,
|
||||
Lock,
|
||||
Globe,
|
||||
Award,
|
||||
FileText,
|
||||
Settings,
|
||||
Crown,
|
||||
User,
|
||||
UserCog,
|
||||
GraduationCap,
|
||||
Eye,
|
||||
Edit,
|
||||
Trash2,
|
||||
Plus,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
Info,
|
||||
ArrowLeft,
|
||||
AlertTriangle,
|
||||
Key,
|
||||
UserCheck as UserCheckIcon
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
interface RightsDocumentationProps {
|
||||
params: Promise<{ orgslug: string }>
|
||||
}
|
||||
|
||||
const RightsDocumentation = ({ params }: RightsDocumentationProps) => {
|
||||
const org = useOrg() as any
|
||||
|
||||
const roleHierarchy = [
|
||||
{
|
||||
name: 'Admin',
|
||||
icon: <Crown className="w-6 h-6 text-purple-600" />,
|
||||
color: 'bg-purple-50 border-purple-200',
|
||||
description: 'Full platform control with all permissions',
|
||||
permissions: ['All permissions', 'Manage organization', 'Manage users', 'Manage courses', 'Manage roles'],
|
||||
level: 4
|
||||
},
|
||||
{
|
||||
name: 'Maintainer',
|
||||
icon: <Shield className="w-6 h-6 text-blue-600" />,
|
||||
color: 'bg-blue-50 border-blue-200',
|
||||
description: 'Mid-level manager with wide permissions',
|
||||
permissions: ['Manage courses', 'Manage users', 'Manage assignments', ],
|
||||
level: 3
|
||||
},
|
||||
{
|
||||
name: 'Instructor',
|
||||
icon: <GraduationCap className="w-6 h-6 text-green-600" />,
|
||||
color: 'bg-green-50 border-green-200',
|
||||
description: 'Can create courses but need ownership for content creation',
|
||||
permissions: ['Create courses', 'Manage own courses', 'Create assignments', 'Grade assignments'],
|
||||
level: 2
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
icon: <User className="w-6 h-6 text-gray-600" />,
|
||||
color: 'bg-gray-50 border-gray-200',
|
||||
description: 'Read-Only Learner',
|
||||
permissions: ['View courses', 'Submit assignments', 'Take assessments'],
|
||||
level: 1
|
||||
}
|
||||
]
|
||||
|
||||
const courseOwnershipTypes = [
|
||||
{
|
||||
name: 'Creator',
|
||||
icon: <Crown className="w-5 h-5 text-yellow-600" />,
|
||||
color: 'bg-yellow-50 border-yellow-200',
|
||||
description: 'Original course creator with full control',
|
||||
permissions: ['Full course control', 'Manage contributors', 'Change access settings', 'Delete course']
|
||||
},
|
||||
{
|
||||
name: 'Maintainer',
|
||||
icon: <Shield className="w-5 h-5 text-blue-600" />,
|
||||
color: 'bg-blue-50 border-blue-200',
|
||||
description: 'Course maintainer with extensive permissions',
|
||||
permissions: ['Manage course content', 'Manage contributors', 'Change access settings', 'Cannot delete course']
|
||||
},
|
||||
{
|
||||
name: 'Contributor',
|
||||
icon: <UserCog className="w-5 h-5 text-green-600" />,
|
||||
color: 'bg-green-50 border-green-200',
|
||||
description: 'Course contributor with limited permissions',
|
||||
permissions: ['Edit course content', 'Create activities', 'Cannot manage contributors', 'Cannot change access']
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#f8f8f8] flex items-center justify-center p-6 pt-16 w-full">
|
||||
<div className="w-full max-w-none mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Top Icon */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="text-center mb-8"
|
||||
>
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-white rounded-full shadow-sm border border-gray-200 mb-6">
|
||||
<Shield className="w-8 h-8 text-blue-500" />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="text-center mb-12"
|
||||
>
|
||||
<Link
|
||||
href={getUriWithOrg(org?.slug, '/dash')}
|
||||
className="inline-flex items-center space-x-2 text-gray-600 hover:text-gray-900 mb-6 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span className="font-medium">Back to Dashboard</span>
|
||||
</Link>
|
||||
<div className="flex items-center justify-center space-x-3 mb-4">
|
||||
<h1 className="text-4xl font-bold text-gray-900">Authorizations & Rights Guide</h1>
|
||||
</div>
|
||||
<p className="text-gray-600 text-lg max-w-2xl mx-auto">
|
||||
Understanding LearnHouse permissions, roles, and access controls based on RBAC system
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Role Hierarchy Section */}
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="mb-16"
|
||||
>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-8 text-center flex items-center justify-center space-x-2">
|
||||
<Crown className="w-6 h-6 text-purple-600" />
|
||||
<span>Role Hierarchy</span>
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 max-w-7xl mx-auto">
|
||||
{roleHierarchy.map((role, index) => (
|
||||
<motion.div
|
||||
key={role.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 + index * 0.1 }}
|
||||
className={`bg-white rounded-xl border ${role.color} shadow-sm hover:shadow-lg transition-all duration-200 p-6 text-center`}
|
||||
>
|
||||
<div className="flex items-center justify-center space-x-3 mb-4">
|
||||
{role.icon}
|
||||
<h3 className="text-lg font-semibold text-gray-900">{role.name}</h3>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm mb-4">{role.description}</p>
|
||||
<ul className="space-y-2 text-left">
|
||||
{role.permissions.map((permission, permIndex) => (
|
||||
<li key={permIndex} className="flex items-center space-x-2 text-sm text-gray-700">
|
||||
<CheckCircle className="w-3 h-3 text-green-600 flex-shrink-0" />
|
||||
<span>{permission}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.section>
|
||||
|
||||
{/* Course Ownership Types */}
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="mb-16"
|
||||
>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-8 text-center flex items-center justify-center space-x-2">
|
||||
<Users className="w-6 h-6 text-blue-600" />
|
||||
<span>Course Ownership Types</span>
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-6xl mx-auto">
|
||||
{courseOwnershipTypes.map((type, index) => (
|
||||
<motion.div
|
||||
key={type.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 + index * 0.1 }}
|
||||
className={`bg-white rounded-xl border ${type.color} shadow-sm hover:shadow-lg transition-all duration-200 p-6 text-center`}
|
||||
>
|
||||
<div className="flex items-center justify-center space-x-3 mb-4">
|
||||
{type.icon}
|
||||
<h3 className="text-lg font-semibold text-gray-900">{type.name}</h3>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm mb-4">{type.description}</p>
|
||||
<ul className="space-y-2 text-left">
|
||||
{type.permissions.map((permission, permIndex) => (
|
||||
<li key={permIndex} className="flex items-center space-x-2 text-sm text-gray-700">
|
||||
<CheckCircle className="w-3 h-3 text-green-600 flex-shrink-0" />
|
||||
<span>{permission}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RightsDocumentation
|
||||
|
|
@ -9,6 +9,7 @@ import { getCourseThumbnailMediaDirectory } from '@services/media/media'
|
|||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import EmptyThumbnailImage from '../../../public/empty_thumbnail.png'
|
||||
import { BookOpen } from 'lucide-react'
|
||||
|
||||
export function CourseOverviewTop({
|
||||
params,
|
||||
|
|
@ -57,7 +58,14 @@ export function CourseOverviewTop({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
href={getUriWithOrg(org?.slug, '/dash/documentation/rights')}
|
||||
className="rounded-lg bg-black hover:scale-105 transition-all duration-100 ease-linear antialiased p-2 px-5 font text-xs font-bold text-white drop-shadow-lg flex space-x-2 items-center"
|
||||
>
|
||||
<BookOpen className="w-4 h-4" />
|
||||
<span>Rights Guide</span>
|
||||
</Link>
|
||||
<SaveState orgslug={params.orgslug} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,40 +3,193 @@ import { useLHSession } from '@components/Contexts/LHSessionContext';
|
|||
import { useEffect, useState, useMemo } from 'react';
|
||||
|
||||
interface Role {
|
||||
org: { id: number };
|
||||
role: { id: number; role_uuid: string };
|
||||
org: { id: number; org_uuid: string };
|
||||
role: {
|
||||
id: number;
|
||||
role_uuid: string;
|
||||
rights?: {
|
||||
[key: string]: {
|
||||
[key: string]: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function useAdminStatus() {
|
||||
interface Rights {
|
||||
courses: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_read_own: boolean;
|
||||
action_update: boolean;
|
||||
action_update_own: boolean;
|
||||
action_delete: boolean;
|
||||
action_delete_own: boolean;
|
||||
};
|
||||
users: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
usergroups: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
collections: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
organizations: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
coursechapters: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
activities: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
roles: {
|
||||
action_create: boolean;
|
||||
action_read: boolean;
|
||||
action_update: boolean;
|
||||
action_delete: boolean;
|
||||
};
|
||||
dashboard: {
|
||||
action_access: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface UseAdminStatusReturn {
|
||||
isAdmin: boolean | null;
|
||||
loading: boolean;
|
||||
userRoles: Role[];
|
||||
rights: Rights | null;
|
||||
}
|
||||
|
||||
function useAdminStatus(): UseAdminStatusReturn {
|
||||
const session = useLHSession() as any;
|
||||
const org = useOrg() as any;
|
||||
const [isAdmin, setIsAdmin] = useState<boolean | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [rights, setRights] = useState<Rights | null>(null);
|
||||
|
||||
const userRoles = useMemo(() => session?.data?.roles || [], [session?.data?.roles]);
|
||||
|
||||
useEffect(() => {
|
||||
if (session.status === 'authenticated' && org?.id) {
|
||||
const isAdminVar = userRoles.some((role: Role) => {
|
||||
return (
|
||||
role.org.id === org.id &&
|
||||
(
|
||||
role.role.id === 1 ||
|
||||
role.role.id === 2 ||
|
||||
role.role.role_uuid === 'role_global_admin' ||
|
||||
role.role.role_uuid === 'role_global_maintainer'
|
||||
)
|
||||
);
|
||||
// Extract rights from the backend session data
|
||||
const extractRightsFromRoles = (): Rights | null => {
|
||||
if (!userRoles || userRoles.length === 0) return null;
|
||||
|
||||
// Find roles for the current organization
|
||||
const orgRoles = userRoles.filter((role: Role) => role.org.id === org.id);
|
||||
if (orgRoles.length === 0) return null;
|
||||
|
||||
// Merge rights from all roles for this organization
|
||||
const mergedRights: Rights = {
|
||||
courses: {
|
||||
action_create: false,
|
||||
action_read: false,
|
||||
action_read_own: false,
|
||||
action_update: false,
|
||||
action_update_own: false,
|
||||
action_delete: false,
|
||||
action_delete_own: false
|
||||
},
|
||||
users: {
|
||||
action_create: false,
|
||||
action_read: false,
|
||||
action_update: false,
|
||||
action_delete: false
|
||||
},
|
||||
usergroups: {
|
||||
action_create: false,
|
||||
action_read: false,
|
||||
action_update: false,
|
||||
action_delete: false
|
||||
},
|
||||
collections: {
|
||||
action_create: false,
|
||||
action_read: false,
|
||||
action_update: false,
|
||||
action_delete: false
|
||||
},
|
||||
organizations: {
|
||||
action_create: false,
|
||||
action_read: false,
|
||||
action_update: false,
|
||||
action_delete: false
|
||||
},
|
||||
coursechapters: {
|
||||
action_create: false,
|
||||
action_read: false,
|
||||
action_update: false,
|
||||
action_delete: false
|
||||
},
|
||||
activities: {
|
||||
action_create: false,
|
||||
action_read: false,
|
||||
action_update: false,
|
||||
action_delete: false
|
||||
},
|
||||
roles: {
|
||||
action_create: false,
|
||||
action_read: false,
|
||||
action_update: false,
|
||||
action_delete: false
|
||||
},
|
||||
dashboard: {
|
||||
action_access: false
|
||||
}
|
||||
};
|
||||
|
||||
// Merge rights from all roles
|
||||
orgRoles.forEach((role: Role) => {
|
||||
if (role.role.rights) {
|
||||
Object.keys(role.role.rights).forEach((resourceType) => {
|
||||
if (mergedRights[resourceType as keyof Rights]) {
|
||||
Object.keys(role.role.rights![resourceType]).forEach((action) => {
|
||||
if (role.role.rights![resourceType][action] === true) {
|
||||
(mergedRights[resourceType as keyof Rights] as any)[action] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return mergedRights;
|
||||
};
|
||||
|
||||
const extractedRights = extractRightsFromRoles();
|
||||
setRights(extractedRights);
|
||||
|
||||
// User is admin only if they have dashboard access
|
||||
const isAdminVar = extractedRights?.dashboard?.action_access === true;
|
||||
setIsAdmin(isAdminVar);
|
||||
setLoading(false); // Set loading to false once the status is determined
|
||||
|
||||
setLoading(false);
|
||||
} else {
|
||||
setIsAdmin(false);
|
||||
setLoading(false); // Set loading to false if not authenticated or org not found
|
||||
setRights(null);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [session.status, userRoles, org.id]);
|
||||
|
||||
return { isAdmin, loading };
|
||||
return { isAdmin, loading, userRoles, rights };
|
||||
}
|
||||
|
||||
export default useAdminStatus;
|
||||
|
|
|
|||
64
apps/web/components/Hooks/useCourseRights.tsx
Normal file
64
apps/web/components/Hooks/useCourseRights.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
'use client'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
import { swrFetcher } from '@services/utils/ts/requests'
|
||||
import useSWR from 'swr'
|
||||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||
|
||||
export interface CourseRights {
|
||||
course_uuid: string
|
||||
user_id: number
|
||||
is_anonymous: boolean
|
||||
permissions: {
|
||||
read: boolean
|
||||
create: boolean
|
||||
update: boolean
|
||||
delete: boolean
|
||||
create_content: boolean
|
||||
update_content: boolean
|
||||
delete_content: boolean
|
||||
manage_contributors: boolean
|
||||
manage_access: boolean
|
||||
grade_assignments: boolean
|
||||
mark_activities_done: boolean
|
||||
create_certifications: boolean
|
||||
}
|
||||
ownership: {
|
||||
is_owner: boolean
|
||||
is_creator: boolean
|
||||
is_maintainer: boolean
|
||||
is_contributor: boolean
|
||||
authorship_status: string
|
||||
}
|
||||
roles: {
|
||||
is_admin: boolean
|
||||
is_maintainer_role: boolean
|
||||
is_instructor: boolean
|
||||
is_user: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export function useCourseRights(courseuuid: string) {
|
||||
const session = useLHSession() as any
|
||||
const access_token = session?.data?.tokens?.access_token
|
||||
|
||||
const { data: rights, error, isLoading } = useSWR<CourseRights>(
|
||||
courseuuid ? `${getAPIUrl()}courses/${courseuuid}/rights` : null,
|
||||
(url) => swrFetcher(url, access_token)
|
||||
)
|
||||
|
||||
return {
|
||||
rights,
|
||||
error,
|
||||
isLoading,
|
||||
hasPermission: (permission: keyof CourseRights['permissions']) => {
|
||||
return rights?.permissions?.[permission] ?? false
|
||||
},
|
||||
hasRole: (role: keyof CourseRights['roles']) => {
|
||||
return rights?.roles?.[role] ?? false
|
||||
},
|
||||
isOwner: rights?.ownership?.is_owner ?? false,
|
||||
isCreator: rights?.ownership?.is_creator ?? false,
|
||||
isMaintainer: rights?.ownership?.is_maintainer ?? false,
|
||||
isContributor: rights?.ownership?.is_contributor ?? false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,108 @@
|
|||
'use client'
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Link from 'next/link'
|
||||
import { Package2, Settings } from 'lucide-react'
|
||||
import { Package2, Settings, Crown, Shield, User, Users, Building, LogOut, User as UserIcon, Home, ChevronDown } from 'lucide-react'
|
||||
import UserAvatar from '@components/Objects/UserAvatar'
|
||||
import useAdminStatus from '@components/Hooks/useAdminStatus'
|
||||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import { getUriWithoutOrg } from '@services/config/config'
|
||||
import Tooltip from '@components/Objects/StyledElements/Tooltip/Tooltip'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@components/ui/dropdown-menu"
|
||||
import { signOut } from 'next-auth/react'
|
||||
|
||||
interface RoleInfo {
|
||||
name: string;
|
||||
icon: React.ReactNode;
|
||||
bgColor: string;
|
||||
textColor: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const HeaderProfileBox = () => {
|
||||
const session = useLHSession() as any
|
||||
const isUserAdmin = useAdminStatus()
|
||||
const { isAdmin, loading, userRoles, rights } = useAdminStatus()
|
||||
const org = useOrg() as any
|
||||
|
||||
useEffect(() => { }
|
||||
, [session])
|
||||
|
||||
const userRoleInfo = useMemo((): RoleInfo | null => {
|
||||
if (!userRoles || userRoles.length === 0) return null;
|
||||
|
||||
// Find the highest priority role for the current organization
|
||||
const orgRoles = userRoles.filter((role: any) => role.org.id === org?.id);
|
||||
|
||||
if (orgRoles.length === 0) return null;
|
||||
|
||||
// Sort by role priority (admin > maintainer > instructor > user)
|
||||
const sortedRoles = orgRoles.sort((a: any, b: any) => {
|
||||
const getRolePriority = (role: any) => {
|
||||
if (role.role.role_uuid === 'role_global_admin' || role.role.id === 1) return 4;
|
||||
if (role.role.role_uuid === 'role_global_maintainer' || role.role.id === 2) return 3;
|
||||
if (role.role.role_uuid === 'role_global_instructor' || role.role.id === 3) return 2;
|
||||
return 1;
|
||||
};
|
||||
return getRolePriority(b) - getRolePriority(a);
|
||||
});
|
||||
|
||||
const highestRole = sortedRoles[0];
|
||||
|
||||
// Define role configurations based on actual database roles
|
||||
const roleConfigs: { [key: string]: RoleInfo } = {
|
||||
'role_global_admin': {
|
||||
name: 'ADMIN',
|
||||
icon: <Crown size={12} />,
|
||||
bgColor: 'bg-purple-600',
|
||||
textColor: 'text-white',
|
||||
description: 'Full platform control with all permissions'
|
||||
},
|
||||
'role_global_maintainer': {
|
||||
name: 'MAINTAINER',
|
||||
icon: <Shield size={12} />,
|
||||
bgColor: 'bg-blue-600',
|
||||
textColor: 'text-white',
|
||||
description: 'Mid-level manager with wide permissions'
|
||||
},
|
||||
'role_global_instructor': {
|
||||
name: 'INSTRUCTOR',
|
||||
icon: <Users size={12} />,
|
||||
bgColor: 'bg-green-600',
|
||||
textColor: 'text-white',
|
||||
description: 'Can manage their own content'
|
||||
},
|
||||
'role_global_user': {
|
||||
name: 'USER',
|
||||
icon: <User size={12} />,
|
||||
bgColor: 'bg-gray-500',
|
||||
textColor: 'text-white',
|
||||
description: 'Read-Only Learner'
|
||||
}
|
||||
};
|
||||
|
||||
// Determine role based on role_uuid or id
|
||||
let roleKey = 'role_global_user'; // default
|
||||
if (highestRole.role.role_uuid) {
|
||||
roleKey = highestRole.role.role_uuid;
|
||||
} else if (highestRole.role.id === 1) {
|
||||
roleKey = 'role_global_admin';
|
||||
} else if (highestRole.role.id === 2) {
|
||||
roleKey = 'role_global_maintainer';
|
||||
} else if (highestRole.role.id === 3) {
|
||||
roleKey = 'role_global_instructor';
|
||||
}
|
||||
|
||||
return roleConfigs[roleKey] || roleConfigs['role_global_user'];
|
||||
}, [userRoles, org?.id]);
|
||||
|
||||
return (
|
||||
<ProfileArea>
|
||||
{session.status == 'unauthenticated' && (
|
||||
|
|
@ -35,35 +120,73 @@ export const HeaderProfileBox = () => {
|
|||
)}
|
||||
{session.status == 'authenticated' && (
|
||||
<AccountArea className="space-x-0">
|
||||
<div className="flex items-center space-x-3">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="cursor-pointer flex items-center space-x-3 hover:bg-gray-50 rounded-lg p-2 transition-colors">
|
||||
<UserAvatar border="border-2" rounded="rounded-lg" width={30} />
|
||||
<div className="flex flex-col space-y-0">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className='flex items-center space-x-2' >
|
||||
<p className='text-sm capitalize'>{session.data.user.username}</p>
|
||||
{isUserAdmin.isAdmin && <div className="text-[10px] bg-rose-300 px-2 font-bold rounded-md shadow-inner py-1">ADMIN</div>}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className='text-sm font-semibold text-gray-900 capitalize'>{session.data.user.username}</p>
|
||||
{userRoleInfo && userRoleInfo.name !== 'USER' && (
|
||||
<Tooltip
|
||||
content={"Your Owned Courses"}
|
||||
content={userRoleInfo.description}
|
||||
sideOffset={15}
|
||||
side="bottom"
|
||||
>
|
||||
<Link className="text-gray-600" href={'/dash/user-account/owned'}>
|
||||
<Package2 size={14} />
|
||||
</Link>
|
||||
<div className={`text-[6px] ${userRoleInfo.bgColor} ${userRoleInfo.textColor} px-1 py-0.5 font-medium rounded-full flex items-center gap-0.5 w-fit`}>
|
||||
{userRoleInfo.icon}
|
||||
{userRoleInfo.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={"Your Settings"}
|
||||
sideOffset={15}
|
||||
side="bottom"
|
||||
)}
|
||||
</div>
|
||||
<p className='text-xs text-gray-500'>{session.data.user.email}</p>
|
||||
</div>
|
||||
<ChevronDown size={16} className="text-gray-500" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="end">
|
||||
<DropdownMenuLabel>
|
||||
<div className="flex items-center space-x-2">
|
||||
<UserAvatar border="border-2" rounded="rounded-full" width={24} />
|
||||
<div>
|
||||
<p className="text-sm font-medium">{session.data.user.username}</p>
|
||||
<p className="text-xs text-gray-500 capitalize">{session.data.user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{rights?.dashboard?.action_access && (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/dash" className="flex items-center space-x-2">
|
||||
<Shield size={16} />
|
||||
<span>Dashboard</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/dash/user-account/settings/general" className="flex items-center space-x-2">
|
||||
<UserIcon size={16} />
|
||||
<span>User Settings</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/dash/user-account/owned" className="flex items-center space-x-2">
|
||||
<Package2 size={16} />
|
||||
<span>My Courses</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => signOut({ callbackUrl: '/' })}
|
||||
className="flex items-center space-x-2 text-red-600 focus:text-red-600"
|
||||
>
|
||||
<Link className="text-gray-600" href={'/dash'}>
|
||||
<Settings size={14} />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<UserAvatar border="border-4" rounded="rounded-lg" width={30} />
|
||||
</div>
|
||||
<LogOut size={16} />
|
||||
<span>Sign Out</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</AccountArea>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -166,6 +166,15 @@ export async function bulkRemoveContributors(course_uuid: string, data: any, acc
|
|||
`${getAPIUrl()}courses/${course_uuid}/bulk-remove-contributors`,
|
||||
RequestBodyWithAuthHeader('PUT', data, null, access_token || undefined)
|
||||
)
|
||||
const res = await getResponseMetadata(result)
|
||||
const res = await errorHandling(result)
|
||||
return res
|
||||
}
|
||||
|
||||
export async function getCourseRights(course_uuid: string, access_token: string | null | undefined) {
|
||||
const result: any = await fetch(
|
||||
`${getAPIUrl()}courses/${course_uuid}/rights`,
|
||||
RequestBodyWithAuthHeader('GET', null, null, access_token || undefined)
|
||||
)
|
||||
const res = await errorHandling(result)
|
||||
return res
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue