From 91cb5740ef4c07d6a07516eeb8a908762a30d4fd Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 19 Jun 2023 00:19:02 +0200 Subject: [PATCH] feat: add custom organization logo feature --- front/app/organizations/new/page.tsx | 3 +- .../organization/general/organization.tsx | 55 ++++- front/components/UI/Elements/Menu/Menu.tsx | 19 +- front/middleware.ts | 2 +- front/services/settings/org.ts | 11 +- src/routers/orgs.py | 10 +- src/security/security.py | 4 +- src/services/blocks/utils/upload_files.py | 1 - src/services/courses/courses.py | 195 +++++++++++++----- src/services/mocks/initial.py | 3 +- src/services/orgs/__init__.py | 0 src/services/orgs/logos.py | 22 ++ src/services/{ => orgs}/orgs.py | 180 +++++++++------- src/services/orgs/schemas/__init__.py | 0 src/services/orgs/schemas/orgs.py | 29 +++ src/services/trail.py | 2 +- 16 files changed, 396 insertions(+), 140 deletions(-) create mode 100644 src/services/orgs/__init__.py create mode 100644 src/services/orgs/logos.py rename src/services/{ => orgs}/orgs.py (53%) create mode 100644 src/services/orgs/schemas/__init__.py create mode 100644 src/services/orgs/schemas/orgs.py diff --git a/front/app/organizations/new/page.tsx b/front/app/organizations/new/page.tsx index 399623c5..d80467e0 100644 --- a/front/app/organizations/new/page.tsx +++ b/front/app/organizations/new/page.tsx @@ -28,7 +28,8 @@ const Organizations = () => { const handleSubmit = async (e: any) => { e.preventDefault(); console.log({ name, description, email }); - const status = await createNewOrganization({ name, description, email, slug , default: false }); + let logo = '' + const status = await createNewOrganization({ name, description, email, logo, slug, default: false }); alert(JSON.stringify(status)); }; diff --git a/front/app/orgs/[orgslug]/settings/organization/general/organization.tsx b/front/app/orgs/[orgslug]/settings/organization/general/organization.tsx index df5cdb9a..bc26be95 100644 --- a/front/app/orgs/[orgslug]/settings/organization/general/organization.tsx +++ b/front/app/orgs/[orgslug]/settings/organization/general/organization.tsx @@ -1,23 +1,51 @@ "use client"; -import React from 'react' +import React, { useState } from 'react' import { Field, Form, Formik } from 'formik'; -import { updateOrganization } from '@services/settings/org'; +import { updateOrganization, uploadOrganizationLogo } from '@services/settings/org'; +import { UploadCloud } from 'lucide-react'; +import { revalidateTags } from '@services/utils/ts/requests'; interface OrganizationValues { name: string; description: string; slug: string; + logo: string; email: string; } function OrganizationClient(props: any) { + const [selectedFile, setSelectedFile] = useState(null); + + // ... + + const handleFileChange = (event: React.ChangeEvent) => { + if (event.target.files && event.target.files.length > 0) { + const file = event.target.files[0]; + setSelectedFile(file); + } + }; + + const uploadLogo = async () => { + if (selectedFile) { + let org_id = org.org_id; + await uploadOrganizationLogo(org_id, selectedFile); + setSelectedFile(null); // Reset the selected file + revalidateTags(['organizations']); + // reload the page + // terrible hack, it will fixed later + window.location.reload(); + } + }; + + const org = props.org; let orgValues: OrganizationValues = { name: org.name, description: org.description, slug: org.slug, + logo: org.logo, email: org.email } @@ -26,6 +54,7 @@ function OrganizationClient(props: any) { await updateOrganization(org_id, values); } + return (

Organization Settings

@@ -62,6 +91,28 @@ function OrganizationClient(props: any) { name="description" /> + + +
+ + +
+ + diff --git a/front/components/UI/Elements/Menu/Menu.tsx b/front/components/UI/Elements/Menu/Menu.tsx index 12933cb1..804e0952 100644 --- a/front/components/UI/Elements/Menu/Menu.tsx +++ b/front/components/UI/Elements/Menu/Menu.tsx @@ -3,16 +3,17 @@ import React from "react"; import learnhouseLogo from "public/learnhouse_logo.png"; import Link from "next/link"; import Image from "next/image"; -import { getUriWithOrg } from "@services/config/config"; +import { getBackendUrl, getUriWithOrg } from "@services/config/config"; import { getOrganizationContextInfo, getOrganizationContextInfoNoAsync } from "@services/organizations/orgs"; import ClientComponentSkeleton from "@components/UI/Utils/ClientComp"; import { HeaderProfileBox } from "@components/Security/HeaderProfileBox"; -export const Menu = (props: any) => { +export const Menu = async (props: any) => { const orgslug = props.orgslug; - const org = getOrganizationContextInfoNoAsync(orgslug, { revalidate: 1800, tags: ['organizations'] }); + const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); console.log(org); + return ( <>
@@ -20,8 +21,16 @@ export const Menu = (props: any) => {
- - + {org?.logo ? ( + Learnhouse + ) : ( + + )}
diff --git a/front/middleware.ts b/front/middleware.ts index 983566d7..4a8f5280 100644 --- a/front/middleware.ts +++ b/front/middleware.ts @@ -25,7 +25,7 @@ export default function middleware(req: NextRequest) { // Organizations & Global settings if (pathname.startsWith("/organizations")) { - return NextResponse.rewrite(new URL("/organizations", req.url)); + return NextResponse.rewrite(new URL(pathname, req.url)); } // Dynamic Pages Editor diff --git a/front/services/settings/org.ts b/front/services/settings/org.ts index 786eb56a..3007f8a7 100644 --- a/front/services/settings/org.ts +++ b/front/services/settings/org.ts @@ -1,5 +1,5 @@ import { getAPIUrl } from "@services/config/config"; -import { RequestBody, errorHandling } from "@services/utils/ts/requests"; +import { RequestBody, errorHandling, RequestBodyForm } from "@services/utils/ts/requests"; /* This file includes only POST, PUT, DELETE requests @@ -11,3 +11,12 @@ export async function updateOrganization(org_id: string, data: any) { const res = await errorHandling(result); return res; } + +export async function uploadOrganizationLogo(org_id: string, logo_file: any) { + // Send file thumbnail as form data + const formData = new FormData(); + formData.append("logo_file", logo_file); + const result: any = await fetch(`${getAPIUrl()}orgs/` + org_id + "/logo", RequestBodyForm("PUT", formData, null)); + const res = await errorHandling(result); + return res; +} \ No newline at end of file diff --git a/src/routers/orgs.py b/src/routers/orgs.py index 579c0c02..f3e2928f 100644 --- a/src/routers/orgs.py +++ b/src/routers/orgs.py @@ -1,7 +1,7 @@ -from fastapi import APIRouter, Depends, Request +from fastapi import APIRouter, Depends, Request, UploadFile from src.security.auth import get_current_user -from src.services.orgs import Organization, create_org, delete_org, get_organization, get_organization_by_slug, get_orgs_by_user, update_org +from src.services.orgs.orgs import Organization, create_org, delete_org, get_organization, get_organization_by_slug, get_orgs_by_user, update_org, update_org_logo from src.services.users.users import PublicUser, User @@ -31,6 +31,12 @@ async def api_get_org_by_slug(request: Request, org_slug: str, current_user: Use """ return await get_organization_by_slug(request, org_slug) +@router.put("/{org_id}/logo") +async def api_update_org_logo(request: Request, org_id: str, logo_file:UploadFile, current_user: PublicUser = Depends(get_current_user)): + """ + Get single Org by Slug + """ + return await update_org_logo(request=request,logo_file=logo_file, org_id=org_id, current_user=current_user) @router.get("/user/page/{page}/limit/{limit}") async def api_user_orgs(request: Request, page: int, limit: int, current_user: PublicUser = Depends(get_current_user)): diff --git a/src/security/security.py b/src/security/security.py index 30c935c5..348adf41 100644 --- a/src/security/security.py +++ b/src/security/security.py @@ -3,7 +3,7 @@ from passlib.context import CryptContext from passlib.hash import pbkdf2_sha256 from src.services.roles.schemas.roles import RoleInDB -from src.services.users.schemas.users import UserInDB +from src.services.users.schemas.users import UserInDB, UserRolesInOrganization ### 🔒 JWT ############################################################## @@ -108,7 +108,7 @@ async def check_element_type(element_id): status_code=status.HTTP_409_CONFLICT, detail="Issue verifying element nature") -async def check_user_role_org_with_element_org(request: Request, element_id: str, roles_list: list[str]): +async def check_user_role_org_with_element_org(request: Request, element_id: str, roles_list: list[UserRolesInOrganization]): element_type = await check_element_type(element_id) element = request.app.db[element_type] diff --git a/src/services/blocks/utils/upload_files.py b/src/services/blocks/utils/upload_files.py index 19a79355..c9ac4e0c 100644 --- a/src/services/blocks/utils/upload_files.py +++ b/src/services/blocks/utils/upload_files.py @@ -49,6 +49,5 @@ async def upload_file_and_return_file_object(request: Request, file: UploadFile, f.write(file_binary) f.close() - # TODO: do some error handling here return uploadable_file diff --git a/src/services/courses/courses.py b/src/services/courses/courses.py index 06c20ef3..1be805f4 100644 --- a/src/services/courses/courses.py +++ b/src/services/courses/courses.py @@ -46,6 +46,7 @@ class CourseChapterInDB(CourseChapter): creationDate: str updateDate: str + #### Classes #################################################### # TODO : Add courses photo & cover upload and delete @@ -55,6 +56,7 @@ class CourseChapterInDB(CourseChapter): # CRUD #################################################### + async def get_course(request: Request, course_id: str, current_user: PublicUser): courses = request.app.db["courses"] @@ -65,7 +67,8 @@ async def get_course(request: Request, course_id: str, current_user: PublicUser) if not course: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + ) course = Course(**course) return course @@ -83,10 +86,12 @@ async def get_course_meta(request: Request, course_id: str, current_user: Public if not course: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + ) - coursechapters = await courses.find_one({"course_id": course_id}, { - "chapters_content": 1, "_id": 0}) + coursechapters = await courses.find_one( + {"course_id": course_id}, {"chapters_content": 1, "_id": 0} + ) # activities coursechapter_activityIds_global = [] @@ -103,42 +108,66 @@ async def get_course_meta(request: Request, course_id: str, current_user: Public coursechapter_activityIds_global.append(activity) chapters[coursechapter.coursechapter_id] = { - "id": coursechapter.coursechapter_id, "name": coursechapter.name, "activityIds": coursechapter_activityIds + "id": coursechapter.coursechapter_id, + "name": coursechapter.name, + "activityIds": coursechapter_activityIds, } # activities activities_list = {} - for activity in await activities.find({"activity_id": {"$in": coursechapter_activityIds_global}}).to_list(length=100): + for activity in await activities.find( + {"activity_id": {"$in": coursechapter_activityIds_global}} + ).to_list(length=100): activity = ActivityInDB(**activity) activities_list[activity.activity_id] = { - "id": activity.activity_id, "name": activity.name, "type": activity.type, "content": activity.content + "id": activity.activity_id, + "name": activity.name, + "type": activity.type, + "content": activity.content, } chapters_list_with_activities = [] for chapter in chapters: chapters_list_with_activities.append( - {"id": chapters[chapter]["id"], "name": chapters[chapter]["name"], "activities": [activities_list[activity] for activity in chapters[chapter]["activityIds"]]}) + { + "id": chapters[chapter]["id"], + "name": chapters[chapter]["name"], + "activities": [ + activities_list[activity] + for activity in chapters[chapter]["activityIds"] + ], + } + ) course = CourseInDB(**course) # Get activity by user trail = await trails.find_one( - {"courses.course_id": course_id, "user_id": current_user.user_id}) + {"courses.course_id": course_id, "user_id": current_user.user_id} + ) print(trail) if trail: # get only the course where course_id == course_id trail_course = next( - (course for course in trail["courses"] if course["course_id"] == course_id), None) + (course for course in trail["courses"] if course["course_id"] == course_id), + None, + ) else: trail_course = "" return { "course": course, "chapters": chapters_list_with_activities, - "trail": trail_course + "trail": trail_course, } -async def create_course(request: Request, course_object: Course, org_id: str, current_user: PublicUser, thumbnail_file: UploadFile | None = None): +async def create_course( + request: Request, + course_object: Course, + org_id: str, + current_user: PublicUser, + thumbnail_file: UploadFile | None = None, +): courses = request.app.db["courses"] # generate course_id with uuid4 @@ -147,27 +176,42 @@ async def create_course(request: Request, course_object: Course, org_id: str, cu # TODO(fix) : the implementation here is clearly not the best one (this entire function) course_object.org_id = org_id course_object.chapters_content = [] - await verify_user_rights_with_roles(request, "create", current_user.user_id, course_id, org_id) + await verify_user_rights_with_roles( + request, "create", current_user.user_id, course_id, org_id + ) - if thumbnail_file: - name_in_disk = f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" + if thumbnail_file and thumbnail_file.filename: + name_in_disk = ( + f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" + ) await upload_thumbnail(thumbnail_file, name_in_disk) course_object.thumbnail = name_in_disk - course = CourseInDB(course_id=course_id, authors=[ - current_user.user_id], creationDate=str(datetime.now()), updateDate=str(datetime.now()), **course_object.dict()) + course = CourseInDB( + course_id=course_id, + authors=[current_user.user_id], + creationDate=str(datetime.now()), + updateDate=str(datetime.now()), + **course_object.dict(), + ) course_in_db = await courses.insert_one(course.dict()) if not course_in_db: raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Unavailable database", + ) return course.dict() -async def update_course_thumbnail(request: Request, course_id: str, current_user: PublicUser, thumbnail_file: UploadFile | None = None): - +async def update_course_thumbnail( + request: Request, + course_id: str, + current_user: PublicUser, + thumbnail_file: UploadFile | None = None, +): # verify course rights await verify_rights(request, course_id, current_user, "update") @@ -178,26 +222,34 @@ async def update_course_thumbnail(request: Request, course_id: str, current_user if course: creationDate = course["creationDate"] authors = course["authors"] - if thumbnail_file: + if thumbnail_file and thumbnail_file.filename: name_in_disk = f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" course = Course(**course).copy(update={"thumbnail": name_in_disk}) await upload_thumbnail(thumbnail_file, name_in_disk) - updated_course = CourseInDB(course_id=course_id, creationDate=creationDate, - authors=authors, updateDate=str(datetime.now()), **course.dict()) + updated_course = CourseInDB( + course_id=course_id, + creationDate=creationDate, + authors=authors, + updateDate=str(datetime.now()), + **course.dict(), + ) - await courses.update_one({"course_id": course_id}, { - "$set": updated_course.dict()}) + await courses.update_one( + {"course_id": course_id}, {"$set": updated_course.dict()} + ) return CourseInDB(**updated_course.dict()) else: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + ) -async def update_course(request: Request, course_object: Course, course_id: str, current_user: PublicUser): - +async def update_course( + request: Request, course_object: Course, course_id: str, current_user: PublicUser +): # verify course rights await verify_rights(request, course_id, current_user, "update") @@ -213,20 +265,26 @@ async def update_course(request: Request, course_object: Course, course_id: str, datetime_object = datetime.now() updated_course = CourseInDB( - course_id=course_id, creationDate=creationDate, authors=authors, updateDate=str(datetime_object), **course_object.dict()) + course_id=course_id, + creationDate=creationDate, + authors=authors, + updateDate=str(datetime_object), + **course_object.dict(), + ) - await courses.update_one({"course_id": course_id}, { - "$set": updated_course.dict()}) + await courses.update_one( + {"course_id": course_id}, {"$set": updated_course.dict()} + ) return CourseInDB(**updated_course.dict()) else: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + ) async def delete_course(request: Request, course_id: str, current_user: PublicUser): - # verify course rights await verify_rights(request, course_id, current_user, "delete") @@ -236,7 +294,8 @@ async def delete_course(request: Request, course_id: str, current_user: PublicUs if not course: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + ) isDeleted = await courses.delete_one({"course_id": course_id}) @@ -244,24 +303,38 @@ async def delete_course(request: Request, course_id: str, current_user: PublicUs return {"detail": "Course deleted"} else: raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Unavailable database", + ) + #################################################### # Misc #################################################### -async def get_courses(request: Request, page: int = 1, limit: int = 10, org_id: str | None = None): +async def get_courses( + request: Request, page: int = 1, limit: int = 10, org_id: str | None = None +): courses = request.app.db["courses"] # TODO : Get only courses that user is admin/has roles of # get all courses from database - all_courses = courses.find({"org_id": org_id}).sort( - "name", 1).skip(10 * (page - 1)).limit(limit) + all_courses = ( + courses.find({"org_id": org_id}) + .sort("name", 1) + .skip(10 * (page - 1)) + .limit(limit) + ) - return [json.loads(json.dumps(course, default=str)) for course in await all_courses.to_list(length=100)] + return [ + json.loads(json.dumps(course, default=str)) + for course in await all_courses.to_list(length=100) + ] -async def get_courses_orgslug(request: Request, page: int = 1, limit: int = 10, org_slug: str | None = None): +async def get_courses_orgslug( + request: Request, page: int = 1, limit: int = 10, org_slug: str | None = None +): courses = request.app.db["courses"] orgs = request.app.db["organizations"] # TODO : Get only courses that user is admin/has roles of @@ -271,37 +344,61 @@ async def get_courses_orgslug(request: Request, page: int = 1, limit: int = 10, if not org: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + ) # get all courses from database - all_courses = courses.find({"org_id": org['org_id']}).sort( - "name", 1).skip(10 * (page - 1)).limit(limit) + all_courses = ( + courses.find({"org_id": org["org_id"]}) + .sort("name", 1) + .skip(10 * (page - 1)) + .limit(limit) + ) - return [json.loads(json.dumps(course, default=str)) for course in await all_courses.to_list(length=100)] + return [ + json.loads(json.dumps(course, default=str)) + for course in await all_courses.to_list(length=100) + ] #### Security #################################################### -async def verify_rights(request: Request, course_id: str, current_user: PublicUser | AnonymousUser, action: str): +async def verify_rights( + request: Request, + course_id: str, + current_user: PublicUser | AnonymousUser, + action: str, +): courses = request.app.db["courses"] course = await courses.find_one({"course_id": course_id}) - if current_user.user_id == "anonymous" and course["public"] is True and action == "read": + if ( + current_user.user_id == "anonymous" + and course["public"] is True + and action == "read" + ): return True if not course: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course/CourseChapter does not exist") + status_code=status.HTTP_409_CONFLICT, + detail="Course/CourseChapter does not exist", + ) - hasRoleRights = await verify_user_rights_with_roles(request, action, current_user.user_id, course_id, course["org_id"]) + hasRoleRights = await verify_user_rights_with_roles( + request, action, current_user.user_id, course_id, course["org_id"] + ) isAuthor = current_user.user_id in course["authors"] if not hasRoleRights and not isAuthor: raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail="Roles/Ownership : Insufficient rights to perform this action") + status_code=status.HTTP_403_FORBIDDEN, + detail="Roles/Ownership : Insufficient rights to perform this action", + ) return True + #### Security #################################################### diff --git a/src/services/mocks/initial.py b/src/services/mocks/initial.py index 8f76684a..70bdaa7c 100644 --- a/src/services/mocks/initial.py +++ b/src/services/mocks/initial.py @@ -9,7 +9,7 @@ from src.services.courses.chapters import CourseChapter, create_coursechapter from src.services.courses.activities.activities import Activity, create_activity from src.services.users.users import PublicUser, UserInDB -from src.services.orgs import Organization, create_org +from src.services.orgs.orgs import Organization, create_org from src.services.roles.schemas.roles import Permission, Elements, RoleInDB from src.services.courses.courses import CourseInDB from faker import Faker @@ -133,6 +133,7 @@ async def create_initial_data(request: Request): description=fake.unique.text(), email=fake.unique.email(), slug=slug, + logo="", default=False ) organizations.append(org) diff --git a/src/services/orgs/__init__.py b/src/services/orgs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/services/orgs/logos.py b/src/services/orgs/logos.py new file mode 100644 index 00000000..e2e4bff8 --- /dev/null +++ b/src/services/orgs/logos.py @@ -0,0 +1,22 @@ +import os +from uuid import uuid4 + + +async def upload_org_logo(logo_file): + contents = logo_file.file.read() + name_in_disk = f"{uuid4()}.{logo_file.filename.split('.')[-1]}" + + try: + if not os.path.exists("content/uploads/logos"): + os.makedirs("content/uploads/logos") + + with open(f"content/uploads/logos/{name_in_disk}", "wb") as f: + f.write(contents) + f.close() + + except Exception: + return {"message": "There was an error uploading the file"} + finally: + logo_file.file.close() + + return name_in_disk diff --git a/src/services/orgs.py b/src/services/orgs/orgs.py similarity index 53% rename from src/services/orgs.py rename to src/services/orgs/orgs.py index ae570e75..8fc78b83 100644 --- a/src/services/orgs.py +++ b/src/services/orgs/orgs.py @@ -1,40 +1,16 @@ import json from typing import Optional from uuid import uuid4 -from click import Option -from pydantic import BaseModel +from src.services.orgs.logos import upload_org_logo +from src.services.orgs.schemas.orgs import ( + Organization, + OrganizationInDB, + PublicOrganization, +) from src.services.users.schemas.users import UserOrganization from src.services.users.users import PublicUser from src.security.security import * -from fastapi import HTTPException, status, Request - -#### Classes #################################################### - - -class Organization(BaseModel): - name: str - description: str - email: str - slug: str - default: Optional[bool] - - -class OrganizationInDB(Organization): - org_id: str - - -class PublicOrganization(Organization): - name: str - description: str - email: str - slug: str - org_id: str - - def __getitem__(self, item): - return getattr(self, item) - - -#### Classes #################################################### +from fastapi import HTTPException, UploadFile, status, Request async def get_organization(request: Request, org_id: str): @@ -44,7 +20,8 @@ async def get_organization(request: Request, org_id: str): if not org: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + ) org = PublicOrganization(**org) return org @@ -57,13 +34,16 @@ async def get_organization_by_slug(request: Request, org_slug: str): if not org: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + ) org = PublicOrganization(**org) return org -async def create_org(request: Request, org_object: Organization, current_user: PublicUser): +async def create_org( + request: Request, org_object: Organization, current_user: PublicUser +): orgs = request.app.db["organizations"] user = request.app.db["users"] @@ -72,7 +52,9 @@ async def create_org(request: Request, org_object: Organization, current_user: P if isOrgAvailable: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization slug already exists") + status_code=status.HTTP_409_CONFLICT, + detail="Organization slug already exists", + ) # generate org_id with uuid4 org_id = str(f"org_{uuid4()}") @@ -82,25 +64,33 @@ async def create_org(request: Request, org_object: Organization, current_user: P org_in_db = await orgs.insert_one(org.dict()) user_organization: UserOrganization = UserOrganization( - org_id=org_id, org_role="owner") + org_id=org_id, org_role="owner" + ) # add org to user - await user.update_one({"user_id": current_user.user_id}, { - "$addToSet": {"orgs": user_organization.dict()}}) - - # add role admin to org - await user.update_one({"user_id": current_user.user_id}, { - "$addToSet": {"roles": {"org_id": org_id, "role_id": "role_admin"}}}) + await user.update_one( + {"user_id": current_user.user_id}, + {"$addToSet": {"orgs": user_organization.dict()}}, + ) + + # add role admin to org + await user.update_one( + {"user_id": current_user.user_id}, + {"$addToSet": {"roles": {"org_id": org_id, "role_id": "role_admin"}}}, + ) if not org_in_db: raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Unavailable database", + ) return org.dict() -async def update_org(request: Request, org_object: Organization, org_id: str, current_user: PublicUser): - +async def update_org( + request: Request, org_object: Organization, org_id: str, current_user: PublicUser +): # verify org rights await verify_org_rights(request, org_id, current_user, "update") @@ -108,21 +98,38 @@ async def update_org(request: Request, org_object: Organization, org_id: str, cu org = await orgs.find_one({"org_id": org_id}) - if not org: - - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist") - - updated_org = OrganizationInDB( - org_id=org_id, **org_object.dict()) + updated_org = OrganizationInDB(org_id=org_id, **org_object.dict()) + # update org await orgs.update_one({"org_id": org_id}, {"$set": updated_org.dict()}) - return Organization(**updated_org.dict()) + + return updated_org.dict() + + +async def update_org_logo( + request: Request, logo_file: UploadFile, org_id: str, current_user: PublicUser +): + # verify org rights + await verify_org_rights(request, org_id, current_user, "update") + + orgs = request.app.db["organizations"] + + org = await orgs.find_one({"org_id": org_id}) + + + name_in_disk = await upload_org_logo(logo_file) + + # update org + org = await orgs.update_one({"org_id": org_id}, {"$set": {"logo": name_in_disk}}) + + return {"detail": "Logo updated"} + + + async def delete_org(request: Request, org_id: str, current_user: PublicUser): - await verify_org_rights(request, org_id, current_user, "delete") orgs = request.app.db["organizations"] @@ -131,7 +138,8 @@ async def delete_org(request: Request, org_id: str, current_user: PublicUser): if not org: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + ) isDeleted = await orgs.delete_one({"org_id": org_id}) @@ -143,56 +151,80 @@ async def delete_org(request: Request, org_id: str, current_user: PublicUser): return {"detail": "Org deleted"} else: raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Unavailable database", + ) -async def get_orgs_by_user(request: Request, user_id: str, page: int = 1, limit: int = 10): +async def get_orgs_by_user( + request: Request, user_id: str, page: int = 1, limit: int = 10 +): orgs = request.app.db["organizations"] user = request.app.db["users"] if user_id == "anonymous": - - # raise error + # raise error raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User not logged in") - + status_code=status.HTTP_409_CONFLICT, detail="User not logged in" + ) + # get user orgs user_orgs = await user.find_one({"user_id": user_id}) org_ids: list[UserOrganization] = [] for org in user_orgs["orgs"]: - if org["org_role"] == "owner" or org["org_role"] == "editor" or org["org_role"] == "member": + if ( + org["org_role"] == "owner" + or org["org_role"] == "editor" + or org["org_role"] == "member" + ): org_ids.append(org["org_id"]) # find all orgs where org_id is in org_ids array - all_orgs = orgs.find({"org_id": {"$in": org_ids}}).sort( - "name", 1).skip(10 * (page - 1)).limit(100) + all_orgs = ( + orgs.find({"org_id": {"$in": org_ids}}) + .sort("name", 1) + .skip(10 * (page - 1)) + .limit(100) + ) - return [json.loads(json.dumps(org, default=str)) for org in await all_orgs.to_list(length=100)] + return [ + json.loads(json.dumps(org, default=str)) + for org in await all_orgs.to_list(length=100) + ] #### Security #################################################### -async def verify_org_rights(request: Request, org_id: str, current_user: PublicUser, action: str,): + +async def verify_org_rights( + request: Request, + org_id: str, + current_user: PublicUser, + action: str, +): orgs = request.app.db["organizations"] org = await orgs.find_one({"org_id": org_id}) if not org: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist") + status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + ) - # check if is owner of org - # todo check if is admin of org + hasRoleRights = await verify_user_rights_with_roles( + request, action, current_user.user_id, org_id, org_id + ) - hasRoleRights = await verify_user_rights_with_roles(request, action, current_user.user_id, org_id, org_id) - - # if not hasRoleRights and not isOwner: - # raise HTTPException( - # status_code=status.HTTP_403_FORBIDDEN, detail="You do not have rights to this organization") + if not hasRoleRights: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You do not have rights to this organization", + ) return True + #### Security #################################################### diff --git a/src/services/orgs/schemas/__init__.py b/src/services/orgs/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/services/orgs/schemas/orgs.py b/src/services/orgs/schemas/orgs.py new file mode 100644 index 00000000..82da07e2 --- /dev/null +++ b/src/services/orgs/schemas/orgs.py @@ -0,0 +1,29 @@ +from typing import Optional +from pydantic import BaseModel +from src.security.security import * + +#### Classes #################################################### + + +class Organization(BaseModel): + name: str + description: str + email: str + slug: str + logo: Optional[str] + default: Optional[bool] = False + + +class OrganizationInDB(Organization): + org_id: str + + +class PublicOrganization(Organization): + name: str + description: str + email: str + slug: str + org_id: str + + def __getitem__(self, item): + return getattr(self, item) diff --git a/src/services/trail.py b/src/services/trail.py index 956ec4e8..bb4f88eb 100644 --- a/src/services/trail.py +++ b/src/services/trail.py @@ -4,7 +4,7 @@ from uuid import uuid4 from fastapi import HTTPException, Request, status from pydantic import BaseModel from src.services.courses.chapters import get_coursechapters_meta -from src.services.orgs import PublicOrganization +from src.services.orgs.orgs import PublicOrganization from src.services.users.users import PublicUser