mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: add custom organization logo feature
This commit is contained in:
parent
8c058db5c6
commit
91cb5740ef
16 changed files with 396 additions and 140 deletions
|
|
@ -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));
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<File | null>(null);
|
||||
|
||||
// ...
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div>
|
||||
<h1 className='text-3xl font-bold'>Organization Settings</h1>
|
||||
|
|
@ -62,6 +91,28 @@ function OrganizationClient(props: any) {
|
|||
name="description"
|
||||
/>
|
||||
|
||||
<label className="block mb-2 font-bold" htmlFor="slug">
|
||||
Logo
|
||||
</label>
|
||||
|
||||
<div className="flex items-center justify-center w-full ">
|
||||
<input
|
||||
className="w-full px-4 py-2 mr-1 border rounded-lg bg-gray-200 cursor-not-allowed"
|
||||
type="file"
|
||||
name="logo"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={uploadLogo}
|
||||
disabled={isSubmitting || selectedFile === null}
|
||||
className="px-6 py-3 text-white bg-gray-500 rounded-lg hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-black"
|
||||
>
|
||||
<UploadCloud size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<label className="block mb-2 font-bold" htmlFor="slug">
|
||||
Slug
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
<div className="h-[60px]"></div>
|
||||
|
|
@ -20,8 +21,16 @@ export const Menu = (props: any) => {
|
|||
<div className="logo flex ">
|
||||
<Link href={getUriWithOrg(orgslug, "/")}>
|
||||
<div className="flex w-auto h-9 rounded-md items-center m-auto justify-center" >
|
||||
<LearnHouseLogo></LearnHouseLogo>
|
||||
|
||||
{org?.logo ? (
|
||||
<img
|
||||
src={`${getBackendUrl()}content/uploads/logos/${org?.logo}`}
|
||||
alt="Learnhouse"
|
||||
style={{ width: "auto", height: "100%" }}
|
||||
className="rounded-md"
|
||||
/>
|
||||
) : (
|
||||
<LearnHouseLogo></LearnHouseLogo>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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)):
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ####################################################
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
0
src/services/orgs/__init__.py
Normal file
0
src/services/orgs/__init__.py
Normal file
22
src/services/orgs/logos.py
Normal file
22
src/services/orgs/logos.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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()}})
|
||||
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": {"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,18 +151,22 @@ 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 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})
|
||||
|
|
@ -162,37 +174,57 @@ async def get_orgs_by_user(request: Request, user_id: str, page: int = 1, limit:
|
|||
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 ####################################################
|
||||
0
src/services/orgs/schemas/__init__.py
Normal file
0
src/services/orgs/schemas/__init__.py
Normal file
29
src/services/orgs/schemas/orgs.py
Normal file
29
src/services/orgs/schemas/orgs.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue