mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
Merge pull request #155 from learnhouse/feat/usergroups
Implement UserGroups
This commit is contained in:
commit
e35487648e
41 changed files with 1970 additions and 149 deletions
|
|
@ -19,6 +19,7 @@ class Permission(BaseModel):
|
||||||
class Rights(BaseModel):
|
class Rights(BaseModel):
|
||||||
courses: Permission
|
courses: Permission
|
||||||
users: Permission
|
users: Permission
|
||||||
|
usergroups : Permission
|
||||||
collections: Permission
|
collections: Permission
|
||||||
organizations: Permission
|
organizations: Permission
|
||||||
coursechapters: Permission
|
coursechapters: Permission
|
||||||
|
|
|
||||||
16
apps/api/src/db/usergroup_resources.py
Normal file
16
apps/api/src/db/usergroup_resources.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
from typing import Optional
|
||||||
|
from sqlalchemy import Column, ForeignKey, Integer
|
||||||
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class UserGroupResource(SQLModel, table=True):
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
usergroup_id: int = Field(
|
||||||
|
sa_column=Column(Integer, ForeignKey("usergroup.id", ondelete="CASCADE"))
|
||||||
|
)
|
||||||
|
resource_uuid: str = ""
|
||||||
|
org_id: int = Field(
|
||||||
|
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||||
|
)
|
||||||
|
creation_date: str = ""
|
||||||
|
update_date: str = ""
|
||||||
18
apps/api/src/db/usergroup_user.py
Normal file
18
apps/api/src/db/usergroup_user.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from typing import Optional
|
||||||
|
from sqlalchemy import Column, ForeignKey, Integer
|
||||||
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class UserGroupUser(SQLModel, table=True):
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
usergroup_id: int = Field(
|
||||||
|
sa_column=Column(Integer, ForeignKey("usergroup.id", ondelete="CASCADE"))
|
||||||
|
)
|
||||||
|
user_id: int = Field(
|
||||||
|
sa_column=Column(Integer, ForeignKey("user.id", ondelete="CASCADE"))
|
||||||
|
)
|
||||||
|
org_id: int = Field(
|
||||||
|
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||||
|
)
|
||||||
|
creation_date: str = ""
|
||||||
|
update_date: str = ""
|
||||||
33
apps/api/src/db/usergroups.py
Normal file
33
apps/api/src/db/usergroups.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
from typing import Optional
|
||||||
|
from sqlalchemy import Column, ForeignKey, Integer
|
||||||
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class UserGroupBase(SQLModel):
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
class UserGroup(UserGroupBase, table=True):
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
org_id: int = Field(
|
||||||
|
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||||
|
)
|
||||||
|
usergroup_uuid: str = ""
|
||||||
|
creation_date: str = ""
|
||||||
|
update_date: str = ""
|
||||||
|
|
||||||
|
class UserGroupCreate(UserGroupBase):
|
||||||
|
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||||
|
pass
|
||||||
|
|
||||||
|
class UserGroupUpdate(UserGroupBase):
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
class UserGroupRead(UserGroupBase):
|
||||||
|
id: int
|
||||||
|
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||||
|
usergroup_uuid: str
|
||||||
|
creation_date: str
|
||||||
|
update_date: str
|
||||||
|
pass
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
from src.routers import usergroups
|
||||||
from src.routers import blocks, dev, trail, users, auth, orgs, roles
|
from src.routers import blocks, dev, trail, users, auth, orgs, roles
|
||||||
from src.routers.ai import ai
|
from src.routers.ai import ai
|
||||||
from src.routers.courses import chapters, collections, courses, activities
|
from src.routers.courses import chapters, collections, courses, activities
|
||||||
|
|
@ -12,6 +13,7 @@ v1_router = APIRouter(prefix="/api/v1")
|
||||||
|
|
||||||
# API Routes
|
# API Routes
|
||||||
v1_router.include_router(users.router, prefix="/users", tags=["users"])
|
v1_router.include_router(users.router, prefix="/users", tags=["users"])
|
||||||
|
v1_router.include_router(usergroups.router, prefix="/usergroups", tags=["usergroups"])
|
||||||
v1_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
v1_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||||
v1_router.include_router(orgs.router, prefix="/orgs", tags=["orgs"])
|
v1_router.include_router(orgs.router, prefix="/orgs", tags=["orgs"])
|
||||||
v1_router.include_router(roles.router, prefix="/roles", tags=["roles"])
|
v1_router.include_router(roles.router, prefix="/roles", tags=["roles"])
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from fastapi import APIRouter, Depends, Request, UploadFile
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
from src.services.orgs.invites import (
|
from src.services.orgs.invites import (
|
||||||
create_invite_code,
|
create_invite_code,
|
||||||
|
create_invite_code_with_usergroup,
|
||||||
delete_invite_code,
|
delete_invite_code,
|
||||||
get_invite_code,
|
get_invite_code,
|
||||||
get_invite_codes,
|
get_invite_codes,
|
||||||
|
|
@ -162,6 +163,22 @@ async def api_create_invite_code(
|
||||||
return await create_invite_code(request, org_id, current_user, db_session)
|
return await create_invite_code(request, org_id, current_user, db_session)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{org_id}/invites_with_usergroups")
|
||||||
|
async def api_create_invite_code_with_ug(
|
||||||
|
request: Request,
|
||||||
|
org_id: int,
|
||||||
|
usergroup_id: int,
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create invite code
|
||||||
|
"""
|
||||||
|
return await create_invite_code_with_usergroup(
|
||||||
|
request, org_id, usergroup_id, current_user, db_session
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{org_id}/invites")
|
@router.get("/{org_id}/invites")
|
||||||
async def api_get_invite_codes(
|
async def api_get_invite_codes(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
|
||||||
197
apps/api/src/routers/usergroups.py
Normal file
197
apps/api/src/routers/usergroups.py
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
from fastapi import APIRouter, Depends, Request
|
||||||
|
from sqlmodel import Session
|
||||||
|
from src.db.usergroups import UserGroupCreate, UserGroupRead, UserGroupUpdate
|
||||||
|
from src.db.users import PublicUser, UserRead
|
||||||
|
from src.services.users.usergroups import (
|
||||||
|
add_resources_to_usergroup,
|
||||||
|
add_users_to_usergroup,
|
||||||
|
create_usergroup,
|
||||||
|
delete_usergroup_by_id,
|
||||||
|
get_usergroups_by_resource,
|
||||||
|
get_users_linked_to_usergroup,
|
||||||
|
read_usergroup_by_id,
|
||||||
|
read_usergroups_by_org_id,
|
||||||
|
remove_resources_from_usergroup,
|
||||||
|
remove_users_from_usergroup,
|
||||||
|
update_usergroup_by_id,
|
||||||
|
)
|
||||||
|
from src.security.auth import get_current_user
|
||||||
|
from src.core.events.database import get_db_session
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=UserGroupRead, tags=["usergroups"])
|
||||||
|
async def api_create_usergroup(
|
||||||
|
*,
|
||||||
|
request: Request,
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
usergroup_object: UserGroupCreate,
|
||||||
|
) -> UserGroupRead:
|
||||||
|
"""
|
||||||
|
Create User
|
||||||
|
"""
|
||||||
|
return await create_usergroup(request, db_session, current_user, usergroup_object)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{usergroup_id}", response_model=UserGroupRead, tags=["usergroups"])
|
||||||
|
async def api_get_usergroup(
|
||||||
|
*,
|
||||||
|
request: Request,
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
usergroup_id: int,
|
||||||
|
) -> UserGroupRead:
|
||||||
|
"""
|
||||||
|
Get UserGroup
|
||||||
|
"""
|
||||||
|
return await read_usergroup_by_id(request, db_session, current_user, usergroup_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{usergroup_id}/users", response_model=list[UserRead], tags=["usergroups"])
|
||||||
|
async def api_get_users_linked_to_usergroup(
|
||||||
|
*,
|
||||||
|
request: Request,
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
usergroup_id: int,
|
||||||
|
) -> list[UserRead]:
|
||||||
|
"""
|
||||||
|
Get Users linked to UserGroup
|
||||||
|
"""
|
||||||
|
return await get_users_linked_to_usergroup(
|
||||||
|
request, db_session, current_user, usergroup_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/org/{org_id}", response_model=list[UserGroupRead], tags=["usergroups"])
|
||||||
|
async def api_get_usergroups(
|
||||||
|
*,
|
||||||
|
request: Request,
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
org_id: int,
|
||||||
|
) -> list[UserGroupRead]:
|
||||||
|
"""
|
||||||
|
Get UserGroups by Org
|
||||||
|
"""
|
||||||
|
return await read_usergroups_by_org_id(request, db_session, current_user, org_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/resource/{resource_uuid}", response_model=list[UserGroupRead], tags=["usergroups"]
|
||||||
|
)
|
||||||
|
async def api_get_usergroupsby_resource(
|
||||||
|
*,
|
||||||
|
request: Request,
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
resource_uuid: str,
|
||||||
|
) -> list[UserGroupRead]:
|
||||||
|
"""
|
||||||
|
Get UserGroups by Org
|
||||||
|
"""
|
||||||
|
return await get_usergroups_by_resource(
|
||||||
|
request, db_session, current_user, resource_uuid
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{usergroup_id}", response_model=UserGroupRead, tags=["usergroups"])
|
||||||
|
async def api_update_usergroup(
|
||||||
|
*,
|
||||||
|
request: Request,
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
usergroup_id: int,
|
||||||
|
usergroup_object: UserGroupUpdate,
|
||||||
|
) -> UserGroupRead:
|
||||||
|
"""
|
||||||
|
Update UserGroup
|
||||||
|
"""
|
||||||
|
return await update_usergroup_by_id(
|
||||||
|
request, db_session, current_user, usergroup_id, usergroup_object
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{usergroup_id}", tags=["usergroups"])
|
||||||
|
async def api_delete_usergroup(
|
||||||
|
*,
|
||||||
|
request: Request,
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
usergroup_id: int,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Delete UserGroup
|
||||||
|
"""
|
||||||
|
return await delete_usergroup_by_id(request, db_session, current_user, usergroup_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{usergroup_id}/add_users", tags=["usergroups"])
|
||||||
|
async def api_add_users_to_usergroup(
|
||||||
|
*,
|
||||||
|
request: Request,
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
usergroup_id: int,
|
||||||
|
user_ids: str,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Add Users to UserGroup
|
||||||
|
"""
|
||||||
|
return await add_users_to_usergroup(
|
||||||
|
request, db_session, current_user, usergroup_id, user_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{usergroup_id}/remove_users", tags=["usergroups"])
|
||||||
|
async def api_delete_users_from_usergroup(
|
||||||
|
*,
|
||||||
|
request: Request,
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
usergroup_id: int,
|
||||||
|
user_ids: str,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Delete Users from UserGroup
|
||||||
|
"""
|
||||||
|
return await remove_users_from_usergroup(
|
||||||
|
request, db_session, current_user, usergroup_id, user_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{usergroup_id}/add_resources", tags=["usergroups"])
|
||||||
|
async def api_add_resources_to_usergroup(
|
||||||
|
*,
|
||||||
|
request: Request,
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
usergroup_id: int,
|
||||||
|
resource_uuids: str,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Add Resources to UserGroup
|
||||||
|
"""
|
||||||
|
return await add_resources_to_usergroup(
|
||||||
|
request, db_session, current_user, usergroup_id, resource_uuids
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{usergroup_id}/remove_resources", tags=["usergroups"])
|
||||||
|
async def api_delete_resources_from_usergroup(
|
||||||
|
*,
|
||||||
|
request: Request,
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
usergroup_id: int,
|
||||||
|
resource_uuids: str,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Delete Resources from UserGroup
|
||||||
|
"""
|
||||||
|
return await remove_resources_from_usergroup(
|
||||||
|
request, db_session, current_user, usergroup_id, resource_uuids
|
||||||
|
)
|
||||||
|
|
@ -143,7 +143,7 @@ async def authorization_verify_based_on_org_admin_status(
|
||||||
|
|
||||||
|
|
||||||
# Tested and working
|
# Tested and working
|
||||||
async def authorization_verify_based_on_roles_and_authorship(
|
async def authorization_verify_based_on_roles_and_authorship_and_usergroups(
|
||||||
request: Request,
|
request: Request,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
action: Literal["read", "update", "delete", "create"],
|
action: Literal["read", "update", "delete", "create"],
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,27 @@
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
|
|
||||||
async def check_element_type(element_id):
|
async def check_element_type(element_uuid):
|
||||||
"""
|
"""
|
||||||
Check if the element is a course, a user, a house or a collection, by checking its prefix
|
Check if the element is a course, a user, a house or a collection, by checking its prefix
|
||||||
"""
|
"""
|
||||||
if element_id.startswith("course_"):
|
if element_uuid.startswith("course_"):
|
||||||
return "courses"
|
return "courses"
|
||||||
elif element_id.startswith("user_"):
|
elif element_uuid.startswith("user_"):
|
||||||
return "users"
|
return "users"
|
||||||
elif element_id.startswith("house_"):
|
elif element_uuid.startswith("usergroup_"):
|
||||||
|
return "usergroups"
|
||||||
|
elif element_uuid.startswith("house_"):
|
||||||
return "houses"
|
return "houses"
|
||||||
elif element_id.startswith("org_"):
|
elif element_uuid.startswith("org_"):
|
||||||
return "organizations"
|
return "organizations"
|
||||||
elif element_id.startswith("chapter_"):
|
elif element_uuid.startswith("chapter_"):
|
||||||
return "coursechapters"
|
return "coursechapters"
|
||||||
elif element_id.startswith("collection_"):
|
elif element_uuid.startswith("collection_"):
|
||||||
return "collections"
|
return "collections"
|
||||||
elif element_id.startswith("activity_"):
|
elif element_uuid.startswith("activity_"):
|
||||||
return "activities"
|
return "activities"
|
||||||
elif element_id.startswith("role_"):
|
elif element_uuid.startswith("role_"):
|
||||||
return "roles"
|
return "roles"
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -28,8 +30,8 @@ async def check_element_type(element_id):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_singular_form_of_element(element_id):
|
async def get_singular_form_of_element(element_uuid):
|
||||||
element_type = await check_element_type(element_id)
|
element_type = await check_element_type(element_uuid)
|
||||||
|
|
||||||
if element_type == "activities":
|
if element_type == "activities":
|
||||||
return "activity"
|
return "activity"
|
||||||
|
|
@ -38,8 +40,8 @@ async def get_singular_form_of_element(element_id):
|
||||||
return singular_form_element
|
return singular_form_element
|
||||||
|
|
||||||
|
|
||||||
async def get_id_identifier_of_element(element_id):
|
async def get_id_identifier_of_element(element_uuid):
|
||||||
singular_form_element = await get_singular_form_of_element(element_id)
|
singular_form_element = await get_singular_form_of_element(element_uuid)
|
||||||
|
|
||||||
if singular_form_element == "ogranizations":
|
if singular_form_element == "ogranizations":
|
||||||
return "org_id"
|
return "org_id"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from sqlmodel import Session, select
|
||||||
from src.db.courses import Course
|
from src.db.courses import Course
|
||||||
from src.db.chapters import Chapter
|
from src.db.chapters import Chapter
|
||||||
from src.security.rbac.rbac import (
|
from src.security.rbac.rbac import (
|
||||||
authorization_verify_based_on_roles_and_authorship,
|
authorization_verify_based_on_roles_and_authorship_and_usergroups,
|
||||||
authorization_verify_if_element_is_public,
|
authorization_verify_if_element_is_public,
|
||||||
authorization_verify_if_user_is_anon,
|
authorization_verify_if_user_is_anon,
|
||||||
)
|
)
|
||||||
|
|
@ -238,14 +238,14 @@ async def rbac_check(
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
else:
|
else:
|
||||||
res = await authorization_verify_based_on_roles_and_authorship(
|
res = await authorization_verify_based_on_roles_and_authorship_and_usergroups(
|
||||||
request, current_user.id, action, course_uuid, db_session
|
request, current_user.id, action, course_uuid, db_session
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
else:
|
else:
|
||||||
await authorization_verify_if_user_is_anon(current_user.id)
|
await authorization_verify_if_user_is_anon(current_user.id)
|
||||||
|
|
||||||
await authorization_verify_based_on_roles_and_authorship(
|
await authorization_verify_based_on_roles_and_authorship_and_usergroups(
|
||||||
request,
|
request,
|
||||||
current_user.id,
|
current_user.id,
|
||||||
action,
|
action,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from src.db.courses import Course
|
||||||
from src.db.organizations import Organization
|
from src.db.organizations import Organization
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
from src.security.rbac.rbac import (
|
from src.security.rbac.rbac import (
|
||||||
authorization_verify_based_on_roles_and_authorship,
|
authorization_verify_based_on_roles_and_authorship_and_usergroups,
|
||||||
authorization_verify_if_user_is_anon,
|
authorization_verify_if_user_is_anon,
|
||||||
)
|
)
|
||||||
from src.db.chapters import Chapter
|
from src.db.chapters import Chapter
|
||||||
|
|
@ -150,7 +150,7 @@ async def rbac_check(
|
||||||
):
|
):
|
||||||
await authorization_verify_if_user_is_anon(current_user.id)
|
await authorization_verify_if_user_is_anon(current_user.id)
|
||||||
|
|
||||||
await authorization_verify_based_on_roles_and_authorship(
|
await authorization_verify_based_on_roles_and_authorship_and_usergroups(
|
||||||
request,
|
request,
|
||||||
current_user.id,
|
current_user.id,
|
||||||
action,
|
action,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from src.db.organizations import Organization
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
from src.security.rbac.rbac import (
|
from src.security.rbac.rbac import (
|
||||||
authorization_verify_based_on_roles_and_authorship,
|
authorization_verify_based_on_roles_and_authorship_and_usergroups,
|
||||||
authorization_verify_if_user_is_anon,
|
authorization_verify_if_user_is_anon,
|
||||||
)
|
)
|
||||||
from src.db.chapters import Chapter
|
from src.db.chapters import Chapter
|
||||||
|
|
@ -232,7 +232,7 @@ async def rbac_check(
|
||||||
):
|
):
|
||||||
await authorization_verify_if_user_is_anon(current_user.id)
|
await authorization_verify_if_user_is_anon(current_user.id)
|
||||||
|
|
||||||
await authorization_verify_based_on_roles_and_authorship(
|
await authorization_verify_based_on_roles_and_authorship_and_usergroups(
|
||||||
request,
|
request,
|
||||||
current_user.id,
|
current_user.id,
|
||||||
action,
|
action,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from uuid import uuid4
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
from src.db.users import AnonymousUser
|
from src.db.users import AnonymousUser
|
||||||
from src.security.rbac.rbac import (
|
from src.security.rbac.rbac import (
|
||||||
authorization_verify_based_on_roles_and_authorship,
|
authorization_verify_based_on_roles_and_authorship_and_usergroups,
|
||||||
authorization_verify_if_element_is_public,
|
authorization_verify_if_element_is_public,
|
||||||
authorization_verify_if_user_is_anon,
|
authorization_verify_if_user_is_anon,
|
||||||
)
|
)
|
||||||
|
|
@ -559,17 +559,16 @@ async def rbac_check(
|
||||||
res = await authorization_verify_if_element_is_public(
|
res = await authorization_verify_if_element_is_public(
|
||||||
request, course_uuid, action, db_session
|
request, course_uuid, action, db_session
|
||||||
)
|
)
|
||||||
print("res", res)
|
|
||||||
return res
|
return res
|
||||||
else:
|
else:
|
||||||
res = await authorization_verify_based_on_roles_and_authorship(
|
res = await authorization_verify_based_on_roles_and_authorship_and_usergroups(
|
||||||
request, current_user.id, action, course_uuid, db_session
|
request, current_user.id, action, course_uuid, db_session
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
else:
|
else:
|
||||||
await authorization_verify_if_user_is_anon(current_user.id)
|
await authorization_verify_if_user_is_anon(current_user.id)
|
||||||
|
|
||||||
await authorization_verify_based_on_roles_and_authorship(
|
await authorization_verify_based_on_roles_and_authorship_and_usergroups(
|
||||||
request,
|
request,
|
||||||
current_user.id,
|
current_user.id,
|
||||||
action,
|
action,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from uuid import uuid4
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
from src.db.users import AnonymousUser
|
from src.db.users import AnonymousUser
|
||||||
from src.security.rbac.rbac import (
|
from src.security.rbac.rbac import (
|
||||||
authorization_verify_based_on_roles_and_authorship,
|
authorization_verify_based_on_roles_and_authorship_and_usergroups,
|
||||||
authorization_verify_if_element_is_public,
|
authorization_verify_if_element_is_public,
|
||||||
authorization_verify_if_user_is_anon,
|
authorization_verify_if_user_is_anon,
|
||||||
)
|
)
|
||||||
|
|
@ -297,14 +297,14 @@ async def rbac_check(
|
||||||
detail="User rights : You are not allowed to read this collection",
|
detail="User rights : You are not allowed to read this collection",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
res = await authorization_verify_based_on_roles_and_authorship(
|
res = await authorization_verify_based_on_roles_and_authorship_and_usergroups(
|
||||||
request, current_user.id, action, collection_uuid, db_session
|
request, current_user.id, action, collection_uuid, db_session
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
else:
|
else:
|
||||||
await authorization_verify_if_user_is_anon(current_user.id)
|
await authorization_verify_if_user_is_anon(current_user.id)
|
||||||
|
|
||||||
await authorization_verify_based_on_roles_and_authorship(
|
await authorization_verify_based_on_roles_and_authorship_and_usergroups(
|
||||||
request,
|
request,
|
||||||
current_user.id,
|
current_user.id,
|
||||||
action,
|
action,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
from sqlalchemy import union
|
||||||
from sqlmodel import Session, select
|
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.db.organizations import Organization
|
||||||
from src.db.trails import TrailRead
|
from src.db.trails import TrailRead
|
||||||
|
|
||||||
from src.services.trail.trail import get_user_trail_with_orgid
|
from src.services.trail.trail import get_user_trail_with_orgid
|
||||||
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
|
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
|
||||||
from src.db.users import PublicUser, AnonymousUser, User, UserRead
|
from src.db.users import PublicUser, AnonymousUser, User, UserRead
|
||||||
|
|
@ -15,7 +17,7 @@ from src.db.courses import (
|
||||||
FullCourseReadWithTrail,
|
FullCourseReadWithTrail,
|
||||||
)
|
)
|
||||||
from src.security.rbac.rbac import (
|
from src.security.rbac.rbac import (
|
||||||
authorization_verify_based_on_roles_and_authorship,
|
authorization_verify_based_on_roles_and_authorship_and_usergroups,
|
||||||
authorization_verify_if_element_is_public,
|
authorization_verify_if_element_is_public,
|
||||||
authorization_verify_if_user_is_anon,
|
authorization_verify_if_user_is_anon,
|
||||||
)
|
)
|
||||||
|
|
@ -106,7 +108,6 @@ async def get_course_meta(
|
||||||
)
|
)
|
||||||
trail = TrailRead.from_orm(trail)
|
trail = TrailRead.from_orm(trail)
|
||||||
|
|
||||||
|
|
||||||
return FullCourseReadWithTrail(
|
return FullCourseReadWithTrail(
|
||||||
**course.dict(),
|
**course.dict(),
|
||||||
chapters=chapters,
|
chapters=chapters,
|
||||||
|
|
@ -142,7 +143,7 @@ async def create_course(
|
||||||
if thumbnail_file and thumbnail_file.filename:
|
if thumbnail_file and thumbnail_file.filename:
|
||||||
name_in_disk = f"{course.course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
|
name_in_disk = f"{course.course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
|
||||||
await upload_thumbnail(
|
await upload_thumbnail(
|
||||||
thumbnail_file, name_in_disk, org.org_uuid, course.course_uuid
|
thumbnail_file, name_in_disk, org.org_uuid, course.course_uuid # type: ignore
|
||||||
)
|
)
|
||||||
course.thumbnail_image = name_in_disk
|
course.thumbnail_image = name_in_disk
|
||||||
|
|
||||||
|
|
@ -213,7 +214,7 @@ async def update_course_thumbnail(
|
||||||
if thumbnail_file and thumbnail_file.filename:
|
if thumbnail_file and thumbnail_file.filename:
|
||||||
name_in_disk = f"{course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
|
name_in_disk = f"{course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
|
||||||
await upload_thumbnail(
|
await upload_thumbnail(
|
||||||
thumbnail_file, name_in_disk, org.org_uuid, course.course_uuid
|
thumbnail_file, name_in_disk, org.org_uuid, course.course_uuid # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update course
|
# Update course
|
||||||
|
|
@ -327,26 +328,47 @@ async def get_courses_orgslug(
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
):
|
):
|
||||||
|
|
||||||
|
# Query for public courses
|
||||||
statement_public = (
|
statement_public = (
|
||||||
select(Course)
|
select(Course)
|
||||||
.join(Organization)
|
.join(Organization)
|
||||||
.where(Organization.slug == org_slug, Course.public == True)
|
.where(Organization.slug == org_slug, Course.public == True)
|
||||||
)
|
)
|
||||||
statement_all = (
|
|
||||||
select(Course).join(Organization).where(Organization.slug == org_slug)
|
# Query for courses where the current user is an author
|
||||||
|
statement_author = (
|
||||||
|
select(Course)
|
||||||
|
.join(Organization)
|
||||||
|
.join(ResourceAuthor, ResourceAuthor.user_id == current_user.id)
|
||||||
|
.where(
|
||||||
|
Organization.slug == org_slug,
|
||||||
|
ResourceAuthor.resource_uuid == Course.course_uuid,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if current_user.id == 0:
|
# Query for courses where the current user is in a user group that has access to the course
|
||||||
statement = statement_public
|
statement_usergroup = (
|
||||||
else:
|
select(Course)
|
||||||
# RBAC check
|
.join(Organization)
|
||||||
await authorization_verify_if_user_is_anon(current_user.id)
|
.join(
|
||||||
|
UserGroupResource, UserGroupResource.resource_uuid == Course.course_uuid
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
UserGroupUser, UserGroupUser.usergroup_id == UserGroupResource.usergroup_id
|
||||||
|
)
|
||||||
|
.where(Organization.slug == org_slug, UserGroupUser.user_id == current_user.id)
|
||||||
|
)
|
||||||
|
|
||||||
statement = statement_all
|
# Combine the results
|
||||||
|
statement_complete = union(
|
||||||
|
statement_public, statement_author, statement_usergroup
|
||||||
|
).subquery()
|
||||||
|
|
||||||
courses = db_session.exec(statement)
|
courses = db_session.execute(select([statement_complete])).all()
|
||||||
|
|
||||||
courses = [CourseRead(**course.dict(), authors=[]) for course in courses]
|
# TODO: I have no idea why this is necessary, but it is
|
||||||
|
courses = [CourseRead(**dict(course._mapping), authors=[]) for course in courses]
|
||||||
|
|
||||||
# for every course, get the authors
|
# for every course, get the authors
|
||||||
for course in courses:
|
for course in courses:
|
||||||
|
|
@ -367,6 +389,7 @@ async def get_courses_orgslug(
|
||||||
|
|
||||||
## 🔒 RBAC Utils ##
|
## 🔒 RBAC Utils ##
|
||||||
|
|
||||||
|
|
||||||
async def rbac_check(
|
async def rbac_check(
|
||||||
request: Request,
|
request: Request,
|
||||||
course_uuid: str,
|
course_uuid: str,
|
||||||
|
|
@ -381,14 +404,16 @@ async def rbac_check(
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
else:
|
else:
|
||||||
res = await authorization_verify_based_on_roles_and_authorship(
|
res = (
|
||||||
request, current_user.id, action, course_uuid, db_session
|
await authorization_verify_based_on_roles_and_authorship_and_usergroups(
|
||||||
|
request, current_user.id, action, course_uuid, db_session
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
else:
|
else:
|
||||||
await authorization_verify_if_user_is_anon(current_user.id)
|
await authorization_verify_if_user_is_anon(current_user.id)
|
||||||
|
|
||||||
await authorization_verify_based_on_roles_and_authorship(
|
await authorization_verify_based_on_roles_and_authorship_and_usergroups(
|
||||||
request,
|
request,
|
||||||
current_user.id,
|
current_user.id,
|
||||||
action,
|
action,
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,12 @@ async def install_default_elements(db_session: Session):
|
||||||
action_update=True,
|
action_update=True,
|
||||||
action_delete=True,
|
action_delete=True,
|
||||||
),
|
),
|
||||||
|
usergroups=Permission(
|
||||||
|
action_create=True,
|
||||||
|
action_read=True,
|
||||||
|
action_update=True,
|
||||||
|
action_delete=True,
|
||||||
|
),
|
||||||
collections=Permission(
|
collections=Permission(
|
||||||
action_create=True,
|
action_create=True,
|
||||||
action_read=True,
|
action_read=True,
|
||||||
|
|
@ -183,6 +189,12 @@ async def install_default_elements(db_session: Session):
|
||||||
action_update=True,
|
action_update=True,
|
||||||
action_delete=True,
|
action_delete=True,
|
||||||
),
|
),
|
||||||
|
usergroups=Permission(
|
||||||
|
action_create=True,
|
||||||
|
action_read=True,
|
||||||
|
action_update=True,
|
||||||
|
action_delete=True,
|
||||||
|
),
|
||||||
collections=Permission(
|
collections=Permission(
|
||||||
action_create=True,
|
action_create=True,
|
||||||
action_read=True,
|
action_read=True,
|
||||||
|
|
@ -231,6 +243,12 @@ async def install_default_elements(db_session: Session):
|
||||||
action_update=False,
|
action_update=False,
|
||||||
action_delete=False,
|
action_delete=False,
|
||||||
),
|
),
|
||||||
|
usergroups=Permission(
|
||||||
|
action_create=False,
|
||||||
|
action_read=True,
|
||||||
|
action_update=False,
|
||||||
|
action_delete=False,
|
||||||
|
),
|
||||||
collections=Permission(
|
collections=Permission(
|
||||||
action_create=False,
|
action_create=False,
|
||||||
action_read=True,
|
action_read=True,
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,85 @@ async def create_invite_code(
|
||||||
return inviteCodeObject
|
return inviteCodeObject
|
||||||
|
|
||||||
|
|
||||||
|
async def create_invite_code_with_usergroup(
|
||||||
|
request: Request,
|
||||||
|
org_id: int,
|
||||||
|
usergroup_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,
|
||||||
|
"usergroup_id": usergroup_id,
|
||||||
|
"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(
|
async def get_invite_codes(
|
||||||
request: Request,
|
request: Request,
|
||||||
org_id: int,
|
org_id: int,
|
||||||
|
|
@ -136,11 +215,13 @@ async def get_invite_codes(
|
||||||
# Get invite codes
|
# Get invite codes
|
||||||
invite_codes = r.keys(f"org_invite_code_*:org:{org.org_uuid}:code:*")
|
invite_codes = r.keys(f"org_invite_code_*:org:{org.org_uuid}:code:*")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
invite_codes_list = []
|
invite_codes_list = []
|
||||||
|
|
||||||
for invite_code in invite_codes:
|
for invite_code in invite_codes: # type: ignore
|
||||||
invite_code = r.get(invite_code)
|
invite_code = r.get(invite_code)
|
||||||
invite_code = json.loads(invite_code) # type: ignore
|
invite_code = json.loads(invite_code) # type: ignore
|
||||||
invite_codes_list.append(invite_code)
|
invite_codes_list.append(invite_code)
|
||||||
|
|
||||||
return invite_codes_list
|
return invite_codes_list
|
||||||
|
|
@ -285,7 +366,7 @@ def send_invite_email(
|
||||||
# Send email
|
# Send email
|
||||||
if invite:
|
if invite:
|
||||||
invite = r.get(invite[0])
|
invite = r.get(invite[0])
|
||||||
invite = json.loads(invite) # type: ignore
|
invite = json.loads(invite) # type: ignore
|
||||||
|
|
||||||
# send email
|
# send email
|
||||||
send_email(
|
send_email(
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from typing import Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
from src.security.rbac.rbac import (
|
from src.security.rbac.rbac import (
|
||||||
authorization_verify_based_on_roles_and_authorship,
|
authorization_verify_based_on_roles_and_authorship_and_usergroups,
|
||||||
authorization_verify_if_user_is_anon,
|
authorization_verify_if_user_is_anon,
|
||||||
)
|
)
|
||||||
from src.db.users import AnonymousUser, PublicUser
|
from src.db.users import AnonymousUser, PublicUser
|
||||||
|
|
@ -133,7 +133,7 @@ async def rbac_check(
|
||||||
):
|
):
|
||||||
await authorization_verify_if_user_is_anon(current_user.id)
|
await authorization_verify_if_user_is_anon(current_user.id)
|
||||||
|
|
||||||
await authorization_verify_based_on_roles_and_authorship(
|
await authorization_verify_based_on_roles_and_authorship_and_usergroups(
|
||||||
request, current_user.id, action, role_uuid, db_session
|
request, current_user.id, action, role_uuid, db_session
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
491
apps/api/src/services/users/usergroups.py
Normal file
491
apps/api/src/services/users/usergroups.py
Normal file
|
|
@ -0,0 +1,491 @@
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
from typing import Literal
|
||||||
|
from uuid import uuid4
|
||||||
|
from fastapi import HTTPException, Request
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from src.security.rbac.rbac import (
|
||||||
|
authorization_verify_based_on_roles_and_authorship_and_usergroups,
|
||||||
|
authorization_verify_if_user_is_anon,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
async def create_usergroup(
|
||||||
|
request: Request,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
usergroup_create: UserGroupCreate,
|
||||||
|
) -> UserGroupRead:
|
||||||
|
|
||||||
|
usergroup = UserGroup.from_orm(usergroup_create)
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(
|
||||||
|
request,
|
||||||
|
usergroup_uuid="usergroup_X",
|
||||||
|
current_user=current_user,
|
||||||
|
action="create",
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if Organization exists
|
||||||
|
statement = select(Organization).where(Organization.id == usergroup_create.org_id)
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
|
||||||
|
if not result.first():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Organization does not exist",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Complete the object
|
||||||
|
usergroup.usergroup_uuid = f"usergroup_{uuid4()}"
|
||||||
|
usergroup.creation_date = str(datetime.now())
|
||||||
|
usergroup.update_date = str(datetime.now())
|
||||||
|
|
||||||
|
# Save the object
|
||||||
|
db_session.add(usergroup)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(usergroup)
|
||||||
|
|
||||||
|
usergroup = UserGroupRead.from_orm(usergroup)
|
||||||
|
|
||||||
|
return usergroup
|
||||||
|
|
||||||
|
|
||||||
|
async def read_usergroup_by_id(
|
||||||
|
request: Request,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
usergroup_id: int,
|
||||||
|
) -> UserGroupRead:
|
||||||
|
|
||||||
|
statement = select(UserGroup).where(UserGroup.id == usergroup_id)
|
||||||
|
usergroup = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
if not usergroup:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="UserGroup not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(
|
||||||
|
request,
|
||||||
|
usergroup_uuid=usergroup.usergroup_uuid,
|
||||||
|
current_user=current_user,
|
||||||
|
action="read",
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
usergroup = UserGroupRead.from_orm(usergroup)
|
||||||
|
|
||||||
|
return usergroup
|
||||||
|
|
||||||
|
|
||||||
|
async def get_users_linked_to_usergroup(
|
||||||
|
request: Request,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
usergroup_id: int,
|
||||||
|
) -> list[UserRead]:
|
||||||
|
|
||||||
|
statement = select(UserGroup).where(UserGroup.id == usergroup_id)
|
||||||
|
usergroup = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
if not usergroup:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="UserGroup not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(
|
||||||
|
request,
|
||||||
|
usergroup_uuid=usergroup.usergroup_uuid,
|
||||||
|
current_user=current_user,
|
||||||
|
action="read",
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
statement = select(UserGroupUser).where(UserGroupUser.usergroup_id == usergroup_id)
|
||||||
|
usergroup_users = db_session.exec(statement).all()
|
||||||
|
|
||||||
|
user_ids = [usergroup_user.user_id for usergroup_user in usergroup_users]
|
||||||
|
|
||||||
|
# get users
|
||||||
|
users = []
|
||||||
|
for user_id in user_ids:
|
||||||
|
statement = select(User).where(User.id == user_id)
|
||||||
|
user = db_session.exec(statement).first()
|
||||||
|
users.append(user)
|
||||||
|
|
||||||
|
users = [UserRead.from_orm(user) for user in users]
|
||||||
|
|
||||||
|
return users
|
||||||
|
|
||||||
|
|
||||||
|
async def read_usergroups_by_org_id(
|
||||||
|
request: Request,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
org_id: int,
|
||||||
|
) -> list[UserGroupRead]:
|
||||||
|
|
||||||
|
statement = select(UserGroup).where(UserGroup.org_id == org_id)
|
||||||
|
usergroups = db_session.exec(statement).all()
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(
|
||||||
|
request,
|
||||||
|
usergroup_uuid="usergroup_X",
|
||||||
|
current_user=current_user,
|
||||||
|
action="read",
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
usergroups = [UserGroupRead.from_orm(usergroup) for usergroup in usergroups]
|
||||||
|
|
||||||
|
return usergroups
|
||||||
|
|
||||||
|
|
||||||
|
async def get_usergroups_by_resource(
|
||||||
|
request: Request,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
resource_uuid: str,
|
||||||
|
) -> list[UserGroupRead]:
|
||||||
|
|
||||||
|
statement = select(UserGroupResource).where(
|
||||||
|
UserGroupResource.resource_uuid == resource_uuid
|
||||||
|
)
|
||||||
|
usergroup_resources = db_session.exec(statement).all()
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(
|
||||||
|
request,
|
||||||
|
usergroup_uuid="usergroup_X",
|
||||||
|
current_user=current_user,
|
||||||
|
action="read",
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
usergroup_ids = [usergroup.usergroup_id for usergroup in usergroup_resources]
|
||||||
|
|
||||||
|
# get usergroups
|
||||||
|
usergroups = []
|
||||||
|
for usergroup_id in usergroup_ids:
|
||||||
|
statement = select(UserGroup).where(UserGroup.id == usergroup_id)
|
||||||
|
usergroup = db_session.exec(statement).first()
|
||||||
|
usergroups.append(usergroup)
|
||||||
|
|
||||||
|
usergroups = [UserGroupRead.from_orm(usergroup) for usergroup in usergroups]
|
||||||
|
|
||||||
|
return usergroups
|
||||||
|
|
||||||
|
|
||||||
|
async def update_usergroup_by_id(
|
||||||
|
request: Request,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
usergroup_id: int,
|
||||||
|
usergroup_update: UserGroupUpdate,
|
||||||
|
) -> UserGroupRead:
|
||||||
|
|
||||||
|
statement = select(UserGroup).where(UserGroup.id == usergroup_id)
|
||||||
|
usergroup = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
if not usergroup:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="UserGroup not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(
|
||||||
|
request,
|
||||||
|
usergroup_uuid=usergroup.usergroup_uuid,
|
||||||
|
current_user=current_user,
|
||||||
|
action="update",
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
usergroup.name = usergroup_update.name
|
||||||
|
usergroup.description = usergroup_update.description
|
||||||
|
usergroup.update_date = str(datetime.now())
|
||||||
|
|
||||||
|
db_session.add(usergroup)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(usergroup)
|
||||||
|
|
||||||
|
usergroup = UserGroupRead.from_orm(usergroup)
|
||||||
|
|
||||||
|
return usergroup
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_usergroup_by_id(
|
||||||
|
request: Request,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
usergroup_id: int,
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
statement = select(UserGroup).where(UserGroup.id == usergroup_id)
|
||||||
|
usergroup = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
if not usergroup:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="UserGroup not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(
|
||||||
|
request,
|
||||||
|
usergroup_uuid=usergroup.usergroup_uuid,
|
||||||
|
current_user=current_user,
|
||||||
|
action="delete",
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
db_session.delete(usergroup)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
return "UserGroup deleted successfully"
|
||||||
|
|
||||||
|
|
||||||
|
async def add_users_to_usergroup(
|
||||||
|
request: Request,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
usergroup_id: int,
|
||||||
|
user_ids: str,
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
statement = select(UserGroup).where(UserGroup.id == usergroup_id)
|
||||||
|
usergroup = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
if not usergroup:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="UserGroup not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(
|
||||||
|
request,
|
||||||
|
usergroup_uuid=usergroup.usergroup_uuid,
|
||||||
|
current_user=current_user,
|
||||||
|
action="create",
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_ids_array = user_ids.split(",")
|
||||||
|
|
||||||
|
for user_id in user_ids_array:
|
||||||
|
statement = select(User).where(User.id == user_id)
|
||||||
|
user = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
# Check if User is already Linked to UserGroup
|
||||||
|
statement = select(UserGroupUser).where(
|
||||||
|
UserGroupUser.usergroup_id == usergroup_id,
|
||||||
|
UserGroupUser.user_id == user_id,
|
||||||
|
)
|
||||||
|
usergroup_user = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
if usergroup_user:
|
||||||
|
logging.error(f"User with id {user_id} already exists in UserGroup")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if user:
|
||||||
|
# Add user to UserGroup
|
||||||
|
if user.id is not None:
|
||||||
|
usergroup_obj = UserGroupUser(
|
||||||
|
usergroup_id=usergroup_id,
|
||||||
|
user_id=user.id,
|
||||||
|
org_id=usergroup.org_id,
|
||||||
|
creation_date=str(datetime.now()),
|
||||||
|
update_date=str(datetime.now()),
|
||||||
|
)
|
||||||
|
|
||||||
|
db_session.add(usergroup_obj)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(usergroup_obj)
|
||||||
|
else:
|
||||||
|
logging.error(f"User with id {user_id} not found")
|
||||||
|
|
||||||
|
return "Users added to UserGroup successfully"
|
||||||
|
|
||||||
|
|
||||||
|
async def remove_users_from_usergroup(
|
||||||
|
request: Request,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
usergroup_id: int,
|
||||||
|
user_ids: str,
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
statement = select(UserGroup).where(UserGroup.id == usergroup_id)
|
||||||
|
usergroup = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
if not usergroup:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="UserGroup not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(
|
||||||
|
request,
|
||||||
|
usergroup_uuid=usergroup.usergroup_uuid,
|
||||||
|
current_user=current_user,
|
||||||
|
action="delete",
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
usergroup_user = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
if usergroup_user:
|
||||||
|
db_session.delete(usergroup_user)
|
||||||
|
db_session.commit()
|
||||||
|
else:
|
||||||
|
logging.error(f"User with id {user_id} not found in UserGroup")
|
||||||
|
|
||||||
|
return "Users removed from UserGroup successfully"
|
||||||
|
|
||||||
|
|
||||||
|
async def add_resources_to_usergroup(
|
||||||
|
request: Request,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
usergroup_id: int,
|
||||||
|
resources_uuids: str,
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
statement = select(UserGroup).where(UserGroup.id == usergroup_id)
|
||||||
|
usergroup = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
if not usergroup:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="UserGroup not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(
|
||||||
|
request,
|
||||||
|
usergroup_uuid=usergroup.usergroup_uuid,
|
||||||
|
current_user=current_user,
|
||||||
|
action="create",
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
resources_uuids_array = resources_uuids.split(",")
|
||||||
|
|
||||||
|
for resource_uuid in resources_uuids_array:
|
||||||
|
# Check if a link between UserGroup and Resource already exists
|
||||||
|
statement = select(UserGroupResource).where(
|
||||||
|
UserGroupResource.usergroup_id == usergroup_id,
|
||||||
|
UserGroupResource.resource_uuid == resource_uuid,
|
||||||
|
)
|
||||||
|
usergroup_resource = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
if usergroup_resource:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Resource {resource_uuid} already exists in UserGroup",
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# TODO : Find a way to check if resource really exists
|
||||||
|
usergroup_obj = UserGroupResource(
|
||||||
|
usergroup_id=usergroup_id,
|
||||||
|
resource_uuid=resource_uuid,
|
||||||
|
org_id=usergroup.org_id,
|
||||||
|
creation_date=str(datetime.now()),
|
||||||
|
update_date=str(datetime.now()),
|
||||||
|
)
|
||||||
|
|
||||||
|
db_session.add(usergroup_obj)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(usergroup_obj)
|
||||||
|
|
||||||
|
return "Resources added to UserGroup successfully"
|
||||||
|
|
||||||
|
|
||||||
|
async def remove_resources_from_usergroup(
|
||||||
|
request: Request,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
usergroup_id: int,
|
||||||
|
resources_uuids: str,
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
statement = select(UserGroup).where(UserGroup.id == usergroup_id)
|
||||||
|
usergroup = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
if not usergroup:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="UserGroup not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(
|
||||||
|
request,
|
||||||
|
usergroup_uuid=usergroup.usergroup_uuid,
|
||||||
|
current_user=current_user,
|
||||||
|
action="delete",
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
resources_uuids_array = resources_uuids.split(",")
|
||||||
|
|
||||||
|
for resource_uuid in resources_uuids_array:
|
||||||
|
statement = select(UserGroupResource).where(
|
||||||
|
UserGroupResource.resource_uuid == resource_uuid
|
||||||
|
)
|
||||||
|
usergroup_resource = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
if usergroup_resource:
|
||||||
|
db_session.delete(usergroup_resource)
|
||||||
|
db_session.commit()
|
||||||
|
else:
|
||||||
|
logging.error(f"resource with uuid {resource_uuid} not found in UserGroup")
|
||||||
|
|
||||||
|
return "Resources removed from UserGroup successfully"
|
||||||
|
|
||||||
|
|
||||||
|
## 🔒 RBAC Utils ##
|
||||||
|
|
||||||
|
|
||||||
|
async def rbac_check(
|
||||||
|
request: Request,
|
||||||
|
usergroup_uuid: str,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
action: Literal["create", "read", "update", "delete"],
|
||||||
|
db_session: Session,
|
||||||
|
):
|
||||||
|
await authorization_verify_if_user_is_anon(current_user.id)
|
||||||
|
|
||||||
|
await authorization_verify_based_on_roles_and_authorship_and_usergroups(
|
||||||
|
request,
|
||||||
|
current_user.id,
|
||||||
|
action,
|
||||||
|
usergroup_uuid,
|
||||||
|
db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## 🔒 RBAC Utils ##
|
||||||
|
|
@ -3,6 +3,7 @@ from typing import Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from fastapi import HTTPException, Request, UploadFile, status
|
from fastapi import HTTPException, Request, UploadFile, status
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
from src.services.users.usergroups import add_users_to_usergroup
|
||||||
from src.services.users.emails import (
|
from src.services.users.emails import (
|
||||||
send_account_creation_email,
|
send_account_creation_email,
|
||||||
)
|
)
|
||||||
|
|
@ -10,7 +11,7 @@ from src.services.orgs.invites import get_invite_code
|
||||||
from src.services.users.avatars import upload_avatar
|
from src.services.users.avatars import upload_avatar
|
||||||
from src.db.roles import Role, RoleRead
|
from src.db.roles import Role, RoleRead
|
||||||
from src.security.rbac.rbac import (
|
from src.security.rbac.rbac import (
|
||||||
authorization_verify_based_on_roles_and_authorship,
|
authorization_verify_based_on_roles_and_authorship_and_usergroups,
|
||||||
authorization_verify_if_user_is_anon,
|
authorization_verify_if_user_is_anon,
|
||||||
)
|
)
|
||||||
from src.db.organizations import Organization, OrganizationRead
|
from src.db.organizations import Organization, OrganizationRead
|
||||||
|
|
@ -124,16 +125,27 @@ async def create_user_with_invite(
|
||||||
):
|
):
|
||||||
|
|
||||||
# Check if invite code exists
|
# Check if invite code exists
|
||||||
isInviteCodeCorrect = await get_invite_code(
|
inviteCode = await get_invite_code(
|
||||||
request, org_id, invite_code, current_user, db_session
|
request, org_id, invite_code, current_user, db_session
|
||||||
)
|
)
|
||||||
|
|
||||||
if not isInviteCodeCorrect:
|
if not inviteCode:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Invite code is incorrect",
|
detail="Invite code is incorrect",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if invite code contains UserGroup
|
||||||
|
if inviteCode.usergroup_id:
|
||||||
|
# Add user to UserGroup
|
||||||
|
await add_users_to_usergroup(
|
||||||
|
request,
|
||||||
|
db_session,
|
||||||
|
current_user,
|
||||||
|
inviteCode.usergroup_id,
|
||||||
|
user_object.username,
|
||||||
|
)
|
||||||
|
|
||||||
user = await create_user(request, db_session, current_user, user_object, org_id)
|
user = await create_user(request, db_session, current_user, user_object, org_id)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
@ -346,6 +358,7 @@ async def update_user_password(
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
async def read_user_by_id(
|
async def read_user_by_id(
|
||||||
request: Request,
|
request: Request,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
|
|
@ -449,7 +462,7 @@ async def authorize_user_action(
|
||||||
request: Request,
|
request: Request,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
current_user: PublicUser | AnonymousUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
ressource_uuid: str,
|
resource_uuid: str,
|
||||||
action: Literal["create", "read", "update", "delete"],
|
action: Literal["create", "read", "update", "delete"],
|
||||||
):
|
):
|
||||||
# Get user
|
# Get user
|
||||||
|
|
@ -463,8 +476,10 @@ async def authorize_user_action(
|
||||||
)
|
)
|
||||||
|
|
||||||
# RBAC check
|
# RBAC check
|
||||||
authorized = await authorization_verify_based_on_roles_and_authorship(
|
authorized = (
|
||||||
request, current_user.id, action, ressource_uuid, db_session
|
await authorization_verify_based_on_roles_and_authorship_and_usergroups(
|
||||||
|
request, current_user.id, action, resource_uuid, db_session
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if authorized:
|
if authorized:
|
||||||
|
|
@ -535,7 +550,7 @@ async def rbac_check(
|
||||||
if current_user.id == 0: # if user is anonymous
|
if current_user.id == 0: # if user is anonymous
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
await authorization_verify_based_on_roles_and_authorship(
|
await authorization_verify_based_on_roles_and_authorship_and_usergroups(
|
||||||
request, current_user.id, "create", "user_x", db_session
|
request, current_user.id, "create", "user_x", db_session
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -546,7 +561,7 @@ async def rbac_check(
|
||||||
if current_user.user_uuid == user_uuid:
|
if current_user.user_uuid == user_uuid:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
await authorization_verify_based_on_roles_and_authorship(
|
await authorization_verify_based_on_roles_and_authorship_and_usergroups(
|
||||||
request, current_user.id, action, user_uuid, db_session
|
request, current_user.id, action, user_uuid, db_session
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ import Link from 'next/link'
|
||||||
import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop'
|
import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import EditCourseGeneral from '@components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral'
|
import EditCourseGeneral from '@components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral'
|
||||||
import { GalleryVerticalEnd, Info } from 'lucide-react'
|
import { GalleryVerticalEnd, Info, Lock, UserRoundCog } from 'lucide-react'
|
||||||
|
import EditCourseAccess from '@components/Dashboard/Course/EditCourseAccess/EditCourseAccess'
|
||||||
|
|
||||||
export type CourseOverviewParams = {
|
export type CourseOverviewParams = {
|
||||||
orgslug: string
|
orgslug: string
|
||||||
|
|
@ -24,9 +25,9 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]">
|
<div className="h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]">
|
||||||
<CourseProvider courseuuid={getEntireCourseUUID(params.courseuuid)}>
|
<CourseProvider courseuuid={getEntireCourseUUID(params.courseuuid)}>
|
||||||
<div className="pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]">
|
<div className="pl-10 pr-10 text-sm tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]">
|
||||||
<CourseOverviewTop params={params} />
|
<CourseOverviewTop params={params} />
|
||||||
<div className="flex space-x-5 font-black text-sm">
|
<div className="flex space-x-3 font-black text-sm">
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
getUriWithOrg(params.orgslug, '') +
|
getUriWithOrg(params.orgslug, '') +
|
||||||
|
|
@ -34,11 +35,10 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`py-2 w-fit text-center border-black transition-all ease-linear ${
|
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general'
|
||||||
params.subpage.toString() === 'general'
|
? 'border-b-4'
|
||||||
? 'border-b-4'
|
: 'opacity-50'
|
||||||
: 'opacity-50'
|
} cursor-pointer`}
|
||||||
} cursor-pointer`}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2.5 mx-2">
|
<div className="flex items-center space-x-2.5 mx-2">
|
||||||
<Info size={16} />
|
<Info size={16} />
|
||||||
|
|
@ -46,6 +46,24 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={
|
||||||
|
getUriWithOrg(params.orgslug, '') +
|
||||||
|
`/dash/courses/course/${params.courseuuid}/access`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'access'
|
||||||
|
? 'border-b-4'
|
||||||
|
: 'opacity-50'
|
||||||
|
} cursor-pointer`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2.5 mx-2">
|
||||||
|
<UserRoundCog size={16} />
|
||||||
|
<div>Access</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
getUriWithOrg(params.orgslug, '') +
|
getUriWithOrg(params.orgslug, '') +
|
||||||
|
|
@ -53,11 +71,10 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${
|
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'content'
|
||||||
params.subpage.toString() === 'content'
|
? 'border-b-4'
|
||||||
? 'border-b-4'
|
: 'opacity-50'
|
||||||
: 'opacity-50'
|
} cursor-pointer`}
|
||||||
} cursor-pointer`}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2.5 mx-2">
|
<div className="flex items-center space-x-2.5 mx-2">
|
||||||
<GalleryVerticalEnd size={16} />
|
<GalleryVerticalEnd size={16} />
|
||||||
|
|
@ -65,7 +82,9 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
|
|
@ -74,16 +93,9 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
||||||
transition={{ duration: 0.1, type: 'spring', stiffness: 80 }}
|
transition={{ duration: 0.1, type: 'spring', stiffness: 80 }}
|
||||||
className="h-full overflow-y-auto"
|
className="h-full overflow-y-auto"
|
||||||
>
|
>
|
||||||
{params.subpage == 'content' ? (
|
{params.subpage == 'content' ? (<EditCourseStructure orgslug={params.orgslug} />) : ('')}
|
||||||
<EditCourseStructure orgslug={params.orgslug} />
|
{params.subpage == 'general' ? (<EditCourseGeneral orgslug={params.orgslug} />) : ('')}
|
||||||
) : (
|
{params.subpage == 'access' ? (<EditCourseAccess orgslug={params.orgslug} />) : ('')}
|
||||||
''
|
|
||||||
)}
|
|
||||||
{params.subpage == 'general' ? (
|
|
||||||
<EditCourseGeneral orgslug={params.orgslug} />
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</CourseProvider>
|
</CourseProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@ import React, { useEffect } from 'react'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { getUriWithOrg } from '@services/config/config'
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
import { ScanEye, UserPlus, Users } from 'lucide-react'
|
import { ScanEye, SquareUserRound, UserPlus, Users } from 'lucide-react'
|
||||||
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
|
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
|
||||||
import { useSession } from '@components/Contexts/SessionContext'
|
import { useSession } from '@components/Contexts/SessionContext'
|
||||||
import { useOrg } from '@components/Contexts/OrgContext'
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
import OrgUsers from '@components/Dashboard/Users/OrgUsers/OrgUsers'
|
import OrgUsers from '@components/Dashboard/Users/OrgUsers/OrgUsers'
|
||||||
import OrgAccess from '@components/Dashboard/Users/OrgAccess/OrgAccess'
|
import OrgAccess from '@components/Dashboard/Users/OrgAccess/OrgAccess'
|
||||||
import OrgUsersAdd from '@components/Dashboard/Users/OrgUsersAdd/OrgUsersAdd'
|
import OrgUsersAdd from '@components/Dashboard/Users/OrgUsersAdd/OrgUsersAdd'
|
||||||
|
import OrgUserGroups from '@components/Dashboard/Users/OrgUserGroups/OrgUserGroups'
|
||||||
|
|
||||||
export type SettingsParams = {
|
export type SettingsParams = {
|
||||||
subpage: string
|
subpage: string
|
||||||
|
|
@ -28,12 +29,16 @@ function UsersSettingsPage({ params }: { params: SettingsParams }) {
|
||||||
setH2Label('Manage your organization users, assign roles and permissions')
|
setH2Label('Manage your organization users, assign roles and permissions')
|
||||||
}
|
}
|
||||||
if (params.subpage == 'signups') {
|
if (params.subpage == 'signups') {
|
||||||
setH1Label('Signup Access')
|
setH1Label('Signups & Invite Codes')
|
||||||
setH2Label('Choose from where users can join your organization')
|
setH2Label('Choose from where users can join your organization')
|
||||||
}
|
}
|
||||||
if (params.subpage == 'add') {
|
if (params.subpage == 'add') {
|
||||||
setH1Label('Invite users')
|
setH1Label('Invite Members')
|
||||||
setH2Label('Invite users to join your organization')
|
setH2Label('Invite members to join your organization')
|
||||||
|
}
|
||||||
|
if (params.subpage == 'usergroups') {
|
||||||
|
setH1Label('UserGroups')
|
||||||
|
setH2Label('Create and manage user groups')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,11 +67,10 @@ function UsersSettingsPage({ params }: { params: SettingsParams }) {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`py-2 w-fit text-center border-black transition-all ease-linear ${
|
className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'users'
|
||||||
params.subpage.toString() === 'users'
|
|
||||||
? 'border-b-4'
|
? 'border-b-4'
|
||||||
: 'opacity-50'
|
: 'opacity-50'
|
||||||
} cursor-pointer`}
|
} cursor-pointer`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2.5 mx-2">
|
<div className="flex items-center space-x-2.5 mx-2">
|
||||||
<Users size={16} />
|
<Users size={16} />
|
||||||
|
|
@ -76,19 +80,18 @@ function UsersSettingsPage({ params }: { params: SettingsParams }) {
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
getUriWithOrg(params.orgslug, '') + `/dash/users/settings/add`
|
getUriWithOrg(params.orgslug, '') + `/dash/users/settings/usergroups`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`py-2 w-fit text-center border-black transition-all ease-linear ${
|
className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'usergroups'
|
||||||
params.subpage.toString() === 'add'
|
|
||||||
? 'border-b-4'
|
? 'border-b-4'
|
||||||
: 'opacity-50'
|
: 'opacity-50'
|
||||||
} cursor-pointer`}
|
} cursor-pointer`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2.5 mx-2">
|
<div className="flex items-center space-x-2.5 mx-2">
|
||||||
<UserPlus size={16} />
|
<SquareUserRound size={16} />
|
||||||
<div>Invite users</div>
|
<div>UserGroups</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -98,18 +101,35 @@ function UsersSettingsPage({ params }: { params: SettingsParams }) {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`py-2 w-fit text-center border-black transition-all ease-linear ${
|
className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'signups'
|
||||||
params.subpage.toString() === 'signups'
|
|
||||||
? 'border-b-4'
|
? 'border-b-4'
|
||||||
: 'opacity-50'
|
: 'opacity-50'
|
||||||
} cursor-pointer`}
|
} cursor-pointer`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2.5 mx-2">
|
<div className="flex items-center space-x-2.5 mx-2">
|
||||||
<ScanEye size={16} />
|
<ScanEye size={16} />
|
||||||
<div>Signup Access</div>
|
<div>Signups & Invite Codes</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={
|
||||||
|
getUriWithOrg(params.orgslug, '') + `/dash/users/settings/add`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'add'
|
||||||
|
? 'border-b-4'
|
||||||
|
: 'opacity-50'
|
||||||
|
} cursor-pointer`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2.5 mx-2">
|
||||||
|
<UserPlus size={16} />
|
||||||
|
<div>Invite Members</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
@ -122,6 +142,7 @@ function UsersSettingsPage({ params }: { params: SettingsParams }) {
|
||||||
{params.subpage == 'users' ? <OrgUsers /> : ''}
|
{params.subpage == 'users' ? <OrgUsers /> : ''}
|
||||||
{params.subpage == 'signups' ? <OrgAccess /> : ''}
|
{params.subpage == 'signups' ? <OrgAccess /> : ''}
|
||||||
{params.subpage == 'add' ? <OrgUsersAdd /> : ''}
|
{params.subpage == 'add' ? <OrgUsersAdd /> : ''}
|
||||||
|
{params.subpage == 'usergroups' ? <OrgUserGroups /> : ''}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
'use client'
|
'use client'
|
||||||
import { OrgProvider } from '@components/Contexts/OrgContext'
|
import { OrgProvider } from '@components/Contexts/OrgContext'
|
||||||
import SessionProvider from '@components/Contexts/SessionContext'
|
import SessionProvider from '@components/Contexts/SessionContext'
|
||||||
|
import Toast from '@components/StyledElements/Toast/Toast'
|
||||||
import '@styles/globals.css'
|
import '@styles/globals.css'
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|
@ -12,6 +13,7 @@ export default function RootLayout({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<Toast />
|
||||||
<OrgProvider orgslug={params.orgslug}>
|
<OrgProvider orgslug={params.orgslug}>
|
||||||
<SessionProvider>{children}</SessionProvider>
|
<SessionProvider>{children}</SessionProvider>
|
||||||
</OrgProvider>
|
</OrgProvider>
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ function InviteOnlySignUpComponent(props: InviteOnlySignUpProps) {
|
||||||
last_name: '',
|
last_name: '',
|
||||||
},
|
},
|
||||||
validate,
|
validate,
|
||||||
|
enableReinitialize: true,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
setError('')
|
setError('')
|
||||||
setMessage('')
|
setMessage('')
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ function OpenSignUpComponent() {
|
||||||
last_name: '',
|
last_name: '',
|
||||||
},
|
},
|
||||||
validate,
|
validate,
|
||||||
|
enableReinitialize: true,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
setError('')
|
setError('')
|
||||||
setMessage('')
|
setMessage('')
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import Link from 'next/link'
|
||||||
import { getUriWithOrg } from '@services/config/config'
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
import { useSession } from '@components/Contexts/SessionContext'
|
import { useSession } from '@components/Contexts/SessionContext'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { MailWarning, Shield, UserPlus } from 'lucide-react'
|
import { MailWarning, Shield, Ticket, UserPlus } from 'lucide-react'
|
||||||
import { useOrg } from '@components/Contexts/OrgContext'
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
import UserAvatar from '@components/Objects/UserAvatar'
|
import UserAvatar from '@components/Objects/UserAvatar'
|
||||||
import OpenSignUpComponent from './OpenSignup'
|
import OpenSignUpComponent from './OpenSignup'
|
||||||
|
|
@ -201,7 +201,7 @@ const NoTokenScreen = (props: any) => {
|
||||||
onClick={validateCode}
|
onClick={validateCode}
|
||||||
className="flex w-fit space-x-2 bg-black px-6 py-2 text-md rounded-lg font-semibold h-fit text-white items-center shadow-md"
|
className="flex w-fit space-x-2 bg-black px-6 py-2 text-md rounded-lg font-semibold h-fit text-white items-center shadow-md"
|
||||||
>
|
>
|
||||||
<Shield size={18} />
|
<Ticket size={18} />
|
||||||
<p>Submit </p>
|
<p>Submit </p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext'
|
||||||
|
import LinkToUserGroup from '@components/Objects/Modals/Dash/EditCourseAccess/LinkToUserGroup'
|
||||||
|
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||||
|
import Modal from '@components/StyledElements/Modal/Modal'
|
||||||
|
import { getAPIUrl } from '@services/config/config'
|
||||||
|
import { unLinkResourcesToUserGroup } from '@services/usergroups/usergroups'
|
||||||
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
|
import { Globe, SquareUserRound, Users, UsersRound, X } from 'lucide-react'
|
||||||
|
import React from 'react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import useSWR, { mutate } from 'swr'
|
||||||
|
|
||||||
|
type EditCourseAccessProps = {
|
||||||
|
orgslug: string
|
||||||
|
course_uuid?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditCourseAccess(props: EditCourseAccessProps) {
|
||||||
|
const [error, setError] = React.useState('')
|
||||||
|
|
||||||
|
const course = useCourse() as any
|
||||||
|
const dispatchCourse = useCourseDispatch() as any
|
||||||
|
const courseStructure = course.courseStructure
|
||||||
|
const { data: usergroups } = useSWR(
|
||||||
|
courseStructure ? `${getAPIUrl()}usergroups/resource/${courseStructure.course_uuid}` : null,
|
||||||
|
swrFetcher
|
||||||
|
)
|
||||||
|
const [isPublic, setIsPublic] = React.useState(courseStructure.public)
|
||||||
|
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// This code will run whenever form values are updated
|
||||||
|
if (isPublic !== courseStructure.public) {
|
||||||
|
dispatchCourse({ type: 'setIsNotSaved' })
|
||||||
|
const updatedCourse = {
|
||||||
|
...courseStructure,
|
||||||
|
public: isPublic,
|
||||||
|
}
|
||||||
|
dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse })
|
||||||
|
}
|
||||||
|
}, [course, isPublic])
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{' '}
|
||||||
|
<div className="h-6"></div>
|
||||||
|
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4">
|
||||||
|
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 ">
|
||||||
|
<h1 className="font-bold text-xl text-gray-800">Access to the course</h1>
|
||||||
|
<h2 className="text-gray-500 text-sm">
|
||||||
|
{' '}
|
||||||
|
Choose if want your course to be publicly available on the internet or only accessible to signed in users{' '}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2 mx-auto mb-3">
|
||||||
|
<ConfirmationModal
|
||||||
|
confirmationButtonText="Change to Public"
|
||||||
|
confirmationMessage="Are you sure you want this course to be publicly available on the internet ?"
|
||||||
|
dialogTitle={'Change to Public ?'}
|
||||||
|
dialogTrigger={
|
||||||
|
<div className="w-full h-[200px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all">
|
||||||
|
{isPublic ? (
|
||||||
|
<div className="bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg">
|
||||||
|
Active
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="flex flex-col space-y-1 justify-center items-center h-full">
|
||||||
|
<Globe className="text-slate-400" size={40}></Globe>
|
||||||
|
<div className="text-2xl text-slate-700 font-bold">
|
||||||
|
Public
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-400 text-md tracking-tight w-[500px] leading-5 text-center">
|
||||||
|
The Course is publicly available on the internet, it is indexed by search engines and can be accessed by anyone
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
functionToExecute={() => {
|
||||||
|
setIsPublic(true)
|
||||||
|
}}
|
||||||
|
status="info"
|
||||||
|
></ConfirmationModal>
|
||||||
|
<ConfirmationModal
|
||||||
|
confirmationButtonText="Change to Users Only"
|
||||||
|
confirmationMessage="Are you sure you want this course to be only accessible to signed in users ?"
|
||||||
|
dialogTitle={'Change to Users Only ?'}
|
||||||
|
dialogTrigger={
|
||||||
|
<div className="w-full h-[200px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all">
|
||||||
|
{!isPublic ? (
|
||||||
|
<div className="bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg">
|
||||||
|
Active
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="flex flex-col space-y-1 justify-center items-center h-full">
|
||||||
|
<Users className="text-slate-400" size={40}></Users>
|
||||||
|
<div className="text-2xl text-slate-700 font-bold">
|
||||||
|
Users Only
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-400 text-md tracking-tight w-[500px] leading-5 text-center">
|
||||||
|
The Course is only accessible to signed in users, additionaly you can choose which UserGroups can access this course
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
functionToExecute={() => {
|
||||||
|
setIsPublic(false)
|
||||||
|
}}
|
||||||
|
status="info"
|
||||||
|
></ConfirmationModal>
|
||||||
|
</div>
|
||||||
|
{!isPublic ? ( <UserGroupsSection usergroups={usergroups} />) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function UserGroupsSection({ usergroups }: { usergroups: any[] }) {
|
||||||
|
const course = useCourse() as any
|
||||||
|
const [userGroupModal, setUserGroupModal] = React.useState(false)
|
||||||
|
|
||||||
|
const removeUserGroupLink = async (usergroup_id: number) => {
|
||||||
|
const res = await unLinkResourcesToUserGroup(usergroup_id, course.courseStructure.course_uuid)
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success('Successfully unliked from usergroup')
|
||||||
|
mutate(`${getAPIUrl()}usergroups/resource/${course.courseStructure.course_uuid}`)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 ">
|
||||||
|
<h1 className="font-bold text-xl text-gray-800">UserGroups</h1>
|
||||||
|
<h2 className="text-gray-500 text-sm">
|
||||||
|
{' '}
|
||||||
|
You can choose to give access to this course to specific groups of users only by linking it to a UserGroup{' '}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
|
||||||
|
<thead className="bg-gray-100 text-gray-500 rounded-xl uppercase">
|
||||||
|
<tr className="font-bolder text-sm">
|
||||||
|
<th className="py-3 px-4">Name</th>
|
||||||
|
<th className="py-3 px-4">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<>
|
||||||
|
<tbody className="mt-5 bg-white rounded-md">
|
||||||
|
{usergroups?.map((usergroup: any) => (
|
||||||
|
<tr
|
||||||
|
key={usergroup.invite_code_uuid}
|
||||||
|
className="border-b border-gray-100 text-sm"
|
||||||
|
>
|
||||||
|
<td className="py-3 px-4">{usergroup.name}</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<ConfirmationModal
|
||||||
|
confirmationButtonText="Delete Link"
|
||||||
|
confirmationMessage="Users from this UserGroup will no longer have access to this course"
|
||||||
|
dialogTitle={'Unlink UserGroup ?'}
|
||||||
|
dialogTrigger={
|
||||||
|
<button className="mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100">
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
<span> Delete link</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
functionToExecute={() => {
|
||||||
|
removeUserGroupLink(usergroup.id)
|
||||||
|
}}
|
||||||
|
status="warning"
|
||||||
|
></ConfirmationModal>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</>
|
||||||
|
</table>
|
||||||
|
<div className='flex flex-row-reverse mt-3 mr-2'>
|
||||||
|
<Modal
|
||||||
|
isDialogOpen={
|
||||||
|
userGroupModal
|
||||||
|
}
|
||||||
|
onOpenChange={() =>
|
||||||
|
setUserGroupModal(!userGroupModal)
|
||||||
|
}
|
||||||
|
minHeight="no-min"
|
||||||
|
minWidth='md'
|
||||||
|
dialogContent={
|
||||||
|
<LinkToUserGroup setUserGroupModal={setUserGroupModal} />
|
||||||
|
|
||||||
|
}
|
||||||
|
dialogTitle="Link Course to a UserGroup"
|
||||||
|
dialogDescription={
|
||||||
|
'Choose a UserGroup to link this course to, Users from this UserGroup will have access to this course.'
|
||||||
|
}
|
||||||
|
dialogTrigger={
|
||||||
|
<button
|
||||||
|
className=" flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100"
|
||||||
|
>
|
||||||
|
<SquareUserRound className="w-4 h-4" />
|
||||||
|
<span>Link to a UserGroup</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditCourseAccess
|
||||||
|
|
@ -116,10 +116,11 @@ function EditCourseGeneral(props: EditCourseStructureProps) {
|
||||||
message={formik.errors.description}
|
message={formik.errors.description}
|
||||||
/>
|
/>
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Textarea
|
<Input
|
||||||
style={{ backgroundColor: 'white' }}
|
style={{ backgroundColor: 'white' }}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
value={formik.values.description}
|
value={formik.values.description}
|
||||||
|
type='text'
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
|
|
@ -177,26 +178,7 @@ function EditCourseGeneral(props: EditCourseStructureProps) {
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField className="flex items-center h-10" name="public">
|
|
||||||
<div className="flex my-auto items-center">
|
|
||||||
<label
|
|
||||||
className="text-black text-[15px] leading-none pr-[15px]"
|
|
||||||
htmlFor="public-course"
|
|
||||||
>
|
|
||||||
Public Course
|
|
||||||
</label>
|
|
||||||
<Switch.Root
|
|
||||||
className="w-[42px] h-[25px] bg-neutral-200 rounded-full relative data-[state=checked]:bg-neutral-500 outline-none cursor-default"
|
|
||||||
id="public-course"
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
formik.setFieldValue('public', checked)
|
|
||||||
}
|
|
||||||
checked={formik.values.public === 'true'}
|
|
||||||
>
|
|
||||||
<Switch.Thumb className="block w-[21px] h-[21px] bg-white rounded-full shadow-[0_2px_2px] shadow-neutral-300 transition-transform duration-100 translate-x-0.5 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
|
||||||
</Switch.Root>
|
|
||||||
</div>
|
|
||||||
</FormField>
|
|
||||||
</FormLayout>
|
</FormLayout>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -46,18 +46,17 @@ function ThumbnailUpdate() {
|
||||||
{localThumbnail ? (
|
{localThumbnail ? (
|
||||||
<img
|
<img
|
||||||
src={URL.createObjectURL(localThumbnail)}
|
src={URL.createObjectURL(localThumbnail)}
|
||||||
className={`${
|
className={`${isLoading ? 'animate-pulse' : ''
|
||||||
isLoading ? 'animate-pulse' : ''
|
} shadow w-[200px] h-[100px] rounded-md`}
|
||||||
} shadow w-[200px] h-[100px] rounded-md`}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
src={`${getCourseThumbnailMediaDirectory(
|
src={`${course.courseStructure.thumbnail_image ? getCourseThumbnailMediaDirectory(
|
||||||
org?.org_uuid,
|
org?.org_uuid,
|
||||||
course.courseStructure.course_uuid,
|
course.courseStructure.course_uuid,
|
||||||
course.courseStructure.thumbnail_image
|
course.courseStructure.thumbnail_image
|
||||||
)}`}
|
) : '/empty_thumbnail.png'}`}
|
||||||
className="shadow w-[200px] h-[100px] rounded-md"
|
className="shadow w-[200px] h-[100px] rounded-md bg-gray-200"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ function BreadCrumbs(props: BreadCrumbsProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="h-2"></div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export function CourseOverviewTop({
|
||||||
last_breadcrumb={course.courseStructure.name}
|
last_breadcrumb={course.courseStructure.name}
|
||||||
></BreadCrumbs>
|
></BreadCrumbs>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex py-5 grow items-center">
|
<div className="flex py-3 grow items-center">
|
||||||
<Link
|
<Link
|
||||||
href={getUriWithOrg(org?.slug, '') + `/course/${params.courseuuid}`}
|
href={getUriWithOrg(org?.slug, '') + `/course/${params.courseuuid}`}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||||
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
|
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
|
||||||
import { swrFetcher } from '@services/utils/ts/requests'
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
import { Globe, Shield, X } from 'lucide-react'
|
import { Globe, Shield, Ticket, User, UserSquare, Users, X } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import useSWR, { mutate } from 'swr'
|
import useSWR, { mutate } from 'swr'
|
||||||
|
|
@ -16,6 +16,8 @@ import {
|
||||||
import Toast from '@components/StyledElements/Toast/Toast'
|
import Toast from '@components/StyledElements/Toast/Toast'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
import Modal from '@components/StyledElements/Modal/Modal'
|
||||||
|
import OrgInviteCodeGenerate from '@components/Objects/Modals/Dash/OrgAccess/OrgInviteCodeGenerate'
|
||||||
|
|
||||||
function OrgAccess() {
|
function OrgAccess() {
|
||||||
const org = useOrg() as any
|
const org = useOrg() as any
|
||||||
|
|
@ -25,6 +27,7 @@ function OrgAccess() {
|
||||||
)
|
)
|
||||||
const [isLoading, setIsLoading] = React.useState(false)
|
const [isLoading, setIsLoading] = React.useState(false)
|
||||||
const [joinMethod, setJoinMethod] = React.useState('closed')
|
const [joinMethod, setJoinMethod] = React.useState('closed')
|
||||||
|
const [invitesModal, setInvitesModal] = React.useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
async function getOrgJoinMethod() {
|
async function getOrgJoinMethod() {
|
||||||
|
|
@ -37,14 +40,7 @@ function OrgAccess() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createInvite() {
|
|
||||||
let res = await createInviteCode(org.id)
|
|
||||||
if (res.status == 200) {
|
|
||||||
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
|
|
||||||
} else {
|
|
||||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteInvite(invite: any) {
|
async function deleteInvite(invite: any) {
|
||||||
let res = await deleteInviteCode(org.id, invite.invite_code_uuid)
|
let res = await deleteInviteCode(org.id, invite.invite_code_uuid)
|
||||||
|
|
@ -74,7 +70,6 @@ function OrgAccess() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toast></Toast>
|
|
||||||
{!isLoading ? (
|
{!isLoading ? (
|
||||||
<>
|
<>
|
||||||
<div className="h-6"></div>
|
<div className="h-6"></div>
|
||||||
|
|
@ -126,7 +121,7 @@ function OrgAccess() {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex flex-col space-y-1 justify-center items-center h-full">
|
<div className="flex flex-col space-y-1 justify-center items-center h-full">
|
||||||
<Shield className="text-slate-400" size={40}></Shield>
|
<Ticket className="text-slate-400" size={40}></Ticket>
|
||||||
<div className="text-2xl text-slate-700 font-bold">
|
<div className="text-2xl text-slate-700 font-bold">
|
||||||
Closed
|
Closed
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -162,6 +157,7 @@ function OrgAccess() {
|
||||||
<tr className="font-bolder text-sm">
|
<tr className="font-bolder text-sm">
|
||||||
<th className="py-3 px-4">Code</th>
|
<th className="py-3 px-4">Code</th>
|
||||||
<th className="py-3 px-4">Signup link</th>
|
<th className="py-3 px-4">Signup link</th>
|
||||||
|
<th className="py-3 px-4">Type</th>
|
||||||
<th className="py-3 px-4">Expiration date</th>
|
<th className="py-3 px-4">Expiration date</th>
|
||||||
<th className="py-3 px-4">Actions</th>
|
<th className="py-3 px-4">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -189,6 +185,19 @@ function OrgAccess() {
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
{invite.usergroup_id ? (
|
||||||
|
<div className="flex space-x-2 items-center">
|
||||||
|
<UserSquare className="w-4 h-4" />
|
||||||
|
<span>Linked to a UserGroup</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex space-x-2 items-center">
|
||||||
|
<Users className="w-4 h-4" />
|
||||||
|
<span>Normal</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="py-3 px-4">
|
<td className="py-3 px-4">
|
||||||
{dayjs(invite.expiration_date)
|
{dayjs(invite.expiration_date)
|
||||||
.add(1, 'year')
|
.add(1, 'year')
|
||||||
|
|
@ -216,13 +225,36 @@ function OrgAccess() {
|
||||||
</tbody>
|
</tbody>
|
||||||
</>
|
</>
|
||||||
</table>
|
</table>
|
||||||
<div className='flex flex-row-reverse mt-3 mr-2'><button
|
<div className='flex flex-row-reverse mt-3 mr-2'>
|
||||||
onClick={() => createInvite()}
|
<Modal
|
||||||
className=" flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100"
|
isDialogOpen={
|
||||||
>
|
invitesModal
|
||||||
<Shield className="w-4 h-4" />
|
}
|
||||||
<span> Generate invite code</span>
|
onOpenChange={() =>
|
||||||
</button></div>
|
setInvitesModal(!invitesModal)
|
||||||
|
}
|
||||||
|
minHeight="no-min"
|
||||||
|
minWidth='lg'
|
||||||
|
dialogContent={
|
||||||
|
<OrgInviteCodeGenerate
|
||||||
|
setInvitesModal={setInvitesModal}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
dialogTitle="Generate Invite Code"
|
||||||
|
dialogDescription={
|
||||||
|
'Generate a new invite code for your organization'
|
||||||
|
}
|
||||||
|
dialogTrigger={
|
||||||
|
<button
|
||||||
|
className=" flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100"
|
||||||
|
>
|
||||||
|
<Ticket className="w-4 h-4" />
|
||||||
|
<span> Generate invite code</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
'use client'
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
|
import AddUserGroup from '@components/Objects/Modals/Dash/OrgUserGroups/AddUserGroup'
|
||||||
|
import ManageUsers from '@components/Objects/Modals/Dash/OrgUserGroups/ManageUsers'
|
||||||
|
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||||
|
import Modal from '@components/StyledElements/Modal/Modal'
|
||||||
|
import { getAPIUrl } from '@services/config/config'
|
||||||
|
import { deleteUserGroup } from '@services/usergroups/usergroups'
|
||||||
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
|
import { SquareUserRound, Users, X } from 'lucide-react'
|
||||||
|
import React from 'react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import useSWR, { mutate } from 'swr'
|
||||||
|
|
||||||
|
function OrgUserGroups() {
|
||||||
|
const org = useOrg() as any
|
||||||
|
const [userGroupManagementModal, setUserGroupManagementModal] = React.useState(false)
|
||||||
|
const [createUserGroupModal, setCreateUserGroupModal] = React.useState(false)
|
||||||
|
const [selectedUserGroup, setSelectedUserGroup] = React.useState(null) as any
|
||||||
|
|
||||||
|
const { data: usergroups } = useSWR(
|
||||||
|
org ? `${getAPIUrl()}usergroups/org/${org.id}` : null,
|
||||||
|
swrFetcher
|
||||||
|
)
|
||||||
|
|
||||||
|
const deleteUserGroupUI = async (usergroup_id: any) => {
|
||||||
|
const res = await deleteUserGroup(usergroup_id)
|
||||||
|
if (res.status == 200) {
|
||||||
|
mutate(`${getAPIUrl()}usergroups/org/${org.id}`)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUserGroupManagementModal = (usergroup_id: any) => {
|
||||||
|
setSelectedUserGroup(usergroup_id)
|
||||||
|
setUserGroupManagementModal(!userGroupManagementModal)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="h-6"></div>
|
||||||
|
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4">
|
||||||
|
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 ">
|
||||||
|
<h1 className="font-bold text-xl text-gray-800">Manage UserGroups & Users</h1>
|
||||||
|
<h2 className="text-gray-500 text-sm">
|
||||||
|
{' '}
|
||||||
|
UserGroups are a way to group users together to manage their access to the resources (Courses) in your organization.{' '}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
|
||||||
|
<thead className="bg-gray-100 text-gray-500 rounded-xl uppercase">
|
||||||
|
<tr className="font-bolder text-sm">
|
||||||
|
<th className="py-3 px-4">UserGroup</th>
|
||||||
|
<th className="py-3 px-4">Description</th>
|
||||||
|
<th className="py-3 px-4">Manage Users</th>
|
||||||
|
<th className="py-3 px-4">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<>
|
||||||
|
<tbody className="mt-5 bg-white rounded-md">
|
||||||
|
{usergroups?.map((usergroup: any) => (
|
||||||
|
<tr key={usergroup.id} className="border-b border-gray-100 text-sm">
|
||||||
|
<td className="py-3 px-4">{usergroup.name}</td>
|
||||||
|
<td className="py-3 px-4 ">{usergroup.description}</td>
|
||||||
|
<td className="py-3 px-4 ">
|
||||||
|
<Modal
|
||||||
|
isDialogOpen={
|
||||||
|
userGroupManagementModal &&
|
||||||
|
selectedUserGroup === usergroup.id
|
||||||
|
}
|
||||||
|
onOpenChange={() =>
|
||||||
|
handleUserGroupManagementModal(usergroup.id)
|
||||||
|
}
|
||||||
|
minHeight="lg"
|
||||||
|
minWidth='lg'
|
||||||
|
dialogContent={
|
||||||
|
<ManageUsers
|
||||||
|
usergroup_id={usergroup.id}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
dialogTitle="Manage UserGroup Users"
|
||||||
|
dialogDescription={
|
||||||
|
'Manage the users in this UserGroup'
|
||||||
|
}
|
||||||
|
dialogTrigger={
|
||||||
|
<button className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-yellow-700 rounded-md font-bold items-center text-sm text-yellow-100">
|
||||||
|
<Users className="w-4 h-4" />
|
||||||
|
<span> Manage Users</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4 ">
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
confirmationButtonText="Delete UserGroup"
|
||||||
|
confirmationMessage="Access to all resources will be removed for all users in this UserGroup. Are you sure you want to delete this UserGroup ?"
|
||||||
|
dialogTitle={'Delete UserGroup ?'}
|
||||||
|
dialogTrigger={
|
||||||
|
<button className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100">
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
<span> Delete</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
functionToExecute={() => {
|
||||||
|
deleteUserGroupUI(usergroup.id)
|
||||||
|
}}
|
||||||
|
status="warning"
|
||||||
|
></ConfirmationModal>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</>
|
||||||
|
</table>
|
||||||
|
<div className='flex justify-end mt-3 mr-2'>
|
||||||
|
<Modal
|
||||||
|
isDialogOpen={
|
||||||
|
createUserGroupModal
|
||||||
|
}
|
||||||
|
onOpenChange={() =>
|
||||||
|
setCreateUserGroupModal(!createUserGroupModal)
|
||||||
|
}
|
||||||
|
minHeight="no-min"
|
||||||
|
dialogContent={
|
||||||
|
<AddUserGroup
|
||||||
|
setCreateUserGroupModal={setCreateUserGroupModal}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
dialogTitle="Create a UserGroup"
|
||||||
|
dialogDescription={
|
||||||
|
'Create a new UserGroup to manage users'
|
||||||
|
}
|
||||||
|
dialogTrigger={
|
||||||
|
<button
|
||||||
|
className=" flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100"
|
||||||
|
>
|
||||||
|
<SquareUserRound className="w-4 h-4" />
|
||||||
|
<span>Create a UserGroup</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrgUserGroups
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
'use client';
|
||||||
|
import { useCourse } from '@components/Contexts/CourseContext';
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext';
|
||||||
|
import { getAPIUrl } from '@services/config/config';
|
||||||
|
import { linkResourcesToUserGroup } from '@services/usergroups/usergroups';
|
||||||
|
import { swrFetcher } from '@services/utils/ts/requests';
|
||||||
|
import { AlertTriangle, Info } from 'lucide-react';
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import useSWR, { mutate } from 'swr'
|
||||||
|
|
||||||
|
type LinkToUserGroupProps = {
|
||||||
|
// React function, todo: fix types
|
||||||
|
setUserGroupModal: any
|
||||||
|
}
|
||||||
|
|
||||||
|
function LinkToUserGroup(props: LinkToUserGroupProps) {
|
||||||
|
const course = useCourse() as any
|
||||||
|
const org = useOrg() as any
|
||||||
|
const courseStructure = course.courseStructure
|
||||||
|
|
||||||
|
const { data: usergroups } = useSWR(
|
||||||
|
courseStructure && org ? `${getAPIUrl()}usergroups/org/${org.id}` : null,
|
||||||
|
swrFetcher
|
||||||
|
)
|
||||||
|
const [selectedUserGroup, setSelectedUserGroup] = React.useState(null) as any
|
||||||
|
|
||||||
|
|
||||||
|
const handleLink = async () => {
|
||||||
|
console.log('selectedUserGroup', selectedUserGroup)
|
||||||
|
const res = await linkResourcesToUserGroup(selectedUserGroup, courseStructure.course_uuid)
|
||||||
|
if (res.status === 200) {
|
||||||
|
props.setUserGroupModal(false)
|
||||||
|
toast.success('Successfully linked to usergroup')
|
||||||
|
mutate(`${getAPIUrl()}usergroups/resource/${courseStructure.course_uuid}`)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (usergroups && usergroups.length > 0) {
|
||||||
|
setSelectedUserGroup(usergroups[0].id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, [usergroups])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col space-y-1 '>
|
||||||
|
<div className='flex bg-yellow-100 text-yellow-900 mx-auto w-fit mt-3 px-4 py-2 space-x-2 text-sm rounded-full items-center'>
|
||||||
|
<Info size={19} />
|
||||||
|
<h1 className=' font-medium'>Users that are not part of the UserGroup will no longer have access to this course</h1>
|
||||||
|
</div>
|
||||||
|
<div className='p-4 flex-row flex justify-between items-center'>
|
||||||
|
|
||||||
|
<div className='py-1'>
|
||||||
|
<span className='px-3 text-gray-400 font-bold rounded-full py-1 bg-gray-100 mx-3'>UserGroup Name </span>
|
||||||
|
<select
|
||||||
|
onChange={(e) => setSelectedUserGroup(e.target.value)}
|
||||||
|
defaultValue={selectedUserGroup}
|
||||||
|
>
|
||||||
|
{usergroups && usergroups.map((group: any) => (
|
||||||
|
<option key={group.id} value={group.id}>{group.name}</option>
|
||||||
|
))}
|
||||||
|
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className='py-3'>
|
||||||
|
<button onClick={() => { handleLink() }} className='bg-green-700 text-white font-bold px-4 py-2 rounded-md shadow'>Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LinkToUserGroup
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
|
import { getAPIUrl } from '@services/config/config'
|
||||||
|
import { createInviteCode, createInviteCodeWithUserGroup } from '@services/organizations/invites'
|
||||||
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
|
import { Shield, Ticket } from 'lucide-react'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import useSWR, { mutate } from 'swr'
|
||||||
|
|
||||||
|
type OrgInviteCodeGenerateProps = {
|
||||||
|
setInvitesModal: any
|
||||||
|
}
|
||||||
|
|
||||||
|
function OrgInviteCodeGenerate(props: OrgInviteCodeGenerateProps) {
|
||||||
|
const org = useOrg() as any
|
||||||
|
const [usergroup_id, setUsergroup_id] = React.useState(0);
|
||||||
|
const { data: usergroups } = useSWR(
|
||||||
|
org ? `${getAPIUrl()}usergroups/org/${org.id}` : null,
|
||||||
|
swrFetcher
|
||||||
|
)
|
||||||
|
|
||||||
|
async function createInviteWithUserGroup() {
|
||||||
|
let res = await createInviteCodeWithUserGroup(org.id, usergroup_id)
|
||||||
|
if (res.status == 200) {
|
||||||
|
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
|
||||||
|
props.setInvitesModal(false)
|
||||||
|
} else {
|
||||||
|
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createInvite() {
|
||||||
|
let res = await createInviteCode(org.id)
|
||||||
|
if (res.status == 200) {
|
||||||
|
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
|
||||||
|
props.setInvitesModal(false)
|
||||||
|
} else {
|
||||||
|
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (usergroups && usergroups.length > 0) {
|
||||||
|
setUsergroup_id(usergroups[0].id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, [usergroups])
|
||||||
|
return (
|
||||||
|
<div className='flex space-x-2 pt-2'>
|
||||||
|
<div className='flex bg-slate-100 w-full h-[140px] rounded-lg'>
|
||||||
|
<div className='flex flex-col mx-auto'>
|
||||||
|
<h1 className='mx-auto pt-4 text-gray-600 font-medium'>Invite Code linked to a UserGroup</h1>
|
||||||
|
<h2 className='mx-auto text-xs text-gray-600 font-medium'>On Signup, Users will be automatically linked to a UserGroup of your choice</h2>
|
||||||
|
<div className='flex items-center space-x-4 pt-3 mx-auto'>
|
||||||
|
<select
|
||||||
|
defaultValue={usergroup_id}
|
||||||
|
className='flex p-2 w-fit rounded-md text-sm bg-gray-100'>
|
||||||
|
{usergroups?.map((usergroup: any) => (
|
||||||
|
<option key={usergroup.id} value={usergroup.id}>
|
||||||
|
{usergroup.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className=''>
|
||||||
|
<button
|
||||||
|
onClick={createInviteWithUserGroup}
|
||||||
|
className="flex space-x-2 w-fit hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100"
|
||||||
|
>
|
||||||
|
<Ticket className="w-4 h-4" />
|
||||||
|
<span> Generate </span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex bg-slate-100 w-full h-[140px] rounded-lg'>
|
||||||
|
<div className='flex flex-col mx-auto'>
|
||||||
|
<h1 className='mx-auto pt-4 text-gray-600 font-medium'>Normal Invite Code</h1>
|
||||||
|
<h2 className='mx-auto text-xs text-gray-600 font-medium'>On Signup, User will not be linked to any UserGroup</h2>
|
||||||
|
<div className='mx-auto pt-4'>
|
||||||
|
<button
|
||||||
|
onClick={createInvite}
|
||||||
|
className="flex space-x-2 w-fit hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100"
|
||||||
|
>
|
||||||
|
<Ticket className="w-4 h-4" />
|
||||||
|
<span> Generate </span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrgInviteCodeGenerate
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
'use client'
|
||||||
|
import FormLayout, {
|
||||||
|
ButtonBlack,
|
||||||
|
Flex,
|
||||||
|
FormField,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
Input,
|
||||||
|
} from '@components/StyledElements/Form/Form'
|
||||||
|
import * as Form from '@radix-ui/react-form'
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
|
import React from 'react'
|
||||||
|
import { BarLoader } from 'react-spinners'
|
||||||
|
import { createUserGroup } from '@services/usergroups/usergroups'
|
||||||
|
import { mutate } from 'swr'
|
||||||
|
import { getAPIUrl } from '@services/config/config'
|
||||||
|
|
||||||
|
type AddUserGroupProps = {
|
||||||
|
setCreateUserGroupModal: any
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddUserGroup(props: AddUserGroupProps) {
|
||||||
|
const org = useOrg() as any
|
||||||
|
const [userGroupName, setUserGroupName] = React.useState('')
|
||||||
|
const [userGroupDescription, setUserGroupDescription] = React.useState('')
|
||||||
|
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
||||||
|
|
||||||
|
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setUserGroupName(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDescriptionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setUserGroupDescription(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
name: userGroupName,
|
||||||
|
description: userGroupDescription,
|
||||||
|
org_id: org.id
|
||||||
|
}
|
||||||
|
const res = await createUserGroup(obj)
|
||||||
|
if (res.status == 200) {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
mutate(`${getAPIUrl()}usergroups/org/${org.id}`)
|
||||||
|
props.setCreateUserGroupModal(false)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormLayout onSubmit={handleSubmit}>
|
||||||
|
<FormField name="name">
|
||||||
|
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormMessage match="valueMissing">
|
||||||
|
Please provide a ug name
|
||||||
|
</FormMessage>
|
||||||
|
</Flex>
|
||||||
|
<Form.Control asChild>
|
||||||
|
<Input onChange={handleNameChange} type="text" required />
|
||||||
|
</Form.Control>
|
||||||
|
</FormField>
|
||||||
|
<FormField name="description">
|
||||||
|
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<FormMessage match="valueMissing">
|
||||||
|
Please provide a ug description
|
||||||
|
</FormMessage>
|
||||||
|
</Flex>
|
||||||
|
<Form.Control asChild>
|
||||||
|
<Input onChange={handleDescriptionChange} type="text" required />
|
||||||
|
</Form.Control>
|
||||||
|
</FormField>
|
||||||
|
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||||
|
<Form.Submit asChild>
|
||||||
|
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<BarLoader
|
||||||
|
cssOverride={{ borderRadius: 60 }}
|
||||||
|
width={60}
|
||||||
|
color="#ffffff"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
'Create UserGroup'
|
||||||
|
)}
|
||||||
|
</ButtonBlack>
|
||||||
|
</Form.Submit>
|
||||||
|
</Flex>
|
||||||
|
</FormLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddUserGroup
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
|
import { getAPIUrl } from '@services/config/config'
|
||||||
|
import { linkUserToUserGroup, unLinkUserToUserGroup } from '@services/usergroups/usergroups'
|
||||||
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
|
import { Check, Plus, X } from 'lucide-react'
|
||||||
|
import React from 'react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import useSWR, { mutate } from 'swr'
|
||||||
|
|
||||||
|
|
||||||
|
type ManageUsersProps = {
|
||||||
|
usergroup_id: any
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManageUsers(props: ManageUsersProps) {
|
||||||
|
const org = useOrg() as any
|
||||||
|
const { data: OrgUsers } = useSWR(
|
||||||
|
org ? `${getAPIUrl()}orgs/${org.id}/users` : null,
|
||||||
|
swrFetcher
|
||||||
|
)
|
||||||
|
const { data: UGusers } = useSWR(
|
||||||
|
org ? `${getAPIUrl()}usergroups/${props.usergroup_id}/users` : null,
|
||||||
|
swrFetcher
|
||||||
|
)
|
||||||
|
|
||||||
|
const isUserPartOfGroup = (user_id: any) => {
|
||||||
|
if (UGusers) {
|
||||||
|
return UGusers.some((user: any) => user.id === user_id)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLinkUser = async (user_id: any) => {
|
||||||
|
const res = await linkUserToUserGroup(props.usergroup_id, user_id)
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success('User linked successfully')
|
||||||
|
mutate(`${getAPIUrl()}usergroups/${props.usergroup_id}/users`)
|
||||||
|
} else {
|
||||||
|
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUnlinkUser = async (user_id: any) => {
|
||||||
|
const res = await unLinkUserToUserGroup(props.usergroup_id, user_id)
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success('User unlinked successfully')
|
||||||
|
mutate(`${getAPIUrl()}usergroups/${props.usergroup_id}/users`)
|
||||||
|
} else {
|
||||||
|
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='py-3'>
|
||||||
|
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
|
||||||
|
<thead className="bg-gray-100 text-gray-500 rounded-xl uppercase">
|
||||||
|
<tr className="font-bolder text-sm">
|
||||||
|
<th className="py-3 px-4">User</th>
|
||||||
|
<th className="py-3 px-4">Linked</th>
|
||||||
|
<th className="py-3 px-4">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<>
|
||||||
|
<tbody className="mt-5 bg-white rounded-md">
|
||||||
|
{OrgUsers?.map((user: any) => (
|
||||||
|
<tr
|
||||||
|
key={user.user.id}
|
||||||
|
className="border-b border-gray-200 border-dashed text-sm"
|
||||||
|
>
|
||||||
|
<td className="py-3 px-4 flex space-x-2 items-center">
|
||||||
|
<span>
|
||||||
|
{user.user.first_name + ' ' + user.user.last_name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs bg-neutral-100 p-1 px-2 rounded-full text-neutral-400 font-semibold">
|
||||||
|
@{user.user.username}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
{isUserPartOfGroup(user.user.id) ?
|
||||||
|
<div className="space-x-1 flex w-fit px-4 py-1 bg-cyan-100 rounded-full items-center text-cyan-800">
|
||||||
|
<Check size={16} />
|
||||||
|
<span>Linked</span>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
<div className="space-x-1 flex w-fit px-4 py-1 bg-gray-100 rounded-full items-center text-gray-800">
|
||||||
|
<X size={16} />
|
||||||
|
<span>Not linked</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4 flex space-x-2 items-end">
|
||||||
|
<button
|
||||||
|
onClick={() => handleLinkUser(user.user.id)}
|
||||||
|
className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-cyan-700 rounded-md font-bold items-center text-sm text-cyan-100">
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span> Link</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleUnlinkUser(user.user.id)}
|
||||||
|
className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-gray-700 rounded-md font-bold items-center text-sm text-gray-100">
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
<span> Unlink</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageUsers
|
||||||
|
|
@ -15,7 +15,7 @@ export const FormLabelAndMessage = (props: {
|
||||||
message?: string
|
message?: string
|
||||||
}) => (
|
}) => (
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<FormLabel className="grow">{props.label}</FormLabel>
|
<FormLabel className="grow text-sm">{props.label}</FormLabel>
|
||||||
{(props.message && (
|
{(props.message && (
|
||||||
<div className="text-red-700 text-sm items-center rounded-md flex space-x-1">
|
<div className="text-red-700 text-sm items-center rounded-md flex space-x-1">
|
||||||
<Info size={10} />
|
<Info size={10} />
|
||||||
|
|
@ -35,7 +35,6 @@ export const FormField = styled(Form.Field, {
|
||||||
})
|
})
|
||||||
|
|
||||||
export const FormLabel = styled(Form.Label, {
|
export const FormLabel = styled(Form.Label, {
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
lineHeight: '35px',
|
lineHeight: '35px',
|
||||||
color: 'black',
|
color: 'black',
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ type ModalParams = {
|
||||||
onOpenChange: any
|
onOpenChange: any
|
||||||
isDialogOpen?: boolean
|
isDialogOpen?: boolean
|
||||||
minHeight?: 'sm' | 'md' | 'lg' | 'xl' | 'no-min'
|
minHeight?: 'sm' | 'md' | 'lg' | 'xl' | 'no-min'
|
||||||
|
minWidth?: 'sm' | 'md' | 'lg' | 'xl' | 'no-min'
|
||||||
}
|
}
|
||||||
|
|
||||||
const Modal = (params: ModalParams) => (
|
const Modal = (params: ModalParams) => (
|
||||||
|
|
@ -28,6 +29,7 @@ const Modal = (params: ModalParams) => (
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="overflow-auto scrollbar-w-2 scrollbar-h-2 scrollbar scrollbar-thumb-black/20 scrollbar-thumb-rounded-full scrollbar-track-rounded-full"
|
className="overflow-auto scrollbar-w-2 scrollbar-h-2 scrollbar scrollbar-thumb-black/20 scrollbar-thumb-rounded-full scrollbar-track-rounded-full"
|
||||||
minHeight={params.minHeight}
|
minHeight={params.minHeight}
|
||||||
|
minWidth={params.minWidth}
|
||||||
>
|
>
|
||||||
<DialogTopBar className="-space-y-1">
|
<DialogTopBar className="-space-y-1">
|
||||||
<DialogTitle>{params.dialogTitle}</DialogTitle>
|
<DialogTitle>{params.dialogTitle}</DialogTitle>
|
||||||
|
|
@ -103,6 +105,23 @@ const DialogContent = styled(Dialog.Content, {
|
||||||
minHeight: '900px',
|
minHeight: '900px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
minWidth: {
|
||||||
|
'no-min': {
|
||||||
|
minWidth: '0px',
|
||||||
|
},
|
||||||
|
sm: {
|
||||||
|
minWidth: '600px',
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
minWidth: '800px',
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
minWidth: '1000px',
|
||||||
|
},
|
||||||
|
xl: {
|
||||||
|
minWidth: '1200px',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,15 @@ export async function createInviteCode(org_id: any) {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createInviteCodeWithUserGroup(org_id: any, usergroup_id: number) {
|
||||||
|
const result = await fetch(
|
||||||
|
`${getAPIUrl()}orgs/${org_id}/invites_with_usergroups?usergroup_id=${usergroup_id}`,
|
||||||
|
RequestBody('POST', null, null)
|
||||||
|
)
|
||||||
|
const res = await getResponseMetadata(result)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteInviteCode(
|
export async function deleteInviteCode(
|
||||||
org_id: any,
|
org_id: any,
|
||||||
org_invite_code_uuid: string
|
org_invite_code_uuid: string
|
||||||
|
|
|
||||||
71
apps/web/services/usergroups/usergroups.ts
Normal file
71
apps/web/services/usergroups/usergroups.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { getAPIUrl } from '@services/config/config'
|
||||||
|
import { RequestBody, getResponseMetadata } from '@services/utils/ts/requests'
|
||||||
|
|
||||||
|
export async function getUserGroups(org_id: any) {
|
||||||
|
const result: any = await fetch(
|
||||||
|
`${getAPIUrl()}usergroups/org/${org_id}`,
|
||||||
|
RequestBody('GET', null, null)
|
||||||
|
)
|
||||||
|
const res = await getResponseMetadata(result)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUserGroup(body: any) {
|
||||||
|
const result: any = await fetch(
|
||||||
|
`${getAPIUrl()}usergroups`,
|
||||||
|
RequestBody('POST', body, null)
|
||||||
|
)
|
||||||
|
const res = await getResponseMetadata(result)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function linkUserToUserGroup(usergroup_id: any, user_id: any) {
|
||||||
|
const result: any = await fetch(
|
||||||
|
`${getAPIUrl()}usergroups/${usergroup_id}/add_users?user_ids=${user_id}`,
|
||||||
|
RequestBody('POST', null, null)
|
||||||
|
)
|
||||||
|
const res = await getResponseMetadata(result)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unLinkUserToUserGroup(usergroup_id: any, user_id: any) {
|
||||||
|
const result: any = await fetch(
|
||||||
|
`${getAPIUrl()}usergroups/${usergroup_id}/remove_users?user_ids=${user_id}`,
|
||||||
|
RequestBody('DELETE', null, null)
|
||||||
|
)
|
||||||
|
const res = await getResponseMetadata(result)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteUserGroup(usergroup_id: number) {
|
||||||
|
const result: any = await fetch(
|
||||||
|
`${getAPIUrl()}usergroups/${usergroup_id}`,
|
||||||
|
RequestBody('DELETE', null, null)
|
||||||
|
)
|
||||||
|
const res = await getResponseMetadata(result)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function linkResourcesToUserGroup(
|
||||||
|
usergroup_id: any,
|
||||||
|
resource_uuids: any
|
||||||
|
) {
|
||||||
|
const result: any = await fetch(
|
||||||
|
`${getAPIUrl()}usergroups/${usergroup_id}/add_resources?resource_uuids=${resource_uuids}`,
|
||||||
|
RequestBody('POST', null, null)
|
||||||
|
)
|
||||||
|
const res = await getResponseMetadata(result)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unLinkResourcesToUserGroup(
|
||||||
|
usergroup_id: any,
|
||||||
|
resource_uuids: any
|
||||||
|
) {
|
||||||
|
const result: any = await fetch(
|
||||||
|
`${getAPIUrl()}usergroups/${usergroup_id}/remove_resources?resource_uuids=${resource_uuids}`,
|
||||||
|
RequestBody('DELETE', null, null)
|
||||||
|
)
|
||||||
|
const res = await getResponseMetadata(result)
|
||||||
|
return res
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue