mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: users management
This commit is contained in:
parent
a552300e15
commit
689625b0d5
22 changed files with 621 additions and 36 deletions
|
|
@ -1,5 +1,8 @@
|
|||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import Field, SQLModel
|
||||
from src.db.roles import RoleRead
|
||||
|
||||
from src.db.organization_config import OrganizationConfig
|
||||
|
||||
|
||||
|
|
@ -32,3 +35,9 @@ class OrganizationRead(OrganizationBase):
|
|||
config: Optional[OrganizationConfig | dict]
|
||||
creation_date: str
|
||||
update_date: str
|
||||
|
||||
|
||||
class OrganizationUser(BaseModel):
|
||||
from src.db.users import UserRead
|
||||
user: UserRead
|
||||
role: RoleRead
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
from typing import Optional
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
from src.db.roles import RoleRead
|
||||
from src.db.organizations import OrganizationRead
|
||||
|
||||
|
||||
|
||||
class UserBase(SQLModel):
|
||||
|
|
@ -45,6 +44,7 @@ class PublicUser(UserRead):
|
|||
|
||||
|
||||
class UserRoleWithOrg(BaseModel):
|
||||
from src.db.organizations import OrganizationRead
|
||||
role: RoleRead
|
||||
org: OrganizationRead
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from src.db.organizations import (
|
|||
OrganizationCreate,
|
||||
OrganizationRead,
|
||||
OrganizationUpdate,
|
||||
OrganizationUser,
|
||||
)
|
||||
from src.core.events.database import get_db_session
|
||||
from src.security.auth import get_current_user
|
||||
|
|
@ -17,9 +18,12 @@ from src.services.orgs.orgs import (
|
|||
delete_org,
|
||||
get_organization,
|
||||
get_organization_by_slug,
|
||||
get_organization_users,
|
||||
get_orgs_by_user,
|
||||
remove_user_from_org,
|
||||
update_org,
|
||||
update_org_logo,
|
||||
update_user_role,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -69,6 +73,52 @@ async def api_get_org(
|
|||
return await get_organization(request, org_id, db_session, current_user)
|
||||
|
||||
|
||||
@router.get("/{org_id}/users")
|
||||
async def api_get_org_users(
|
||||
request: Request,
|
||||
org_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
) -> list[OrganizationUser]:
|
||||
"""
|
||||
Get single Org by ID
|
||||
"""
|
||||
return await get_organization_users(request, org_id, db_session, current_user)
|
||||
|
||||
|
||||
@router.put("/{org_id}/users/{user_id}/role/{role_uuid}")
|
||||
async def api_update_user_role(
|
||||
request: Request,
|
||||
org_id: str,
|
||||
user_id: str,
|
||||
role_uuid: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Update user role
|
||||
"""
|
||||
return await update_user_role(
|
||||
request, org_id, user_id, role_uuid, db_session, current_user
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{org_id}/users/{user_id}")
|
||||
async def api_remove_user_from_org(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
user_id: int,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Remove user from org
|
||||
"""
|
||||
return await remove_user_from_org(
|
||||
request, org_id, user_id, db_session, current_user
|
||||
)
|
||||
|
||||
|
||||
@router.get("/slug/{org_slug}")
|
||||
async def api_get_org_by_slug(
|
||||
request: Request,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ async def authorization_verify_if_element_is_public(
|
|||
element_uuid: str,
|
||||
action: Literal["read"],
|
||||
db_session: Session,
|
||||
):
|
||||
):
|
||||
element_nature = await check_element_type(element_uuid)
|
||||
# Verifies if the element is public
|
||||
if element_nature == ("courses" or "collections") and action == "read":
|
||||
|
|
@ -106,6 +106,34 @@ async def authorization_verify_based_on_roles(
|
|||
return False
|
||||
|
||||
|
||||
async def authorization_verify_based_on_org_admin_status(
|
||||
request: Request,
|
||||
user_id: int,
|
||||
action: Literal["read", "update", "delete", "create"],
|
||||
element_uuid: str,
|
||||
db_session: Session,
|
||||
):
|
||||
await check_element_type(element_uuid)
|
||||
|
||||
# Get user roles bound to an organization and standard roles
|
||||
statement = (
|
||||
select(Role)
|
||||
.join(UserOrganization)
|
||||
.where((UserOrganization.org_id == Role.org_id) | (Role.org_id == null()))
|
||||
.where(UserOrganization.user_id == user_id)
|
||||
)
|
||||
|
||||
user_roles_in_organization_and_standard_roles = db_session.exec(statement).all()
|
||||
|
||||
# Find in roles list if there is a role that matches users action for this type of element
|
||||
for role in user_roles_in_organization_and_standard_roles:
|
||||
role = Role.from_orm(role)
|
||||
if role.id == 1 or role.id == 2:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
# Tested and working
|
||||
async def authorization_verify_based_on_roles_and_authorship(
|
||||
request: Request,
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ async def get_course_chapters(
|
|||
chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters]
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session) # type: ignore
|
||||
|
||||
# Get activities for each chapter
|
||||
for chapter in chapters:
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ async def update_course_thumbnail(
|
|||
if thumbnail_file and thumbnail_file.filename:
|
||||
name_in_disk = f"{course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
|
||||
await upload_thumbnail(
|
||||
thumbnail_file, name_in_disk, 'users', course.course_uuid
|
||||
thumbnail_file, name_in_disk, org.org_uuid, course.course_uuid
|
||||
)
|
||||
|
||||
# Update course
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from datetime import datetime
|
|||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
from sqlmodel import Session, select
|
||||
from src.db.roles import Role, RoleRead
|
||||
from src.db.organization_config import (
|
||||
AIConfig,
|
||||
AIEnabledFeatures,
|
||||
|
|
@ -15,16 +16,18 @@ from src.db.organization_config import (
|
|||
OrganizationConfigBase,
|
||||
)
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_based_on_org_admin_status,
|
||||
authorization_verify_based_on_roles,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.db.users import AnonymousUser, PublicUser, User, UserRead
|
||||
from src.db.user_organizations import UserOrganization
|
||||
from src.db.organizations import (
|
||||
Organization,
|
||||
OrganizationCreate,
|
||||
OrganizationRead,
|
||||
OrganizationUpdate,
|
||||
OrganizationUser,
|
||||
)
|
||||
from src.services.orgs.logos import upload_org_logo
|
||||
from fastapi import HTTPException, UploadFile, status, Request
|
||||
|
|
@ -66,6 +69,199 @@ async def get_organization(
|
|||
return org
|
||||
|
||||
|
||||
async def get_organization_users(
|
||||
request: Request,
|
||||
org_id: str,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
) -> list[OrganizationUser]:
|
||||
statement = select(Organization).where(Organization.id == org_id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
org = result.first()
|
||||
|
||||
if not org:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Organization not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
|
||||
|
||||
statement = (
|
||||
select(User)
|
||||
.join(UserOrganization)
|
||||
.join(Organization)
|
||||
.where(Organization.id == org_id)
|
||||
)
|
||||
users = db_session.exec(statement)
|
||||
users = users.all()
|
||||
|
||||
org_users_list = []
|
||||
|
||||
for user in users:
|
||||
statement = select(UserOrganization).where(
|
||||
UserOrganization.user_id == user.id, UserOrganization.org_id == org_id
|
||||
)
|
||||
result = db_session.exec(statement)
|
||||
user_org = result.first()
|
||||
|
||||
if not user_org:
|
||||
logging.error(f"User {user.id} not found")
|
||||
|
||||
# skip this user
|
||||
continue
|
||||
|
||||
|
||||
statement = select(Role).where(Role.id == user_org.role_id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
role = result.first()
|
||||
|
||||
if not role:
|
||||
logging.error(f"Role {user_org.role_id} not found")
|
||||
|
||||
# skip this user
|
||||
continue
|
||||
|
||||
statement = select(User).where(User.id == user_org.user_id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
user = result.first()
|
||||
|
||||
if not user:
|
||||
logging.error(f"User {user_org.user_id} not found")
|
||||
|
||||
# skip this user
|
||||
continue
|
||||
|
||||
user = UserRead.from_orm(user)
|
||||
role = RoleRead.from_orm(role)
|
||||
|
||||
org_user = OrganizationUser(
|
||||
user=user,
|
||||
role=role,
|
||||
)
|
||||
|
||||
org_users_list.append(org_user)
|
||||
|
||||
return org_users_list
|
||||
|
||||
|
||||
async def remove_user_from_org(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
user_id: int,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
):
|
||||
statement = select(Organization).where(Organization.id == org_id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
org = result.first()
|
||||
|
||||
if not org:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Organization not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
|
||||
|
||||
statement = select(UserOrganization).where(
|
||||
UserOrganization.user_id == user_id, UserOrganization.org_id == org.id
|
||||
)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
user_org = result.first()
|
||||
|
||||
if not user_org:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
# Check if user is the last admin
|
||||
statement = select(UserOrganization).where(
|
||||
UserOrganization.org_id == org.id, UserOrganization.role_id == 1
|
||||
)
|
||||
result = db_session.exec(statement)
|
||||
admins = result.all()
|
||||
|
||||
if len(admins) == 1 and admins[0].user_id == user_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="You can't remove the last admin of the organization",
|
||||
)
|
||||
|
||||
|
||||
db_session.delete(user_org)
|
||||
db_session.commit()
|
||||
|
||||
return {"detail": "User removed from org"}
|
||||
|
||||
|
||||
async def update_user_role(
|
||||
request: Request,
|
||||
org_id: str,
|
||||
user_id: str,
|
||||
role_uuid: str,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
):
|
||||
# find role
|
||||
statement = select(Role).where(Role.role_uuid == role_uuid)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
role = result.first()
|
||||
|
||||
if not role:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Role not found",
|
||||
)
|
||||
|
||||
role_id = role.id
|
||||
|
||||
statement = select(Organization).where(Organization.id == org_id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
org = result.first()
|
||||
|
||||
if not org:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Organization not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
|
||||
|
||||
statement = select(UserOrganization).where(
|
||||
UserOrganization.user_id == user_id, UserOrganization.org_id == org.id
|
||||
)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
user_org = result.first()
|
||||
|
||||
if not user_org:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
if role_id is not None:
|
||||
user_org.role_id = role_id
|
||||
|
||||
db_session.add(user_org)
|
||||
db_session.commit()
|
||||
db_session.refresh(user_org)
|
||||
|
||||
return {"detail": "User role updated"}
|
||||
|
||||
|
||||
async def get_organization_by_slug(
|
||||
request: Request,
|
||||
org_slug: str,
|
||||
|
|
@ -443,7 +639,7 @@ async def get_orgs_by_user(
|
|||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
org_id: str,
|
||||
org_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
|
|
@ -455,8 +651,12 @@ async def rbac_check(
|
|||
else:
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, org_id, db_session
|
||||
await authorization_verify_based_on_roles(
|
||||
request, current_user.id, action, org_uuid, db_session
|
||||
)
|
||||
|
||||
await authorization_verify_based_on_org_admin_status(
|
||||
request, current_user.id, action, org_uuid, db_session
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ async def upload_avatar(avatar_file, name_in_disk, user_uuid):
|
|||
contents = avatar_file.file.read()
|
||||
try:
|
||||
await upload_content(
|
||||
f"avatars",
|
||||
"avatars",
|
||||
"users",
|
||||
user_uuid,
|
||||
contents,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue