mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: implement usage limits across the app
This commit is contained in:
parent
85ffb44d93
commit
a5fbf49304
15 changed files with 281 additions and 160 deletions
|
|
@ -30,7 +30,7 @@ def migrate_v0_to_v1(v0_config):
|
|||
),
|
||||
},
|
||||
"usergroups": {
|
||||
"enabled": True,
|
||||
"enabled": False,
|
||||
"limit": (
|
||||
v0_config["GeneralConfig"]["limits"]["max_staff"]
|
||||
if v0_config["GeneralConfig"]["limits"]["limits_enabled"]
|
||||
|
|
@ -46,7 +46,7 @@ def migrate_v0_to_v1(v0_config):
|
|||
),
|
||||
},
|
||||
"ai": {
|
||||
"enabled": v0_config["AIConfig"]["enabled"],
|
||||
"enabled": False,
|
||||
"limit": (
|
||||
v0_config["AIConfig"]["limits"]["max_asks"]
|
||||
if v0_config["AIConfig"]["limits"]["limits_enabled"]
|
||||
|
|
@ -54,12 +54,12 @@ def migrate_v0_to_v1(v0_config):
|
|||
),
|
||||
"model": 'gpt-4o-mini',
|
||||
},
|
||||
"assignments": {"enabled": True, "limit": 10},
|
||||
"assignments": {"enabled": True, "limit": 5},
|
||||
"payments": {"enabled": False, "stripe_key": ""},
|
||||
"discussions": {"enabled": False, "limit": 10},
|
||||
"analytics": {"enabled": False, "limit": 10},
|
||||
"collaboration": {
|
||||
"enabled": v0_config["GeneralConfig"]["collaboration"],
|
||||
"enabled": False,
|
||||
"limit": 10,
|
||||
},
|
||||
"api": {"enabled": False, "limit": 10},
|
||||
|
|
|
|||
|
|
@ -92,17 +92,17 @@ async def api_update_activity(
|
|||
)
|
||||
|
||||
|
||||
@router.delete("/{activity_id}")
|
||||
@router.delete("/{activity_uuid}")
|
||||
async def api_delete_activity(
|
||||
request: Request,
|
||||
activity_id: str,
|
||||
activity_uuid: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session=Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Delete activity by activity_id
|
||||
"""
|
||||
return await delete_activity(request, activity_id, current_user, db_session)
|
||||
return await delete_activity(request, activity_uuid, current_user, db_session)
|
||||
|
||||
|
||||
# Video activity
|
||||
|
|
|
|||
141
apps/api/src/security/features_utils/usage.py
Normal file
141
apps/api/src/security/features_utils/usage.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import redis
|
||||
from src.db.organization_config import OrganizationConfig
|
||||
from config.config import get_learnhouse_config
|
||||
from typing import Literal, TypeAlias
|
||||
from fastapi import HTTPException
|
||||
from sqlmodel import Session, select
|
||||
|
||||
FeatureSet: TypeAlias = Literal[
|
||||
"ai",
|
||||
"analytics",
|
||||
"api",
|
||||
"assignments",
|
||||
"collaboration",
|
||||
"courses",
|
||||
"discussions",
|
||||
"members",
|
||||
"payments",
|
||||
"storage",
|
||||
"usergroups",
|
||||
]
|
||||
|
||||
|
||||
def check_limits_with_usage(
|
||||
feature: FeatureSet,
|
||||
org_id: int,
|
||||
db_session: Session,
|
||||
):
|
||||
|
||||
# Get the Organization Config
|
||||
statement = select(OrganizationConfig).where(OrganizationConfig.org_id == org_id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
org_config = result.first()
|
||||
|
||||
if org_config is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Organization has no config",
|
||||
)
|
||||
|
||||
# Check if the Organizations has AI enabled
|
||||
if org_config.config["features"][feature]["enabled"] == False:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"{feature.capitalize()} is not enabled for this organization",
|
||||
)
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
# Connect to Redis
|
||||
r = redis.Redis.from_url(redis_conn_string)
|
||||
|
||||
# Check limits
|
||||
feature_limit = org_config.config["features"][feature]["limit"]
|
||||
|
||||
if feature_limit > 0:
|
||||
# Get the number of feature usage
|
||||
feature_usage = r.get(f"{feature}_usage:{org_id}")
|
||||
|
||||
# Get a number of feature asks
|
||||
if feature_usage is None:
|
||||
feature_usage_count = 0
|
||||
else:
|
||||
feature_usage_count = int(feature_usage) # type: ignore
|
||||
|
||||
# Check if the Number of usage is less than the max_asks limit
|
||||
if feature_limit <= feature_usage_count:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Usage Limit has been reached for {feature.capitalize()}",
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def increase_feature_usage(
|
||||
feature: FeatureSet,
|
||||
org_id: int,
|
||||
db_session: Session,
|
||||
):
|
||||
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",
|
||||
)
|
||||
|
||||
# Connect to Redis
|
||||
r = redis.Redis.from_url(redis_conn_string)
|
||||
|
||||
# Get the number of feature usage
|
||||
feature_usage = r.get(f"{feature}_usage:{org_id}")
|
||||
|
||||
# Get a number of feature asks
|
||||
if feature_usage is None:
|
||||
feature_usage_count = 0
|
||||
else:
|
||||
feature_usage_count = int(feature_usage) # type: ignore
|
||||
|
||||
# Increment the feature usage
|
||||
r.set(f"{feature}_usage:{org_id}", feature_usage_count + 1)
|
||||
return True
|
||||
|
||||
|
||||
def decrease_feature_usage(
|
||||
feature: FeatureSet,
|
||||
org_id: int,
|
||||
db_session: Session,
|
||||
):
|
||||
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",
|
||||
)
|
||||
|
||||
# Connect to Redis
|
||||
r = redis.Redis.from_url(redis_conn_string)
|
||||
|
||||
# Get the number of feature usage
|
||||
feature_usage = r.get(f"{feature}_usage:{org_id}")
|
||||
|
||||
# Get a number of feature asks
|
||||
if feature_usage is None:
|
||||
feature_usage_count = 0
|
||||
else:
|
||||
feature_usage_count = int(feature_usage) # type: ignore
|
||||
|
||||
# Increment the feature usage
|
||||
r.set(f"{feature}_usage:{org_id}", feature_usage_count - 1)
|
||||
return True
|
||||
|
|
@ -2,7 +2,10 @@ from fastapi import Depends, HTTPException, Request
|
|||
from sqlmodel import Session, select
|
||||
from src.db.organization_config import OrganizationConfig
|
||||
from src.db.organizations import Organization
|
||||
from src.services.ai.utils import check_limits_and_config, count_ai_ask
|
||||
from src.security.features_utils.usage import (
|
||||
check_limits_with_usage,
|
||||
increase_feature_usage,
|
||||
)
|
||||
from src.db.courses.courses import Course, CourseRead
|
||||
from src.core.events.database import get_db_session
|
||||
from src.db.users import PublicUser
|
||||
|
|
@ -52,9 +55,15 @@ def ai_start_activity_chat_session(
|
|||
statement = select(Organization).where(Organization.id == course.org_id)
|
||||
org = db_session.exec(statement).first()
|
||||
|
||||
if not org or org.id is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Organization not found",
|
||||
)
|
||||
|
||||
# Check limits and usage
|
||||
check_limits_and_config(db_session, org) # type: ignore
|
||||
count_ai_ask(org, "increment") # type: ignore
|
||||
check_limits_with_usage("ai", org.id, db_session)
|
||||
increase_feature_usage("ai", org.id, db_session)
|
||||
|
||||
if not activity:
|
||||
raise HTTPException(
|
||||
|
|
@ -147,8 +156,8 @@ def ai_send_activity_chat_message(
|
|||
org = db_session.exec(statement).first()
|
||||
|
||||
# Check limits and usage
|
||||
check_limits_and_config(db_session, org) # type: ignore
|
||||
count_ai_ask(org, "increment") # type: ignore
|
||||
check_limits_with_usage("ai", course.org_id, db_session)
|
||||
increase_feature_usage("ai", course.org_id, db_session)
|
||||
|
||||
if not activity:
|
||||
raise HTTPException(
|
||||
|
|
|
|||
|
|
@ -1,114 +0,0 @@
|
|||
from typing import Literal
|
||||
import redis
|
||||
from fastapi import HTTPException
|
||||
from sqlmodel import Session, select
|
||||
from config.config import get_learnhouse_config
|
||||
from src.db.organization_config import OrganizationConfig
|
||||
from src.db.organizations import Organization
|
||||
|
||||
|
||||
def count_ai_ask(
|
||||
organization: Organization,
|
||||
operation: Literal["increment", "decrement"],
|
||||
):
|
||||
"""
|
||||
Count the number of AI asks
|
||||
"""
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
# 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 the number of AI asks
|
||||
ai_asks = r.get(f"ai_asks:{organization.org_uuid}")
|
||||
|
||||
if ai_asks is None:
|
||||
ai_asks = 0
|
||||
|
||||
# Increment or decrement the number of AI asks
|
||||
if operation == "increment":
|
||||
ai_asks = int(ai_asks) + 1
|
||||
elif operation == "decrement":
|
||||
ai_asks = int(ai_asks) - 1
|
||||
|
||||
# Update the number of AI asks
|
||||
r.set(f"ai_asks:{organization.org_uuid}", ai_asks)
|
||||
|
||||
# Set the expiration time to 30 days
|
||||
r.expire(f"ai_asks:{organization.org_uuid}", 2592000)
|
||||
|
||||
|
||||
def check_limits_and_config(db_session: Session, organization: Organization):
|
||||
"""
|
||||
Check the limits and config of an Organization
|
||||
"""
|
||||
|
||||
# Get the Organization Config
|
||||
statement = select(OrganizationConfig).where(
|
||||
OrganizationConfig.org_id == organization.id
|
||||
)
|
||||
result = db_session.exec(statement)
|
||||
org_config = result.first()
|
||||
|
||||
if org_config is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Organization has no config",
|
||||
)
|
||||
|
||||
# Check if the Organizations has AI enabled
|
||||
if org_config.config["features"]["ai"]["enabled"] == False:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Organization has AI disabled",
|
||||
)
|
||||
|
||||
# Check if the Organization has Limits enabled and if the max_asks limit has been reached
|
||||
if org_config.config["features"]["ai"]["limit"] > 0:
|
||||
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",
|
||||
)
|
||||
|
||||
# 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 the number of AI asks
|
||||
ai_asks = r.get(f"ai_asks:{organization.org_uuid}")
|
||||
|
||||
# Get a number of AI asks
|
||||
if ai_asks is None:
|
||||
ai_asks = 0
|
||||
else:
|
||||
ai_asks = int(ai_asks)
|
||||
|
||||
# Check if the Number of asks is less than the max_asks limit
|
||||
if org_config.config["features"]["ai"]["limit"] <= ai_asks:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Organization has reached the max number of AI asks",
|
||||
)
|
||||
|
|
@ -28,6 +28,11 @@ from src.db.organizations import Organization
|
|||
from src.db.trail_runs import TrailRun
|
||||
from src.db.trail_steps import TrailStep
|
||||
from src.db.users import AnonymousUser, PublicUser, User
|
||||
from src.security.features_utils.usage import (
|
||||
check_limits_with_usage,
|
||||
decrease_feature_usage,
|
||||
increase_feature_usage,
|
||||
)
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship_and_usergroups,
|
||||
authorization_verify_if_element_is_public,
|
||||
|
|
@ -61,6 +66,9 @@ async def create_assignment(
|
|||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# Usage check
|
||||
check_limits_with_usage("assignments", course.org_id, db_session)
|
||||
|
||||
# Create Assignment
|
||||
assignment = Assignment(**assignment_object.model_dump())
|
||||
|
||||
|
|
@ -74,6 +82,9 @@ async def create_assignment(
|
|||
db_session.commit()
|
||||
db_session.refresh(assignment)
|
||||
|
||||
# Feature usage
|
||||
increase_feature_usage("assignments", course.org_id, db_session)
|
||||
|
||||
# return assignment read
|
||||
return AssignmentRead.model_validate(assignment)
|
||||
|
||||
|
|
@ -228,6 +239,9 @@ async def delete_assignment(
|
|||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Feature usage
|
||||
decrease_feature_usage("assignments", course.org_id, db_session)
|
||||
|
||||
# Delete Assignment
|
||||
db_session.delete(assignment)
|
||||
db_session.commit()
|
||||
|
|
@ -275,6 +289,9 @@ async def delete_assignment_from_activity_uuid(
|
|||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Feature usage
|
||||
decrease_feature_usage("assignments", course.org_id, db_session)
|
||||
|
||||
# Delete Assignment
|
||||
db_session.delete(assignment)
|
||||
|
||||
|
|
@ -1119,9 +1136,9 @@ async def create_assignment_submission(
|
|||
# Add TrailStep
|
||||
trail = await check_trail_presence(
|
||||
org_id=course.org_id,
|
||||
user_id=user.id,
|
||||
user_id=user.id, # type: ignore
|
||||
request=request,
|
||||
user=user,
|
||||
user=user, # type: ignore
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
|
|
@ -1137,7 +1154,7 @@ async def create_assignment_submission(
|
|||
trail_id=trail.id if trail.id is not None else 0,
|
||||
course_id=course.id if course.id is not None else 0,
|
||||
org_id=course.org_id,
|
||||
user_id=user.id,
|
||||
user_id=user.id, # type: ignore
|
||||
creation_date=str(datetime.now()),
|
||||
update_date=str(datetime.now()),
|
||||
)
|
||||
|
|
@ -1162,7 +1179,7 @@ async def create_assignment_submission(
|
|||
complete=True,
|
||||
teacher_verified=False,
|
||||
grade="",
|
||||
user_id=user.id,
|
||||
user_id=user.id, # type: ignore
|
||||
creation_date=str(datetime.now()),
|
||||
update_date=str(datetime.now()),
|
||||
)
|
||||
|
|
@ -1410,7 +1427,6 @@ async def grade_assignment_submission(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Check if assignment user submission exists
|
||||
|
|
@ -1552,7 +1568,6 @@ async def mark_activity_as_done_for_user(
|
|||
detail="Course not found",
|
||||
)
|
||||
|
||||
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
if not activity:
|
||||
|
|
@ -1596,6 +1611,7 @@ async def mark_activity_as_done_for_user(
|
|||
# return OK
|
||||
return {"message": "Activity marked as done for user"}
|
||||
|
||||
|
||||
async def get_assignments_from_course(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ from sqlmodel import Session, select
|
|||
from src.db.usergroup_resources import UserGroupResource
|
||||
from src.db.usergroup_user import UserGroupUser
|
||||
from src.db.organizations import Organization
|
||||
from src.security.features_utils.usage import (
|
||||
check_limits_with_usage,
|
||||
decrease_feature_usage,
|
||||
increase_feature_usage,
|
||||
)
|
||||
from src.services.trail.trail import get_user_trail_with_orgid
|
||||
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
|
||||
from src.db.users import PublicUser, AnonymousUser, User, UserRead
|
||||
|
|
@ -58,6 +63,7 @@ async def get_course(
|
|||
|
||||
return course
|
||||
|
||||
|
||||
async def get_course_by_id(
|
||||
request: Request,
|
||||
course_id: str,
|
||||
|
|
@ -91,6 +97,7 @@ async def get_course_by_id(
|
|||
|
||||
return course
|
||||
|
||||
|
||||
async def get_course_meta(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
|
|
@ -158,6 +165,9 @@ async def create_course(
|
|||
# RBAC check
|
||||
await rbac_check(request, "course_x", current_user, "create", db_session)
|
||||
|
||||
# Usage check
|
||||
check_limits_with_usage("courses", org_id, db_session)
|
||||
|
||||
# Complete course object
|
||||
course.org_id = course.org_id
|
||||
|
||||
|
|
@ -207,6 +217,9 @@ async def create_course(
|
|||
)
|
||||
authors = db_session.exec(authors_statement).all()
|
||||
|
||||
# Feature usage
|
||||
increase_feature_usage("courses", course.org_id, db_session)
|
||||
|
||||
# convert from User to UserRead
|
||||
authors = [UserRead.model_validate(author) for author in authors]
|
||||
|
||||
|
|
@ -344,6 +357,9 @@ async def delete_course(
|
|||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
# Feature usage
|
||||
decrease_feature_usage("courses", course.org_id, db_session)
|
||||
|
||||
db_session.delete(course)
|
||||
db_session.commit()
|
||||
|
||||
|
|
@ -372,7 +388,7 @@ async def get_courses_orgslug(
|
|||
statement_author = (
|
||||
select(Course)
|
||||
.join(Organization)
|
||||
.join(ResourceAuthor, ResourceAuthor.user_id == current_user.id)
|
||||
.join(ResourceAuthor, ResourceAuthor.user_id == current_user.id) # type: ignore
|
||||
.where(
|
||||
Organization.slug == org_slug,
|
||||
ResourceAuthor.resource_uuid == Course.course_uuid,
|
||||
|
|
@ -383,9 +399,9 @@ async def get_courses_orgslug(
|
|||
statement_usergroup = (
|
||||
select(Course)
|
||||
.join(Organization)
|
||||
.join(UserGroupResource, UserGroupResource.resource_uuid == Course.course_uuid)
|
||||
.join(UserGroupResource, UserGroupResource.resource_uuid == Course.course_uuid) # type: ignore
|
||||
.join(
|
||||
UserGroupUser, UserGroupUser.usergroup_id == UserGroupResource.usergroup_id
|
||||
UserGroupUser, UserGroupUser.usergroup_id == UserGroupResource.usergroup_id # type: ignore
|
||||
)
|
||||
.where(Organization.slug == org_slug, UserGroupUser.user_id == current_user.id)
|
||||
)
|
||||
|
|
@ -396,12 +412,11 @@ async def get_courses_orgslug(
|
|||
).subquery()
|
||||
|
||||
# TODO: migrate this to exec
|
||||
courses = db_session.execute(select(statement_complete)).all()
|
||||
courses = db_session.execute(select(statement_complete)).all() # type: ignore
|
||||
|
||||
# TODO: I have no idea why this is necessary, but it is
|
||||
courses = [CourseRead(**course._asdict(), authors=[]) for course in courses]
|
||||
|
||||
|
||||
# for every course, get the authors
|
||||
for course in courses:
|
||||
authors_statement = (
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ from sqlmodel import Session, select
|
|||
from src.db.organizations import Organization
|
||||
from src.db.user_organizations import UserOrganization
|
||||
from src.db.users import AnonymousUser, PublicUser, User
|
||||
from src.security.features_utils.usage import (
|
||||
check_limits_with_usage,
|
||||
increase_feature_usage,
|
||||
)
|
||||
from src.services.orgs.invites import get_invite_code
|
||||
from src.services.orgs.orgs import get_org_join_mechanism
|
||||
|
||||
|
|
@ -27,12 +31,14 @@ async def join_org(
|
|||
|
||||
org = result.first()
|
||||
|
||||
if not org:
|
||||
if not org or org.id is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Organization not found",
|
||||
)
|
||||
|
||||
check_limits_with_usage("members", org.id, db_session)
|
||||
|
||||
join_method = await get_org_join_mechanism(
|
||||
request, args.org_id, current_user, db_session
|
||||
)
|
||||
|
|
@ -104,6 +110,8 @@ async def join_org(
|
|||
db_session.add(user_organization)
|
||||
db_session.commit()
|
||||
|
||||
increase_feature_usage("members", org.id, db_session)
|
||||
|
||||
return "Great, You're part of the Organization"
|
||||
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import logging
|
|||
import redis
|
||||
from fastapi import HTTPException, Request
|
||||
from sqlmodel import Session, select
|
||||
from src.security.features_utils.usage import decrease_feature_usage
|
||||
from src.services.orgs.invites import send_invite_email
|
||||
from config.config import get_learnhouse_config
|
||||
from src.services.orgs.orgs import rbac_check
|
||||
|
|
@ -147,6 +148,8 @@ async def remove_user_from_org(
|
|||
db_session.delete(user_org)
|
||||
db_session.commit()
|
||||
|
||||
decrease_feature_usage("members", org_id, db_session)
|
||||
|
||||
return {"detail": "User removed from org"}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ from typing import Literal
|
|||
from uuid import uuid4
|
||||
from fastapi import HTTPException, Request
|
||||
from sqlmodel import Session, select
|
||||
from src.security.features_utils.usage import (
|
||||
check_limits_with_usage,
|
||||
increase_feature_usage,
|
||||
)
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship_and_usergroups,
|
||||
authorization_verify_if_user_is_anon,
|
||||
|
|
@ -35,14 +39,17 @@ async def create_usergroup(
|
|||
|
||||
# Check if Organization exists
|
||||
statement = select(Organization).where(Organization.id == usergroup_create.org_id)
|
||||
result = db_session.exec(statement)
|
||||
org = db_session.exec(statement).first()
|
||||
|
||||
if not result.first():
|
||||
if not org or org.id is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Organization does not exist",
|
||||
)
|
||||
|
||||
# Usage check
|
||||
check_limits_with_usage("courses", org.id, db_session)
|
||||
|
||||
# Complete the object
|
||||
usergroup.usergroup_uuid = f"usergroup_{uuid4()}"
|
||||
usergroup.creation_date = str(datetime.now())
|
||||
|
|
@ -53,6 +60,9 @@ async def create_usergroup(
|
|||
db_session.commit()
|
||||
db_session.refresh(usergroup)
|
||||
|
||||
# Feature usage
|
||||
increase_feature_usage("usergroups", org.id, db_session)
|
||||
|
||||
usergroup = UserGroupRead.model_validate(usergroup)
|
||||
|
||||
return usergroup
|
||||
|
|
@ -253,6 +263,9 @@ async def delete_usergroup_by_id(
|
|||
db_session=db_session,
|
||||
)
|
||||
|
||||
# Feature usage
|
||||
increase_feature_usage("usergroups", usergroup.org_id, db_session)
|
||||
|
||||
db_session.delete(usergroup)
|
||||
db_session.commit()
|
||||
|
||||
|
|
@ -276,8 +289,6 @@ async def add_users_to_usergroup(
|
|||
detail="UserGroup not found",
|
||||
)
|
||||
|
||||
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(
|
||||
request,
|
||||
|
|
@ -353,7 +364,9 @@ async def remove_users_from_usergroup(
|
|||
user_ids_array = user_ids.split(",")
|
||||
|
||||
for user_id in user_ids_array:
|
||||
statement = select(UserGroupUser).where(UserGroupUser.user_id == user_id, UserGroupUser.usergroup_id == usergroup_id)
|
||||
statement = select(UserGroupUser).where(
|
||||
UserGroupUser.user_id == user_id, UserGroupUser.usergroup_id == usergroup_id
|
||||
)
|
||||
usergroup_user = db_session.exec(statement).first()
|
||||
|
||||
if usergroup_user:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ from typing import Literal
|
|||
from uuid import uuid4
|
||||
from fastapi import HTTPException, Request, UploadFile, status
|
||||
from sqlmodel import Session, select
|
||||
from src.security.features_utils.usage import (
|
||||
check_limits_with_usage,
|
||||
increase_feature_usage,
|
||||
)
|
||||
from src.services.users.usergroups import add_users_to_usergroup
|
||||
from src.services.users.emails import (
|
||||
send_account_creation_email,
|
||||
|
|
@ -61,6 +65,9 @@ async def create_user(
|
|||
detail="Organization does not exist",
|
||||
)
|
||||
|
||||
# Usage check
|
||||
check_limits_with_usage("members", org_id, db_session)
|
||||
|
||||
# Username
|
||||
statement = select(User).where(User.username == user.username)
|
||||
result = db_session.exec(statement)
|
||||
|
|
@ -106,6 +113,8 @@ async def create_user(
|
|||
|
||||
user = UserRead.model_validate(user)
|
||||
|
||||
increase_feature_usage("members", org_id, db_session)
|
||||
|
||||
# Send Account creation email
|
||||
send_account_creation_email(
|
||||
user=user,
|
||||
|
|
@ -135,6 +144,9 @@ async def create_user_with_invite(
|
|||
detail="Invite code is incorrect",
|
||||
)
|
||||
|
||||
# Usage check
|
||||
check_limits_with_usage("members", org_id, db_session)
|
||||
|
||||
# Check if invite code contains UserGroup
|
||||
if inviteCode.usergroup_id:
|
||||
# Add user to UserGroup
|
||||
|
|
@ -148,6 +160,8 @@ async def create_user_with_invite(
|
|||
|
||||
user = await create_user(request, db_session, current_user, user_object, org_id)
|
||||
|
||||
increase_feature_usage("members", org_id, db_session)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ import { getAPIUrl } from '@services/config/config'
|
|||
import { mutate } from 'swr'
|
||||
import { createAssignment } from '@services/courses/assignments'
|
||||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||
import { createActivity } from '@services/courses/activities'
|
||||
import { createActivity, deleteActivity } from '@services/courses/activities'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
function NewAssignment({ submitActivity, chapterId, course, closeModal }: any) {
|
||||
const org = useOrg() as any;
|
||||
|
|
@ -55,7 +56,7 @@ function NewAssignment({ submitActivity, chapterId, course, closeModal }: any) {
|
|||
}
|
||||
|
||||
const activity_res = await createActivity(activity, chapterId, org?.id, session.data?.tokens?.access_token)
|
||||
await createAssignment({
|
||||
const res = await createAssignment({
|
||||
title: activityName,
|
||||
description: activityDescription,
|
||||
due_date: dueDate,
|
||||
|
|
@ -66,6 +67,14 @@ function NewAssignment({ submitActivity, chapterId, course, closeModal }: any) {
|
|||
activity_id: activity_res?.id,
|
||||
}, session.data?.tokens?.access_token)
|
||||
|
||||
if (res.success) {
|
||||
toast.success('Assignment created successfully')
|
||||
} else {
|
||||
toast.error(res.data.detail)
|
||||
await deleteActivity(activity_res.activity_uuid, session.data?.tokens?.access_token)
|
||||
|
||||
}
|
||||
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||
setIsSubmitting(false)
|
||||
closeModal()
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { BarLoader } from 'react-spinners'
|
|||
import { revalidateTags } from '@services/utils/ts/requests'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
function CreateCourseModal({ closeModal, orgslug }: any) {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
|
@ -69,21 +70,27 @@ function CreateCourseModal({ closeModal, orgslug }: any) {
|
|||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
|
||||
let status = await createNewCourse(
|
||||
let res = await createNewCourse(
|
||||
orgId,
|
||||
{ name, description, tags, visibility },
|
||||
thumbnail,
|
||||
session.data?.tokens?.access_token
|
||||
)
|
||||
if (res.success) {
|
||||
await revalidateTags(['courses'], orgslug)
|
||||
setIsSubmitting(false)
|
||||
toast.success('Course created successfully')
|
||||
|
||||
if (status.org_id == orgId) {
|
||||
if (res.data.org_id == orgId) {
|
||||
closeModal()
|
||||
router.refresh()
|
||||
await revalidateTags(['courses'], orgslug)
|
||||
} else {
|
||||
alert('Error creating course, please see console logs')
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
setIsSubmitting(false)
|
||||
toast.error(res.data.detail)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -99,9 +99,9 @@ export async function getActivityByID(
|
|||
return res
|
||||
}
|
||||
|
||||
export async function deleteActivity(activity_id: any, access_token: string) {
|
||||
export async function deleteActivity(activity_uuid: any, access_token: string) {
|
||||
const result = await fetch(
|
||||
`${getAPIUrl()}activities/${activity_id}`,
|
||||
`${getAPIUrl()}activities/${activity_uuid}`,
|
||||
RequestBodyWithAuthHeader('DELETE', null, null, access_token)
|
||||
)
|
||||
const res = await result.json()
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export async function createNewCourse(
|
|||
`${getAPIUrl()}courses/?org_id=${org_id}`,
|
||||
RequestBodyFormWithAuthHeader('POST', formData, null, access_token)
|
||||
)
|
||||
const res = await errorHandling(result)
|
||||
const res = await getResponseMetadata(result)
|
||||
return res
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue