mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: invite-only org signup
This commit is contained in:
parent
689625b0d5
commit
0d775a0fe9
22 changed files with 1733 additions and 387 deletions
|
|
@ -1,6 +1,20 @@
|
|||
from typing import List
|
||||
from typing import List, Literal
|
||||
from fastapi import APIRouter, Depends, Request, UploadFile
|
||||
from sqlmodel import Session
|
||||
from src.services.orgs.invites import (
|
||||
create_invite_code,
|
||||
delete_invite_code,
|
||||
get_invite_code,
|
||||
get_invite_codes,
|
||||
)
|
||||
from src.services.orgs.users import (
|
||||
get_list_of_invited_users,
|
||||
get_organization_users,
|
||||
invite_batch_users,
|
||||
remove_invited_user,
|
||||
remove_user_from_org,
|
||||
update_user_role,
|
||||
)
|
||||
from src.db.organization_config import OrganizationConfigBase
|
||||
from src.db.users import PublicUser
|
||||
from src.db.organizations import (
|
||||
|
|
@ -18,12 +32,10 @@ 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,
|
||||
update_org_signup_mechanism,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -119,6 +131,120 @@ async def api_remove_user_from_org(
|
|||
)
|
||||
|
||||
|
||||
# Config related routes
|
||||
@router.put("/{org_id}/signup_mechanism")
|
||||
async def api_get_org_signup_mechanism(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
signup_mechanism: Literal["open", "inviteOnly"],
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Get org signup mechanism
|
||||
"""
|
||||
return await update_org_signup_mechanism(
|
||||
request, signup_mechanism, org_id, current_user, db_session
|
||||
)
|
||||
|
||||
|
||||
# Invites related routes
|
||||
@router.post("/{org_id}/invites")
|
||||
async def api_create_invite_code(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Create invite code
|
||||
"""
|
||||
return await create_invite_code(request, org_id, current_user, db_session)
|
||||
|
||||
|
||||
@router.get("/{org_id}/invites")
|
||||
async def api_get_invite_codes(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Get invite codes
|
||||
"""
|
||||
return await get_invite_codes(request, org_id, current_user, db_session)
|
||||
|
||||
@router.get("/{org_id}/invites/code/{invite_code}")
|
||||
async def api_get_invite_code(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
invite_code: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Get invite code
|
||||
"""
|
||||
print(f"org_id: {org_id}, invite_code: {invite_code}")
|
||||
return await get_invite_code(request, org_id,invite_code, current_user, db_session)
|
||||
|
||||
|
||||
@router.delete("/{org_id}/invites/{org_invite_code_uuid}")
|
||||
async def api_delete_invite_code(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
org_invite_code_uuid: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Delete invite code
|
||||
"""
|
||||
return await delete_invite_code(
|
||||
request, org_id, org_invite_code_uuid, current_user, db_session
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{org_id}/invites/users/batch")
|
||||
async def api_invite_batch_users(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
users: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Invite batch users
|
||||
"""
|
||||
return await invite_batch_users(request, org_id, users, db_session, current_user)
|
||||
|
||||
|
||||
@router.get("/{org_id}/invites/users")
|
||||
async def api_get_org_users_invites(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Get org users invites
|
||||
"""
|
||||
return await get_list_of_invited_users(request, org_id, db_session, current_user)
|
||||
|
||||
@router.delete("/{org_id}/invites/users/{email}")
|
||||
async def api_delete_org_users_invites(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
email: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Delete org users invites
|
||||
"""
|
||||
return await remove_invited_user(request, org_id, email, db_session, current_user)
|
||||
|
||||
|
||||
@router.get("/slug/{org_slug}")
|
||||
async def api_get_org_by_slug(
|
||||
request: Request,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from typing import Literal
|
||||
from fastapi import APIRouter, Depends, Request, UploadFile
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
|
||||
from sqlmodel import Session
|
||||
from src.services.orgs.orgs import get_org_join_mechanism
|
||||
from src.security.auth import get_current_user
|
||||
from src.core.events.database import get_db_session
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ from src.db.users import (
|
|||
from src.services.users.users import (
|
||||
authorize_user_action,
|
||||
create_user,
|
||||
create_user_with_invite,
|
||||
create_user_without_org,
|
||||
delete_user_by_id,
|
||||
get_user_session,
|
||||
|
|
@ -78,7 +80,48 @@ async def api_create_user_with_orgid(
|
|||
"""
|
||||
Create User with Org ID
|
||||
"""
|
||||
return await create_user(request, db_session, current_user, user_object, org_id)
|
||||
print(await get_org_join_mechanism(request, org_id, current_user, db_session))
|
||||
|
||||
# TODO(fix) : This is temporary, logic should be moved to service
|
||||
if (
|
||||
await get_org_join_mechanism(request, org_id, current_user, db_session)
|
||||
== "inviteOnly"
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You need an invite to join this organization",
|
||||
)
|
||||
else:
|
||||
return await create_user(request, db_session, current_user, user_object, org_id)
|
||||
|
||||
|
||||
@router.post("/{org_id}/invite/{invite_code}", response_model=UserRead, tags=["users"])
|
||||
async def api_create_user_with_orgid_and_invite(
|
||||
*,
|
||||
request: Request,
|
||||
db_session: Session = Depends(get_db_session),
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
user_object: UserCreate,
|
||||
invite_code: str,
|
||||
org_id: int,
|
||||
) -> UserRead:
|
||||
"""
|
||||
Create User with Org ID and invite code
|
||||
"""
|
||||
|
||||
# TODO: This is temporary, logic should be moved to service
|
||||
if (
|
||||
await get_org_join_mechanism(request, org_id, current_user, db_session)
|
||||
== "inviteOnly"
|
||||
):
|
||||
return await create_user_with_invite(
|
||||
request, db_session, current_user, user_object, org_id, invite_code
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This organization does not require an invite code",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/", response_model=UserRead, tags=["users"])
|
||||
|
|
|
|||
251
apps/api/src/services/orgs/invites.py
Normal file
251
apps/api/src/services/orgs/invites.py
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
import json
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import redis
|
||||
from datetime import datetime, timedelta
|
||||
from sqlmodel import Session, select
|
||||
from config.config import get_learnhouse_config
|
||||
from src.services.orgs.orgs import rbac_check
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.db.organizations import (
|
||||
Organization,
|
||||
)
|
||||
from fastapi import HTTPException, Request
|
||||
|
||||
|
||||
async def create_invite_code(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
# Redis init
|
||||
LH_CONFIG = get_learnhouse_config()
|
||||
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
|
||||
|
||||
if not redis_conn_string:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Redis connection string not found",
|
||||
)
|
||||
|
||||
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, "update", db_session)
|
||||
|
||||
# Connect to Redis
|
||||
r = redis.Redis.from_url(redis_conn_string)
|
||||
|
||||
if not r:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Could not connect to Redis",
|
||||
)
|
||||
|
||||
# Check if this org has more than 6 invite codes
|
||||
invite_codes = r.keys(f"*:org:{org.org_uuid}:code:*")
|
||||
|
||||
if len(invite_codes) >= 6:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Organization has reached the maximum number of invite codes",
|
||||
)
|
||||
|
||||
# Generate invite code
|
||||
def generate_code(length=5):
|
||||
letters_and_digits = string.ascii_letters + string.digits
|
||||
return "".join(random.choice(letters_and_digits) for _ in range(length))
|
||||
|
||||
generated_invite_code = generate_code()
|
||||
invite_code_uuid = f"org_invite_code_{uuid.uuid4()}"
|
||||
|
||||
# time to live in days to seconds
|
||||
ttl = int(timedelta(days=365).total_seconds())
|
||||
|
||||
inviteCodeObject = {
|
||||
"invite_code": generated_invite_code,
|
||||
"invite_code_uuid": invite_code_uuid,
|
||||
"invite_code_expires": ttl,
|
||||
"invite_code_type": "signup",
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"created_by": current_user.user_uuid,
|
||||
}
|
||||
|
||||
r.set(
|
||||
f"{invite_code_uuid}:org:{org.org_uuid}:code:{generated_invite_code}",
|
||||
json.dumps(inviteCodeObject),
|
||||
ex=ttl,
|
||||
)
|
||||
|
||||
return inviteCodeObject
|
||||
|
||||
|
||||
async def get_invite_codes(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
# Redis init
|
||||
LH_CONFIG = get_learnhouse_config()
|
||||
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
|
||||
|
||||
if not redis_conn_string:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Redis connection string not found",
|
||||
)
|
||||
|
||||
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, "update", db_session)
|
||||
|
||||
# Connect to Redis
|
||||
r = redis.Redis.from_url(redis_conn_string)
|
||||
|
||||
if not r:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Could not connect to Redis",
|
||||
)
|
||||
|
||||
# Get invite codes
|
||||
invite_codes = r.keys(f"org_invite_code_*:org:{org.org_uuid}:code:*")
|
||||
|
||||
invite_codes_list = []
|
||||
|
||||
for invite_code in invite_codes:
|
||||
invite_code = r.get(invite_code)
|
||||
invite_code = json.loads(invite_code)
|
||||
invite_codes_list.append(invite_code)
|
||||
|
||||
return invite_codes_list
|
||||
|
||||
async def get_invite_code(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
invite_code: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
# Redis init
|
||||
LH_CONFIG = get_learnhouse_config()
|
||||
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
|
||||
|
||||
if not redis_conn_string:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Redis connection string not found",
|
||||
)
|
||||
|
||||
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, "update", db_session)
|
||||
|
||||
# Connect to Redis
|
||||
r = redis.Redis.from_url(redis_conn_string)
|
||||
|
||||
if not r:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Could not connect to Redis",
|
||||
)
|
||||
|
||||
|
||||
# Get invite code
|
||||
invite_code = r.keys(f"org_invite_code_*:org:{org.org_uuid}:code:{invite_code}") # type: ignore
|
||||
|
||||
if not invite_code:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Invite code not found",
|
||||
)
|
||||
|
||||
invite_code = r.get(invite_code[0]) # type: ignore
|
||||
invite_code = json.loads(invite_code)
|
||||
|
||||
return invite_code
|
||||
|
||||
|
||||
|
||||
async def delete_invite_code(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
invite_code_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
# Redis init
|
||||
LH_CONFIG = get_learnhouse_config()
|
||||
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
|
||||
|
||||
if not redis_conn_string:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Redis connection string not found",
|
||||
)
|
||||
|
||||
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, "update", db_session)
|
||||
|
||||
# Connect to Redis
|
||||
r = redis.Redis.from_url(redis_conn_string)
|
||||
|
||||
if not r:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Could not connect to Redis",
|
||||
)
|
||||
|
||||
# Delete invite code
|
||||
invite_code = r.delete(f"{invite_code_uuid}:org:{org.org_uuid}:code:*")
|
||||
|
||||
if not invite_code:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Invite code not found",
|
||||
)
|
||||
|
||||
return invite_code
|
||||
|
|
@ -9,7 +9,7 @@ async def upload_org_logo(logo_file, org_uuid):
|
|||
|
||||
await upload_content(
|
||||
"logos",
|
||||
org_uuid,
|
||||
"orgs",
|
||||
org_uuid,
|
||||
contents,
|
||||
name_in_disk,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ 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,
|
||||
|
|
@ -17,17 +16,15 @@ from src.db.organization_config import (
|
|||
)
|
||||
from src.security.rbac.rbac import (
|
||||
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, User, UserRead
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
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
|
||||
|
|
@ -69,199 +66,6 @@ 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,
|
||||
|
|
@ -634,6 +438,102 @@ async def get_orgs_by_user(
|
|||
return orgs
|
||||
|
||||
|
||||
# Config related
|
||||
async def update_org_signup_mechanism(
|
||||
request: Request,
|
||||
signup_mechanism: Literal["open", "inviteOnly"],
|
||||
org_id: int,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
statement = select(Organization).where(Organization.id == org_id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
org = result.first()
|
||||
|
||||
if not org:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Organization not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, org.org_uuid, current_user, "update", db_session)
|
||||
|
||||
# Get org config
|
||||
statement = select(OrganizationConfig).where(OrganizationConfig.org_id == org.id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
org_config = result.first()
|
||||
|
||||
if org_config is None:
|
||||
logging.error(f"Organization {org_id} has no config")
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Organization config not found",
|
||||
)
|
||||
|
||||
updated_config = org_config.config
|
||||
|
||||
# Update config
|
||||
updated_config = OrganizationConfigBase(**updated_config)
|
||||
updated_config.GeneralConfig.users.signup_mechanism = signup_mechanism
|
||||
|
||||
# Update the database
|
||||
org_config.config = json.loads(updated_config.json())
|
||||
org_config.update_date = str(datetime.now())
|
||||
|
||||
db_session.add(org_config)
|
||||
db_session.commit()
|
||||
db_session.refresh(org_config)
|
||||
|
||||
return {"detail": "Signup mechanism updated"}
|
||||
|
||||
|
||||
async def get_org_join_mechanism(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
statement = select(Organization).where(Organization.id == org_id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
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)
|
||||
|
||||
# Get org config
|
||||
statement = select(OrganizationConfig).where(OrganizationConfig.org_id == org.id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
org_config = result.first()
|
||||
|
||||
if org_config is None:
|
||||
logging.error(f"Organization {org_id} has no config")
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Organization config not found",
|
||||
)
|
||||
|
||||
config = org_config.config
|
||||
|
||||
# Get the signup mechanism
|
||||
config = OrganizationConfigBase(**config)
|
||||
signup_mechanism = config.GeneralConfig.users.signup_mechanism
|
||||
|
||||
return signup_mechanism
|
||||
|
||||
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
||||
|
||||
|
|
@ -649,15 +549,25 @@ async def rbac_check(
|
|||
return True
|
||||
|
||||
else:
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
isUserAnon = await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
await authorization_verify_based_on_roles(
|
||||
request, current_user.id, action, org_uuid, db_session
|
||||
isAllowedOnOrgAdminStatus = (
|
||||
await authorization_verify_based_on_org_admin_status(
|
||||
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
|
||||
)
|
||||
if isUserAnon:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="You should be logged in to be able to achieve this action",
|
||||
)
|
||||
|
||||
if not isAllowedOnOrgAdminStatus:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights (admin status) : You don't have the right to perform this action",
|
||||
)
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
|
|
|||
410
apps/api/src/services/orgs/users.py
Normal file
410
apps/api/src/services/orgs/users.py
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import logging
|
||||
|
||||
import redis
|
||||
from fastapi import HTTPException, Request
|
||||
from sqlmodel import Session, select
|
||||
from config.config import get_learnhouse_config
|
||||
from src.services.orgs.orgs import rbac_check
|
||||
from src.db.roles import Role, RoleRead
|
||||
from src.db.users import AnonymousUser, PublicUser, User, UserRead
|
||||
from src.db.user_organizations import UserOrganization
|
||||
from src.db.organizations import (
|
||||
Organization,
|
||||
OrganizationUser,
|
||||
)
|
||||
|
||||
|
||||
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, "delete", 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, "update", db_session)
|
||||
|
||||
# Check if user is the last admin and if the new role is not admin
|
||||
statement = select(UserOrganization).where(
|
||||
UserOrganization.org_id == org.id, UserOrganization.role_id == 1
|
||||
)
|
||||
result = db_session.exec(statement)
|
||||
admins = result.all()
|
||||
|
||||
if not admins:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="There is no admin in the organization",
|
||||
)
|
||||
|
||||
if (
|
||||
len(admins) == 1
|
||||
and int(admins[0].user_id) == int(user_id)
|
||||
and str(role_uuid) != "role_global_admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Organization must have at least one admin",
|
||||
)
|
||||
|
||||
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 invite_batch_users(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
emails: str,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
):
|
||||
# Redis init
|
||||
LH_CONFIG = get_learnhouse_config()
|
||||
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
|
||||
|
||||
if not redis_conn_string:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Redis connection string not found",
|
||||
)
|
||||
|
||||
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, "create", db_session)
|
||||
|
||||
# Connect to Redis
|
||||
r = redis.Redis.from_url(redis_conn_string)
|
||||
|
||||
if not r:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Could not connect to Redis",
|
||||
)
|
||||
|
||||
invite_list = emails.split(",")
|
||||
|
||||
# invitations expire after 30 days
|
||||
ttl = int(timedelta(days=365).total_seconds())
|
||||
|
||||
for email in invite_list:
|
||||
email = email.strip()
|
||||
|
||||
# Check if user is already invited
|
||||
invited_user = r.get(f"invited_user:{email}:org:{org.org_uuid}")
|
||||
|
||||
if invited_user:
|
||||
logging.error(f"User {email} already invited")
|
||||
# skip this user
|
||||
continue
|
||||
|
||||
invited_user_object = {
|
||||
"email": email,
|
||||
"org_id": org.id,
|
||||
"pending": True,
|
||||
"email_sent": False,
|
||||
"expires": ttl,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"created_by": current_user.user_uuid,
|
||||
}
|
||||
|
||||
invited_user = r.set(
|
||||
f"invited_user:{email}:org:{org.org_uuid}",
|
||||
json.dumps(invited_user_object),
|
||||
ex=ttl,
|
||||
)
|
||||
|
||||
return {"detail": "Users invited"}
|
||||
|
||||
|
||||
async def get_list_of_invited_users(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
):
|
||||
# Redis init
|
||||
LH_CONFIG = get_learnhouse_config()
|
||||
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
|
||||
|
||||
if not redis_conn_string:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Redis connection string not found",
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
# Connect to Redis
|
||||
r = redis.Redis.from_url(redis_conn_string)
|
||||
|
||||
if not r:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Could not connect to Redis",
|
||||
)
|
||||
|
||||
invited_users = r.keys(f"invited_user:*:org:{org.org_uuid}")
|
||||
|
||||
invited_users_list = []
|
||||
|
||||
for user in invited_users:
|
||||
invited_user = r.get(user)
|
||||
if invited_user:
|
||||
invited_user = json.loads(invited_user.decode("utf-8"))
|
||||
invited_users_list.append(invited_user)
|
||||
|
||||
return invited_users_list
|
||||
|
||||
|
||||
async def remove_invited_user(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
email: str,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
):
|
||||
# Redis init
|
||||
LH_CONFIG = get_learnhouse_config()
|
||||
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
|
||||
|
||||
if not redis_conn_string:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Redis connection string not found",
|
||||
)
|
||||
|
||||
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, "delete", db_session)
|
||||
|
||||
# Connect to Redis
|
||||
r = redis.Redis.from_url(redis_conn_string)
|
||||
|
||||
if not r:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Could not connect to Redis",
|
||||
)
|
||||
|
||||
invited_user = r.get(f"invited_user:{email}:org:{org.org_uuid}")
|
||||
|
||||
if not invited_user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
r.delete(f"invited_user:{email}:org:{org.org_uuid}")
|
||||
|
||||
return {"detail": "User removed"}
|
||||
|
|
@ -3,6 +3,7 @@ from typing import Literal
|
|||
from uuid import uuid4
|
||||
from fastapi import HTTPException, Request, UploadFile, status
|
||||
from sqlmodel import Session, select
|
||||
from src.services.orgs.invites import get_invite_code
|
||||
from src.services.users.avatars import upload_avatar
|
||||
from src.db.roles import Role, RoleRead
|
||||
from src.security.rbac.rbac import (
|
||||
|
|
@ -103,6 +104,27 @@ async def create_user(
|
|||
|
||||
return user
|
||||
|
||||
async def create_user_with_invite(
|
||||
request: Request,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
user_object: UserCreate,
|
||||
org_id: int,
|
||||
invite_code: str,
|
||||
):
|
||||
|
||||
# Check if invite code exists
|
||||
isInviteCodeCorrect = await get_invite_code(request, org_id, invite_code, current_user, db_session)
|
||||
|
||||
if not isInviteCodeCorrect:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invite code is incorrect",
|
||||
)
|
||||
|
||||
user = await create_user(request, db_session, current_user, user_object, org_id)
|
||||
|
||||
return user
|
||||
|
||||
async def create_user_without_org(
|
||||
request: Request,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue