diff --git a/apps/api/migrations/orgconfigs/v0tov1.py b/apps/api/migrations/orgconfigs/orgconfigs_migrations.py similarity index 76% rename from apps/api/migrations/orgconfigs/v0tov1.py rename to apps/api/migrations/orgconfigs/orgconfigs_migrations.py index b010340f..f9d8efe0 100644 --- a/apps/api/migrations/orgconfigs/v0tov1.py +++ b/apps/api/migrations/orgconfigs/orgconfigs_migrations.py @@ -67,3 +67,33 @@ def migrate_v0_to_v1(v0_config): } return v1_config + + +def migrate_to_v1_1(v1_config): + # Start by copying the existing configuration + v1_1_config = v1_config.copy() + + # Update the config version + v1_1_config["config_version"] = "1.1" + + # Add the new 'cloud' object at the end + v1_1_config['cloud'] = { + "plan": "free", + "custom_domain": False + } + + return v1_1_config + +def migrate_to_v1_2(v1_1_config): + v1_2_config = v1_1_config.copy() + + v1_2_config['config_version'] = '1.2' + + # Enable payments for everyone + v1_2_config['features']['payments']['enabled'] = True + + # Only delete stripe_key if it exists + if 'stripe_key' in v1_2_config['features']['payments']: + del v1_2_config['features']['payments']['stripe_key'] + + return v1_2_config \ No newline at end of file diff --git a/apps/api/migrations/orgconfigs/v1_1.py b/apps/api/migrations/orgconfigs/v1_1.py deleted file mode 100644 index e3a3fabb..00000000 --- a/apps/api/migrations/orgconfigs/v1_1.py +++ /dev/null @@ -1,14 +0,0 @@ -def migrate_to_v1_1(v1_config): - # Start by copying the existing configuration - v1_1_config = v1_config.copy() - - # Update the config version - v1_1_config["config_version"] = "1.1" - - # Add the new 'cloud' object at the end - v1_1_config['cloud'] = { - "plan": "free", - "custom_domain": False - } - - return v1_1_config diff --git a/apps/api/src/routers/dev.py b/apps/api/src/routers/dev.py index 5ce8c1b8..52472875 100644 --- a/apps/api/src/routers/dev.py +++ b/apps/api/src/routers/dev.py @@ -1,8 +1,7 @@ from fastapi import APIRouter, Depends from sqlmodel import Session, select from config.config import get_learnhouse_config -from migrations.orgconfigs.v0tov1 import migrate_v0_to_v1 -from migrations.orgconfigs.v1_1 import migrate_to_v1_1 +from migrations.orgconfigs.orgconfigs_migrations import migrate_to_v1_1, migrate_to_v1_2, migrate_v0_to_v1 from src.core.events.database import get_db_session from src.db.organization_config import OrganizationConfig @@ -51,4 +50,22 @@ async def migratev1_1( db_session.add(orgConfig) db_session.commit() + return {"message": "Migration successful"} + +@router.post("/migrate_orgconfig_v1_to_v1.2") +async def migratev1_2( + db_session: Session = Depends(get_db_session), +): + """ + Migrate organization config from v0 to v1 + """ + statement = select(OrganizationConfig) + result = db_session.exec(statement) + + for orgConfig in result: + orgConfig.config = migrate_to_v1_2(orgConfig.config) + + db_session.add(orgConfig) + db_session.commit() + return {"message": "Migration successful"} \ No newline at end of file diff --git a/apps/api/src/services/users/usergroups.py b/apps/api/src/services/users/usergroups.py index acc68ba3..cfada64b 100644 --- a/apps/api/src/services/users/usergroups.py +++ b/apps/api/src/services/users/usergroups.py @@ -16,7 +16,7 @@ from src.db.usergroup_resources import UserGroupResource from src.db.usergroup_user import UserGroupUser from src.db.organizations import Organization from src.db.usergroups import UserGroup, UserGroupCreate, UserGroupRead, UserGroupUpdate -from src.db.users import AnonymousUser, PublicUser, User, UserRead +from src.db.users import AnonymousUser, InternalUser, PublicUser, User, UserRead async def create_usergroup( @@ -275,7 +275,7 @@ async def delete_usergroup_by_id( async def add_users_to_usergroup( request: Request, db_session: Session, - current_user: PublicUser | AnonymousUser, + current_user: PublicUser | AnonymousUser | InternalUser, usergroup_id: int, user_ids: str, ) -> str: @@ -486,10 +486,13 @@ async def remove_resources_from_usergroup( async def rbac_check( request: Request, usergroup_uuid: str, - current_user: PublicUser | AnonymousUser, + current_user: PublicUser | AnonymousUser | InternalUser, action: Literal["create", "read", "update", "delete"], db_session: Session, ): + if isinstance(current_user, InternalUser): + return True + await authorization_verify_if_user_is_anon(current_user.id) await authorization_verify_based_on_roles_and_authorship( diff --git a/apps/api/src/services/users/users.py b/apps/api/src/services/users/users.py index 401f48ab..c23f6df7 100644 --- a/apps/api/src/services/users/users.py +++ b/apps/api/src/services/users/users.py @@ -21,6 +21,7 @@ from src.security.rbac.rbac import ( from src.db.organizations import Organization, OrganizationRead from src.db.users import ( AnonymousUser, + InternalUser, PublicUser, User, UserCreate, @@ -147,19 +148,21 @@ async def create_user_with_invite( # Usage check check_limits_with_usage("members", org_id, db_session) + + + user = await create_user(request, db_session, current_user, user_object, org_id) + # Check if invite code contains UserGroup - if inviteCode.usergroup_id: + if inviteCode.get("usergroup_id"): # Add user to UserGroup await add_users_to_usergroup( request, db_session, - current_user, - inviteCode.usergroup_id, - user_object.username, + InternalUser(id=0), + int(inviteCode.get("usergroup_id")), # Convert to int since usergroup_id is expected to be int + str(user.id), ) - user = await create_user(request, db_session, current_user, user_object, org_id) - increase_feature_usage("members", org_id, db_session) return user diff --git a/apps/web/app/auth/signup/signup.tsx b/apps/web/app/auth/signup/signup.tsx index 71e977e1..8ed3b064 100644 --- a/apps/web/app/auth/signup/signup.tsx +++ b/apps/web/app/auth/signup/signup.tsx @@ -3,7 +3,7 @@ import learnhouseIcon from 'public/learnhouse_bigicon_1.png' import Image from 'next/image' import { getOrgLogoMediaDirectory } from '@services/media/media' import Link from 'next/link' -import { getUriWithOrg } from '@services/config/config' +import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config' import { useLHSession } from '@components/Contexts/LHSessionContext' import React, { useEffect } from 'react' import { MailWarning, Ticket, UserPlus } from 'lucide-react' @@ -191,7 +191,7 @@ const NoTokenScreen = (props: any) => { "Invite code is valid, you'll be redirected to the signup page in a few seconds" ) setTimeout(() => { - router.push(`/signup?inviteCode=${inviteCode}&orgslug=${org.slug}`) + router.push(getUriWithoutOrg(`/signup?inviteCode=${inviteCode}&orgslug=${org.slug}`)) }, 2000) } else { toast.error('Invite code is invalid') diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx index beeb26b0..7c4849ee 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx @@ -74,8 +74,12 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro const removeOption = (qIndex: number, oIndex: number) => { const updatedQuestions = [...questions]; - updatedQuestions[qIndex].options.splice(oIndex, 1); - setQuestions(updatedQuestions); + if (updatedQuestions[qIndex].options.length > 1) { + updatedQuestions[qIndex].options.splice(oIndex, 1); + setQuestions(updatedQuestions); + } else { + toast.error('Cannot delete the last option. At least one option is required.'); + } }; const addQuestion = () => { diff --git a/apps/web/components/Dashboard/Pages/Course/EditCourseGeneral/EditCourseGeneral.tsx b/apps/web/components/Dashboard/Pages/Course/EditCourseGeneral/EditCourseGeneral.tsx index e8c8cbf4..e1403d1c 100644 --- a/apps/web/components/Dashboard/Pages/Course/EditCourseGeneral/EditCourseGeneral.tsx +++ b/apps/web/components/Dashboard/Pages/Course/EditCourseGeneral/EditCourseGeneral.tsx @@ -51,7 +51,7 @@ function EditCourseGeneral(props: EditCourseStructureProps) { about: courseStructure?.about || '', learnings: courseStructure?.learnings || '', tags: courseStructure?.tags || '', - public: courseStructure?.public || '', + public: courseStructure?.public || false, }, validate, onSubmit: async values => { diff --git a/apps/web/components/Dashboard/Pages/Users/OrgAccess/OrgAccess.tsx b/apps/web/components/Dashboard/Pages/Users/OrgAccess/OrgAccess.tsx index b5221e66..ae14fe86 100644 --- a/apps/web/components/Dashboard/Pages/Users/OrgAccess/OrgAccess.tsx +++ b/apps/web/components/Dashboard/Pages/Users/OrgAccess/OrgAccess.tsx @@ -1,7 +1,7 @@ import { useOrg } from '@components/Contexts/OrgContext' import PageLoading from '@components/Objects/Loaders/PageLoading' import ConfirmationModal from '@components/Objects/StyledElements/ConfirmationModal/ConfirmationModal' -import { getAPIUrl, getUriWithOrg } from '@services/config/config' +import { getAPIUrl, getUriWithOrg, getUriWithoutOrg } from '@services/config/config' import { swrFetcher } from '@services/utils/ts/requests' import { Globe, Ticket, UserSquare, Users, X } from 'lucide-react' import Link from 'next/link' @@ -173,14 +173,12 @@ function OrgAccess() { - {getUriWithOrg( - org?.slug, - `/signup?inviteCode=${invite.invite_code}` + {getUriWithoutOrg( + `/signup?inviteCode=${invite.invite_code}&orgslug=${org.slug}` )} diff --git a/apps/web/components/Objects/Activities/DynamicCanva/AI/AICanvaToolkit.tsx b/apps/web/components/Objects/Activities/DynamicCanva/AI/AICanvaToolkit.tsx index 8a9bb0c9..ff9f88e2 100644 --- a/apps/web/components/Objects/Activities/DynamicCanva/AI/AICanvaToolkit.tsx +++ b/apps/web/components/Objects/Activities/DynamicCanva/AI/AICanvaToolkit.tsx @@ -163,7 +163,9 @@ function AIActionButton(props: { }) await dispatchAIChatBot({ type: 'setIsWaitingForResponse' }) const response = await startActivityAIChatSession( - message, access_token + message, + access_token, + props.activity.activity_uuid ) if (response.success == false) { await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' }) diff --git a/apps/web/components/Objects/Courses/CourseActions/CoursesActions.tsx b/apps/web/components/Objects/Courses/CourseActions/CoursesActions.tsx index bbcabe0d..2d69cdb8 100644 --- a/apps/web/components/Objects/Courses/CourseActions/CoursesActions.tsx +++ b/apps/web/components/Objects/Courses/CourseActions/CoursesActions.tsx @@ -6,7 +6,7 @@ import { revalidateTags } from '@services/utils/ts/requests' import { useRouter } from 'next/navigation' import { useLHSession } from '@components/Contexts/LHSessionContext' import { useMediaQuery } from 'usehooks-ts' -import { getUriWithOrg } from '@services/config/config' +import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config' import { getProductsByCourse } from '@services/payments/products' import { LogIn, LogOut, ShoppingCart, AlertCircle } from 'lucide-react' import Modal from '@components/Objects/StyledElements/Modal/Modal' @@ -126,7 +126,7 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => { const handleCourseAction = async () => { if (!session.data?.user) { - router.push(getUriWithOrg(orgslug, '/signup?orgslug=' + orgslug)) + router.push(getUriWithoutOrg(`/signup?orgslug=${orgslug}`)) return } const action = isStarted ? removeCourse : startCourse diff --git a/apps/web/components/Objects/Editor/Extensions/Badges/BadgesExtension.tsx b/apps/web/components/Objects/Editor/Extensions/Badges/BadgesExtension.tsx index 888eb730..41e1a2fc 100644 --- a/apps/web/components/Objects/Editor/Extensions/Badges/BadgesExtension.tsx +++ b/apps/web/components/Objects/Editor/Extensions/Badges/BadgesExtension.tsx @@ -135,13 +135,11 @@ const BadgesExtension: React.FC = (props: any) => { return ( -
-
+
+
{emoji} {isEditable && ( @@ -176,6 +174,7 @@ const BadgesExtension: React.FC = (props: any) => {
)}
+ {isEditable && ( )} + {isEditable && showPredefinedCallouts && ( -
+
{predefinedBadges.map((badge, index) => (