mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
Merge branch 'dev' of https://github.com/learnhouse/learnhouse into add_precommit_and_docker_compose_healthcheck
This commit is contained in:
commit
5558fc0c89
143 changed files with 3677 additions and 1857 deletions
22
.github/workflows/web-lint.yaml
vendored
22
.github/workflows/web-lint.yaml
vendored
|
|
@ -9,16 +9,22 @@ on:
|
|||
jobs:
|
||||
next-lint:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
version: 8
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "pnpm"
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: pnpm install
|
||||
working-directory: ./apps/web
|
||||
- name: Lint code
|
||||
run: npm run lint
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
working-directory: ./apps/web
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -15,14 +15,15 @@ LearnHouse is an open source platform that makes it easy for anyone to provide w
|
|||

|
||||
|
||||
- 📄✨Dynamic notion-like pages
|
||||
- 👨🎓 Easy to use
|
||||
- 🏎️ Easy to use
|
||||
- 👥 Multi-Organization
|
||||
- 📹 Supports Uploadable Videos and external videos like YouTube
|
||||
- 📄 Supports documents like PDF
|
||||
- 🍱 Course Collections
|
||||
- 👨🎓 Users Management
|
||||
- 🙋 Quizzes
|
||||
- 👟 Course progress
|
||||
- ⚡ (Incoming) Live Collaboration
|
||||
- ✨ LearnHouse AI : The Teachers and Students copilot
|
||||
- More to come
|
||||
|
||||
## Community
|
||||
|
|
@ -48,13 +49,14 @@ Thank you for you interest 💖, here is how you can help :
|
|||
|
||||
LearnHouse uses a number of open source projects to work properly:
|
||||
|
||||
- **Next.js** (13 with the App Directory) - The React Framework
|
||||
- **Next.js** (14 with the App Directory) - The React Framework
|
||||
- **TailwindCSS** - Styling
|
||||
- **Radix UI** - Accessible UI Components
|
||||
- **Tiptap** - An editor framework and headless wrapper around ProseMirror
|
||||
- **FastAPI** - A high performance, async API framework for Python
|
||||
- **YJS** - Shared data types for building collaborative software
|
||||
- **MongoDB** - NoSQL Database
|
||||
- **PostgreSQL** - SQL Database
|
||||
- **LangChain** - LangChain is a framework for developing applications powered by language models
|
||||
- **React** - duh
|
||||
|
||||
## A word
|
||||
|
|
|
|||
2
apps/api/.gitignore
vendored
2
apps/api/.gitignore
vendored
|
|
@ -10,7 +10,7 @@ __pycache__/
|
|||
.vscode/
|
||||
|
||||
# Learnhouse
|
||||
content/org_*
|
||||
content/*
|
||||
|
||||
# Flyio
|
||||
fly.toml
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from fastapi_jwt_auth.exceptions import AuthJWTException
|
|||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
|
||||
|
||||
|
||||
# from src.services.mocks.initial import create_initial_data
|
||||
|
||||
########################
|
||||
|
|
@ -26,7 +25,6 @@ app = FastAPI(
|
|||
title=learnhouse_config.site_name,
|
||||
description=learnhouse_config.site_description,
|
||||
version="0.1.0",
|
||||
root_path="/",
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
|
|
@ -61,8 +59,8 @@ app.mount("/content", StaticFiles(directory="content"), name="content")
|
|||
# Global Routes
|
||||
app.include_router(v1_router)
|
||||
|
||||
|
||||
# General Routes
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"Message": "Welcome to LearnHouse ✨"}
|
||||
|
||||
|
|
|
|||
1021
apps/api/poetry.lock
generated
1021
apps/api/poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -11,7 +11,7 @@ readme = "README.md"
|
|||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
fastapi = "0.104.1"
|
||||
fastapi = "0.109.1"
|
||||
pydantic = {version = ">=1.8.0,<2.0.0", extras = ["email"]}
|
||||
sqlmodel = "0.0.10"
|
||||
uvicorn = "0.23.2"
|
||||
|
|
@ -34,7 +34,6 @@ langchain = "0.1.0"
|
|||
tiktoken = "^0.5.2"
|
||||
openai = "^1.7.1"
|
||||
chromadb = "^0.4.22"
|
||||
sentence-transformers = "^2.2.2"
|
||||
python-dotenv = "^1.0.0"
|
||||
redis = "^5.0.1"
|
||||
langchain-community = "^0.0.11"
|
||||
|
|
|
|||
|
|
@ -24,6 +24,5 @@ langchain-openai
|
|||
tiktoken
|
||||
openai
|
||||
chromadb
|
||||
sentence-transformers
|
||||
python-dotenv
|
||||
redis
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from typing import Optional
|
||||
from sqlalchemy import JSON, BigInteger, Column, ForeignKey
|
||||
from sqlalchemy import JSON, Column, ForeignKey, Integer
|
||||
from sqlmodel import Field, SQLModel
|
||||
from enum import Enum
|
||||
|
||||
|
|
@ -38,12 +38,12 @@ class ActivityBase(SQLModel):
|
|||
|
||||
class Activity(ActivityBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||
org_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||
)
|
||||
course_id: int = Field(
|
||||
default=None,
|
||||
sa_column=Column(
|
||||
BigInteger, ForeignKey("course.id", ondelete="CASCADE")
|
||||
),
|
||||
sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE")),
|
||||
)
|
||||
activity_uuid: str = ""
|
||||
creation_date: str = ""
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class BlockBase(SQLModel):
|
|||
class Block(BlockBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
content: dict = Field(default={}, sa_column=Column(JSON))
|
||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||
org_id: int = Field(sa_column= Column("org_id", ForeignKey("organization.id", ondelete="CASCADE")))
|
||||
course_id: int = Field(sa_column= Column("course_id", ForeignKey("course.id", ondelete="CASCADE")))
|
||||
chapter_id: int = Field(sa_column= Column("chapter_id", ForeignKey("chapter.id", ondelete="CASCADE")))
|
||||
activity_id: int = Field(sa_column= Column("activity_id", ForeignKey("activity.id", ondelete="CASCADE")))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from typing import Optional
|
||||
from sqlalchemy import BigInteger, Column, ForeignKey
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
|
|
@ -10,7 +11,9 @@ class CollectionBase(SQLModel):
|
|||
|
||||
class Collection(CollectionBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||
org_id: int = Field(
|
||||
sa_column=Column(BigInteger, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||
)
|
||||
collection_uuid: str = ""
|
||||
creation_date: str = ""
|
||||
update_date: str = ""
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
from typing import Optional
|
||||
from sqlalchemy import BigInteger, Column, ForeignKey
|
||||
from sqlalchemy import Column, ForeignKey, Integer
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
class CollectionCourse(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
collection_id: int = Field(sa_column=Column(BigInteger, ForeignKey("collection.id", ondelete="CASCADE")))
|
||||
course_id: int = Field(sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE")))
|
||||
collection_id: int = Field(sa_column=Column(Integer, ForeignKey("collection.id", ondelete="CASCADE")))
|
||||
course_id: int = Field(sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE")))
|
||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||
creation_date: str
|
||||
update_date: str
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from typing import Optional
|
||||
from sqlalchemy import BigInteger, Column, ForeignKey
|
||||
from sqlalchemy import Column, ForeignKey, Integer
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
|
|
@ -7,10 +7,10 @@ class CourseChapter(SQLModel, table=True):
|
|||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
order: int
|
||||
course_id: int = Field(
|
||||
sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE"))
|
||||
sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE"))
|
||||
)
|
||||
chapter_id: int = Field(
|
||||
sa_column=Column(BigInteger, ForeignKey("chapter.id", ondelete="CASCADE"))
|
||||
sa_column=Column(Integer, ForeignKey("chapter.id", ondelete="CASCADE"))
|
||||
)
|
||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||
creation_date: str
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from typing import List, Optional
|
||||
from sqlalchemy import Column, ForeignKey, Integer
|
||||
from sqlmodel import Field, SQLModel
|
||||
from src.db.users import UserRead
|
||||
from src.db.trails import TrailRead
|
||||
|
|
@ -17,7 +18,9 @@ class CourseBase(SQLModel):
|
|||
|
||||
class Course(CourseBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||
org_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||
)
|
||||
course_uuid: str = ""
|
||||
creation_date: str = ""
|
||||
update_date: str = ""
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ class AIConfig(BaseModel):
|
|||
enabled : bool = True
|
||||
limits: AILimitsSettings = AILimitsSettings()
|
||||
embeddings: Literal[
|
||||
"text-embedding-ada-002", "all-MiniLM-L6-v2"
|
||||
] = "all-MiniLM-L6-v2"
|
||||
"text-embedding-ada-002",
|
||||
] = "text-embedding-ada-002"
|
||||
ai_model: Literal["gpt-3.5-turbo", "gpt-4-1106-preview"] = "gpt-3.5-turbo"
|
||||
features: AIEnabledFeatures = AIEnabledFeatures()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import Field, SQLModel
|
||||
from src.db.roles import RoleRead
|
||||
|
||||
from src.db.organization_config import OrganizationConfig
|
||||
|
||||
|
||||
|
|
@ -32,3 +35,9 @@ class OrganizationRead(OrganizationBase):
|
|||
config: Optional[OrganizationConfig | dict]
|
||||
creation_date: str
|
||||
update_date: str
|
||||
|
||||
|
||||
class OrganizationUser(BaseModel):
|
||||
from src.db.users import UserRead
|
||||
user: UserRead
|
||||
role: RoleRead
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from enum import Enum
|
||||
from typing import Optional
|
||||
from sqlalchemy import Column, ForeignKey, Integer
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
|
|
@ -12,7 +13,9 @@ class ResourceAuthorshipEnum(str, Enum):
|
|||
class ResourceAuthor(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
resource_uuid: str
|
||||
user_id: int = Field(default=None, foreign_key="user.id")
|
||||
user_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("user.id", ondelete="CASCADE"))
|
||||
)
|
||||
authorship: ResourceAuthorshipEnum = ResourceAuthorshipEnum.CREATOR
|
||||
creation_date: str = ""
|
||||
update_date: str = ""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from enum import Enum
|
||||
from typing import Optional, Union
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import JSON, Column
|
||||
from sqlalchemy import JSON, Column, ForeignKey, Integer
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
|
|
@ -45,7 +45,10 @@ class RoleBase(SQLModel):
|
|||
|
||||
class Role(RoleBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||
org_id: Optional[int] = Field(
|
||||
default=None,
|
||||
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||
)
|
||||
role_type: RoleTypeEnum = RoleTypeEnum.TYPE_GLOBAL
|
||||
role_uuid: str = ""
|
||||
creation_date: str = ""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import JSON, Column
|
||||
from sqlalchemy import JSON, Column, ForeignKey, Integer
|
||||
from sqlmodel import Field, SQLModel
|
||||
from enum import Enum
|
||||
|
||||
|
|
@ -23,10 +23,18 @@ class TrailRun(SQLModel, table=True):
|
|||
data: dict = Field(default={}, sa_column=Column(JSON))
|
||||
status: StatusEnum = StatusEnum.STATUS_IN_PROGRESS
|
||||
# foreign keys
|
||||
trail_id: int = Field(default=None, foreign_key="trail.id")
|
||||
course_id: int = Field(default=None, foreign_key="course.id")
|
||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||
user_id: int = Field(default=None, foreign_key="user.id")
|
||||
trail_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("trail.id", ondelete="CASCADE"))
|
||||
)
|
||||
course_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE"))
|
||||
)
|
||||
org_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||
)
|
||||
user_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("user.id", ondelete="CASCADE"))
|
||||
)
|
||||
# timestamps
|
||||
creation_date: str
|
||||
update_date: str
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from enum import Enum
|
||||
from typing import Optional
|
||||
from sqlmodel import Field, SQLModel
|
||||
from sqlalchemy import BigInteger, ForeignKey, JSON, Column
|
||||
from sqlalchemy import ForeignKey, JSON, Column, Integer
|
||||
|
||||
|
||||
class TrailStepTypeEnum(str, Enum):
|
||||
|
|
@ -18,13 +18,23 @@ class TrailStep(SQLModel, table=True):
|
|||
data: dict = Field(default={}, sa_column=Column(JSON))
|
||||
# foreign keys
|
||||
trailrun_id: int = Field(
|
||||
sa_column=Column(BigInteger, ForeignKey("trailrun.id", ondelete="CASCADE"))
|
||||
sa_column=Column(Integer, ForeignKey("trailrun.id", ondelete="CASCADE"))
|
||||
)
|
||||
trail_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("trail.id", ondelete="CASCADE"))
|
||||
)
|
||||
activity_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("activity.id", ondelete="CASCADE"))
|
||||
)
|
||||
course_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE"))
|
||||
)
|
||||
org_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||
)
|
||||
user_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("user.id", ondelete="CASCADE"))
|
||||
)
|
||||
trail_id: int = Field(default=None, foreign_key="trail.id")
|
||||
activity_id: int = Field(default=None, foreign_key="activity.id")
|
||||
course_id: int = Field(default=None, foreign_key="course.id")
|
||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||
user_id: int = Field(default=None, foreign_key="user.id")
|
||||
# timestamps
|
||||
creation_date: str
|
||||
update_date: str
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import Column, ForeignKey, Integer
|
||||
from sqlmodel import Field, SQLModel
|
||||
from src.db.trail_runs import TrailRunRead
|
||||
|
||||
|
|
@ -24,8 +25,12 @@ class TrailCreate(TrailBase):
|
|||
class TrailRead(BaseModel):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
trail_uuid: Optional[str]
|
||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||
user_id: int = Field(default=None, foreign_key="user.id")
|
||||
org_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||
)
|
||||
user_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("user.id", ondelete="CASCADE"))
|
||||
)
|
||||
creation_date: Optional[str]
|
||||
update_date: Optional[str]
|
||||
runs: list[TrailRunRead]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from typing import Optional
|
||||
from sqlalchemy import BigInteger, Column, ForeignKey
|
||||
from sqlalchemy import Column, ForeignKey, Integer
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
|
|
@ -7,7 +7,7 @@ class UserOrganization(SQLModel, table=True):
|
|||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
user_id: int = Field(default=None, foreign_key="user.id")
|
||||
org_id: int = Field(
|
||||
sa_column=Column(BigInteger, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||
)
|
||||
role_id: int = Field(default=None, foreign_key="role.id")
|
||||
creation_date: str
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
from typing import Optional
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
from src.db.roles import RoleRead
|
||||
from src.db.organizations import OrganizationRead
|
||||
|
||||
|
||||
|
||||
class UserBase(SQLModel):
|
||||
|
|
@ -45,6 +44,7 @@ class PublicUser(UserRead):
|
|||
|
||||
|
||||
class UserRoleWithOrg(BaseModel):
|
||||
from src.db.organizations import OrganizationRead
|
||||
role: RoleRead
|
||||
org: OrganizationRead
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from datetime import timedelta
|
||||
from fastapi import Depends, APIRouter, HTTPException, Response, status, Request
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlmodel import Session
|
||||
|
|
@ -10,7 +11,7 @@ from src.security.auth import AuthJWT, authenticate_user
|
|||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/refresh")
|
||||
@router.get("/refresh")
|
||||
def refresh(response: Response, Authorize: AuthJWT = Depends()):
|
||||
"""
|
||||
The jwt_refresh_token_required() function insures a valid refresh
|
||||
|
|
@ -28,6 +29,7 @@ def refresh(response: Response, Authorize: AuthJWT = Depends()):
|
|||
value=new_access_token,
|
||||
httponly=False,
|
||||
domain=get_learnhouse_config().hosting_config.cookie_config.domain,
|
||||
expires=int(timedelta(hours=8).total_seconds()),
|
||||
)
|
||||
return {"access_token": new_access_token}
|
||||
|
||||
|
|
@ -53,12 +55,14 @@ async def login(
|
|||
access_token = Authorize.create_access_token(subject=form_data.username)
|
||||
refresh_token = Authorize.create_refresh_token(subject=form_data.username)
|
||||
Authorize.set_refresh_cookies(refresh_token)
|
||||
|
||||
# set cookies using fastapi
|
||||
response.set_cookie(
|
||||
key="access_token_cookie",
|
||||
value=access_token,
|
||||
httponly=False,
|
||||
domain=get_learnhouse_config().hosting_config.cookie_config.domain,
|
||||
expires=int(timedelta(hours=8).total_seconds()),
|
||||
)
|
||||
|
||||
user = UserRead.from_orm(user)
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ async def api_create_course(
|
|||
name: str = Form(),
|
||||
description: str = Form(),
|
||||
public: bool = Form(),
|
||||
learnings: str = Form(),
|
||||
tags: str = Form(),
|
||||
learnings: str = Form(None),
|
||||
tags: str = Form(None),
|
||||
about: str = Form(),
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,20 @@
|
|||
from typing import List
|
||||
from typing import List, Literal
|
||||
from fastapi import APIRouter, Depends, Request, UploadFile
|
||||
from sqlmodel import Session
|
||||
from src.services.orgs.invites import (
|
||||
create_invite_code,
|
||||
delete_invite_code,
|
||||
get_invite_code,
|
||||
get_invite_codes,
|
||||
)
|
||||
from src.services.orgs.users import (
|
||||
get_list_of_invited_users,
|
||||
get_organization_users,
|
||||
invite_batch_users,
|
||||
remove_invited_user,
|
||||
remove_user_from_org,
|
||||
update_user_role,
|
||||
)
|
||||
from src.db.organization_config import OrganizationConfigBase
|
||||
from src.db.users import PublicUser
|
||||
from src.db.organizations import (
|
||||
|
|
@ -8,6 +22,7 @@ from src.db.organizations import (
|
|||
OrganizationCreate,
|
||||
OrganizationRead,
|
||||
OrganizationUpdate,
|
||||
OrganizationUser,
|
||||
)
|
||||
from src.core.events.database import get_db_session
|
||||
from src.security.auth import get_current_user
|
||||
|
|
@ -20,6 +35,7 @@ from src.services.orgs.orgs import (
|
|||
get_orgs_by_user,
|
||||
update_org,
|
||||
update_org_logo,
|
||||
update_org_signup_mechanism,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -69,6 +85,166 @@ async def api_get_org(
|
|||
return await get_organization(request, org_id, db_session, current_user)
|
||||
|
||||
|
||||
@router.get("/{org_id}/users")
|
||||
async def api_get_org_users(
|
||||
request: Request,
|
||||
org_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
) -> list[OrganizationUser]:
|
||||
"""
|
||||
Get single Org by ID
|
||||
"""
|
||||
return await get_organization_users(request, org_id, db_session, current_user)
|
||||
|
||||
|
||||
@router.put("/{org_id}/users/{user_id}/role/{role_uuid}")
|
||||
async def api_update_user_role(
|
||||
request: Request,
|
||||
org_id: str,
|
||||
user_id: str,
|
||||
role_uuid: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Update user role
|
||||
"""
|
||||
return await update_user_role(
|
||||
request, org_id, user_id, role_uuid, db_session, current_user
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{org_id}/users/{user_id}")
|
||||
async def api_remove_user_from_org(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
user_id: int,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Remove user from org
|
||||
"""
|
||||
return await remove_user_from_org(
|
||||
request, org_id, user_id, db_session, current_user
|
||||
)
|
||||
|
||||
|
||||
# Config related routes
|
||||
@router.put("/{org_id}/signup_mechanism")
|
||||
async def api_get_org_signup_mechanism(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
signup_mechanism: Literal["open", "inviteOnly"],
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Get org signup mechanism
|
||||
"""
|
||||
return await update_org_signup_mechanism(
|
||||
request, signup_mechanism, org_id, current_user, db_session
|
||||
)
|
||||
|
||||
|
||||
# Invites related routes
|
||||
@router.post("/{org_id}/invites")
|
||||
async def api_create_invite_code(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Create invite code
|
||||
"""
|
||||
return await create_invite_code(request, org_id, current_user, db_session)
|
||||
|
||||
|
||||
@router.get("/{org_id}/invites")
|
||||
async def api_get_invite_codes(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Get invite codes
|
||||
"""
|
||||
return await get_invite_codes(request, org_id, current_user, db_session)
|
||||
|
||||
@router.get("/{org_id}/invites/code/{invite_code}")
|
||||
async def api_get_invite_code(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
invite_code: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Get invite code
|
||||
"""
|
||||
print(f"org_id: {org_id}, invite_code: {invite_code}")
|
||||
return await get_invite_code(request, org_id,invite_code, current_user, db_session)
|
||||
|
||||
|
||||
@router.delete("/{org_id}/invites/{org_invite_code_uuid}")
|
||||
async def api_delete_invite_code(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
org_invite_code_uuid: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Delete invite code
|
||||
"""
|
||||
return await delete_invite_code(
|
||||
request, org_id, org_invite_code_uuid, current_user, db_session
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{org_id}/invites/users/batch")
|
||||
async def api_invite_batch_users(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
users: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Invite batch users
|
||||
"""
|
||||
return await invite_batch_users(request, org_id, users, db_session, current_user)
|
||||
|
||||
|
||||
@router.get("/{org_id}/invites/users")
|
||||
async def api_get_org_users_invites(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Get org users invites
|
||||
"""
|
||||
return await get_list_of_invited_users(request, org_id, db_session, current_user)
|
||||
|
||||
@router.delete("/{org_id}/invites/users/{email}")
|
||||
async def api_delete_org_users_invites(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
email: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Delete org users invites
|
||||
"""
|
||||
return await remove_invited_user(request, org_id, email, db_session, current_user)
|
||||
|
||||
|
||||
@router.get("/slug/{org_slug}")
|
||||
async def api_get_org_by_slug(
|
||||
request: Request,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from typing import Literal
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
|
||||
from sqlmodel import Session
|
||||
from src.services.orgs.orgs import get_org_join_mechanism
|
||||
from src.security.auth import get_current_user
|
||||
from src.core.events.database import get_db_session
|
||||
|
||||
|
|
@ -16,12 +17,14 @@ from src.db.users import (
|
|||
from src.services.users.users import (
|
||||
authorize_user_action,
|
||||
create_user,
|
||||
create_user_with_invite,
|
||||
create_user_without_org,
|
||||
delete_user_by_id,
|
||||
get_user_session,
|
||||
read_user_by_id,
|
||||
read_user_by_uuid,
|
||||
update_user,
|
||||
update_user_avatar,
|
||||
update_user_password,
|
||||
)
|
||||
|
||||
|
|
@ -77,7 +80,48 @@ async def api_create_user_with_orgid(
|
|||
"""
|
||||
Create User with Org ID
|
||||
"""
|
||||
return await create_user(request, db_session, current_user, user_object, org_id)
|
||||
print(await get_org_join_mechanism(request, org_id, current_user, db_session))
|
||||
|
||||
# TODO(fix) : This is temporary, logic should be moved to service
|
||||
if (
|
||||
await get_org_join_mechanism(request, org_id, current_user, db_session)
|
||||
== "inviteOnly"
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You need an invite to join this organization",
|
||||
)
|
||||
else:
|
||||
return await create_user(request, db_session, current_user, user_object, org_id)
|
||||
|
||||
|
||||
@router.post("/{org_id}/invite/{invite_code}", response_model=UserRead, tags=["users"])
|
||||
async def api_create_user_with_orgid_and_invite(
|
||||
*,
|
||||
request: Request,
|
||||
db_session: Session = Depends(get_db_session),
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
user_object: UserCreate,
|
||||
invite_code: str,
|
||||
org_id: int,
|
||||
) -> UserRead:
|
||||
"""
|
||||
Create User with Org ID and invite code
|
||||
"""
|
||||
|
||||
# TODO: This is temporary, logic should be moved to service
|
||||
if (
|
||||
await get_org_join_mechanism(request, org_id, current_user, db_session)
|
||||
== "inviteOnly"
|
||||
):
|
||||
return await create_user_with_invite(
|
||||
request, db_session, current_user, user_object, org_id, invite_code
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="This organization does not require an invite code",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/", response_model=UserRead, tags=["users"])
|
||||
|
|
@ -137,6 +181,20 @@ async def api_update_user(
|
|||
return await update_user(request, db_session, user_id, current_user, user_object)
|
||||
|
||||
|
||||
@router.put("/update_avatar/{user_id}", response_model=UserRead, tags=["users"])
|
||||
async def api_update_avatar_user(
|
||||
*,
|
||||
request: Request,
|
||||
db_session: Session = Depends(get_db_session),
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
avatar_file: UploadFile | None = None,
|
||||
) -> UserRead:
|
||||
"""
|
||||
Update User
|
||||
"""
|
||||
return await update_user_avatar(request, db_session, current_user, avatar_file)
|
||||
|
||||
|
||||
@router.put("/change_password/{user_id}", response_model=UserRead, tags=["users"])
|
||||
async def api_update_user_password(
|
||||
*,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ class Settings(BaseModel):
|
|||
authjwt_secret_key: str = "secret" if isDevModeEnabled() else SECRET_KEY
|
||||
authjwt_token_location = {"cookies", "headers"}
|
||||
authjwt_cookie_csrf_protect = False
|
||||
authjwt_access_token_expires = False if isDevModeEnabled() else 28800
|
||||
authjwt_access_token_expires = (
|
||||
False if isDevModeEnabled() else timedelta(hours=8).total_seconds()
|
||||
)
|
||||
authjwt_cookie_samesite = "lax"
|
||||
authjwt_cookie_secure = True
|
||||
authjwt_cookie_domain = get_learnhouse_config().hosting_config.cookie_config.domain
|
||||
|
|
|
|||
|
|
@ -19,9 +19,8 @@ async def authorization_verify_if_element_is_public(
|
|||
):
|
||||
element_nature = await check_element_type(element_uuid)
|
||||
# Verifies if the element is public
|
||||
if element_nature == ("courses" or "collections") and action == "read":
|
||||
if element_nature == ("courses") and action == "read":
|
||||
if element_nature == "courses":
|
||||
print("looking for course")
|
||||
statement = select(Course).where(
|
||||
Course.public == True, Course.course_uuid == element_uuid
|
||||
)
|
||||
|
|
@ -29,20 +28,29 @@ async def authorization_verify_if_element_is_public(
|
|||
if course:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights : You don't have the right to perform this action",
|
||||
)
|
||||
|
||||
if element_nature == "collections" and action == "read":
|
||||
|
||||
if element_nature == "collections":
|
||||
statement = select(Collection).where(
|
||||
Collection.public == True, Collection.collection_uuid == element_uuid
|
||||
)
|
||||
collection = db_session.exec(statement).first()
|
||||
|
||||
if collection:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights : You don't have the right to perform this action",
|
||||
)
|
||||
else:
|
||||
return False
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights : You don't have the right to perform this action",
|
||||
)
|
||||
|
||||
|
||||
# Tested and working
|
||||
|
|
@ -106,6 +114,34 @@ async def authorization_verify_based_on_roles(
|
|||
return False
|
||||
|
||||
|
||||
async def authorization_verify_based_on_org_admin_status(
|
||||
request: Request,
|
||||
user_id: int,
|
||||
action: Literal["read", "update", "delete", "create"],
|
||||
element_uuid: str,
|
||||
db_session: Session,
|
||||
):
|
||||
await check_element_type(element_uuid)
|
||||
|
||||
# Get user roles bound to an organization and standard roles
|
||||
statement = (
|
||||
select(Role)
|
||||
.join(UserOrganization)
|
||||
.where((UserOrganization.org_id == Role.org_id) | (Role.org_id == null()))
|
||||
.where(UserOrganization.user_id == user_id)
|
||||
)
|
||||
|
||||
user_roles_in_organization_and_standard_roles = db_session.exec(statement).all()
|
||||
|
||||
# Find in roles list if there is a role that matches users action for this type of element
|
||||
for role in user_roles_in_organization_and_standard_roles:
|
||||
role = Role.from_orm(role)
|
||||
if role.id == 1 or role.id == 2:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
# Tested and working
|
||||
async def authorization_verify_based_on_roles_and_authorship(
|
||||
request: Request,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ async def check_element_type(element_id):
|
|||
"""
|
||||
Check if the element is a course, a user, a house or a collection, by checking its prefix
|
||||
"""
|
||||
print("element_id", element_id)
|
||||
if element_id.startswith("course_"):
|
||||
return "courses"
|
||||
elif element_id.startswith("user_"):
|
||||
|
|
|
|||
|
|
@ -67,8 +67,11 @@ def ai_start_activity_chat_session(
|
|||
|
||||
# Serialize Activity Content Blocks to a text comprehensible by the AI
|
||||
structured = structure_activity_content_by_type(content)
|
||||
|
||||
isEmpty = structured == []
|
||||
|
||||
ai_friendly_text = serialize_activity_text_to_ai_comprehensible_text(
|
||||
structured, course, activity
|
||||
structured, course, activity, isActivityEmpty=isEmpty
|
||||
)
|
||||
|
||||
# Get Activity Organization
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ from typing import Optional
|
|||
from uuid import uuid4
|
||||
from langchain.agents import AgentExecutor
|
||||
from langchain.text_splitter import CharacterTextSplitter
|
||||
from langchain.embeddings.sentence_transformer import SentenceTransformerEmbeddings
|
||||
from langchain_community.vectorstores import Chroma
|
||||
from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent
|
||||
from langchain.prompts import MessagesPlaceholder
|
||||
|
|
@ -45,7 +44,6 @@ def ask_ai(
|
|||
texts = text_splitter.split_documents(documents)
|
||||
|
||||
embedding_models = {
|
||||
"all-MiniLM-L6-v2": SentenceTransformerEmbeddings,
|
||||
"text-embedding-ada-002": OpenAIEmbeddings,
|
||||
}
|
||||
|
||||
|
|
@ -53,11 +51,11 @@ def ask_ai(
|
|||
|
||||
if embedding_model_name in embedding_models:
|
||||
if embedding_model_name == "text-embedding-ada-002":
|
||||
embedding_function = embedding_models[embedding_model_name](model=embedding_model_name, api_key=openai_api_key)
|
||||
if embedding_model_name == "all-MiniLM-L6-v2":
|
||||
embedding_function = embedding_models[embedding_model_name](model_name=embedding_model_name)
|
||||
embedding_function = embedding_models[embedding_model_name](
|
||||
model=embedding_model_name, api_key=openai_api_key
|
||||
)
|
||||
else:
|
||||
embedding_function = embedding_models[embedding_model_name](model_name=embedding_model_name)
|
||||
raise Exception("Embedding model not found")
|
||||
|
||||
# load it into Chroma and use it as a retriever
|
||||
db = Chroma.from_documents(texts, embedding_function)
|
||||
|
|
@ -75,7 +73,10 @@ def ask_ai(
|
|||
memory_key = "history"
|
||||
|
||||
memory = AgentTokenBufferMemory(
|
||||
memory_key=memory_key, llm=llm, chat_memory=message_history, max_token_limit=1000
|
||||
memory_key=memory_key,
|
||||
llm=llm,
|
||||
chat_memory=message_history,
|
||||
max_token_limit=1000,
|
||||
)
|
||||
|
||||
system_message = SystemMessage(content=(message_for_the_prompt))
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ async def upload_file_and_return_file_object(
|
|||
|
||||
await upload_content(
|
||||
f"courses/{course_uuid}/activities/{activity_uuid}/dynamic/blocks/{type_of_block}/{block_id}",
|
||||
org_uuid=org_uuid,
|
||||
type_of_dir='orgs',
|
||||
uuid=org_uuid,
|
||||
file_binary=file_binary,
|
||||
file_and_format=f"{file_id}.{file_format}",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from typing import Literal
|
||||
from sqlmodel import Session, select
|
||||
from src.db.courses import Course
|
||||
from src.db.chapters import Chapter
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
|
|
@ -25,7 +26,6 @@ async def create_activity(
|
|||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
activity = Activity.from_orm(activity_object)
|
||||
|
||||
# CHeck if org exists
|
||||
statement = select(Chapter).where(Chapter.id == activity_object.chapter_id)
|
||||
|
|
@ -40,6 +40,9 @@ async def create_activity(
|
|||
# RBAC check
|
||||
await rbac_check(request, chapter.chapter_uuid, current_user, "create", db_session)
|
||||
|
||||
# Create Activity
|
||||
activity = Activity(**activity_object.dict())
|
||||
|
||||
activity.activity_uuid = str(f"activity_{uuid4()}")
|
||||
activity.creation_date = str(datetime.now())
|
||||
activity.update_date = str(datetime.now())
|
||||
|
|
@ -96,8 +99,18 @@ async def get_activity(
|
|||
detail="Activity not found",
|
||||
)
|
||||
|
||||
# Get course from that activity
|
||||
statement = select(Course).where(Course.id == activity.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, activity.activity_uuid, current_user, "read", db_session)
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
activity = ActivityRead.from_orm(activity)
|
||||
|
||||
|
|
@ -223,7 +236,6 @@ async def rbac_check(
|
|||
res = await authorization_verify_if_element_is_public(
|
||||
request, course_uuid, action, db_session
|
||||
)
|
||||
print('res',res)
|
||||
return res
|
||||
else:
|
||||
res = await authorization_verify_based_on_roles_and_authorship(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ async def upload_pdf(pdf_file, activity_uuid, org_uuid, course_uuid):
|
|||
try:
|
||||
await upload_content(
|
||||
f"courses/{course_uuid}/activities/{activity_uuid}/documentpdf",
|
||||
"orgs",
|
||||
org_uuid,
|
||||
contents,
|
||||
f"documentpdf.{pdf_format}",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ async def upload_video(video_file, activity_uuid, org_uuid, course_uuid):
|
|||
try:
|
||||
await upload_content(
|
||||
f"courses/{course_uuid}/activities/{activity_uuid}/video",
|
||||
'orgs',
|
||||
org_uuid,
|
||||
contents,
|
||||
f"video.{video_format}",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ from src.db.courses import CourseRead
|
|||
|
||||
def structure_activity_content_by_type(activity):
|
||||
### Get Headings, Texts, Callouts, Answers and Paragraphs from the activity as a big list of strings (text only) and return it
|
||||
|
||||
if "content" not in activity or not activity["content"]:
|
||||
return []
|
||||
|
||||
content = activity["content"]
|
||||
|
||||
headings = []
|
||||
|
|
@ -11,10 +15,12 @@ def structure_activity_content_by_type(activity):
|
|||
paragraphs = []
|
||||
|
||||
for item in content:
|
||||
if 'content' in item:
|
||||
if "content" in item:
|
||||
if item["type"] == "heading" and "text" in item["content"][0]:
|
||||
headings.append(item["content"][0]["text"])
|
||||
elif item["type"] in ["calloutInfo", "calloutWarning"] and all("text" in text_item for text_item in item["content"]):
|
||||
elif item["type"] in ["calloutInfo", "calloutWarning"] and all(
|
||||
"text" in text_item for text_item in item["content"]
|
||||
):
|
||||
callouts.append(
|
||||
"".join([text_item["text"] for text_item in item["content"]])
|
||||
)
|
||||
|
|
@ -34,15 +40,29 @@ def structure_activity_content_by_type(activity):
|
|||
# Add Paragraphs
|
||||
data_array.append({"Paragraphs": paragraphs})
|
||||
|
||||
print(data_array)
|
||||
|
||||
return data_array
|
||||
|
||||
|
||||
def serialize_activity_text_to_ai_comprehensible_text(
|
||||
data_array, course: CourseRead, activity: ActivityRead
|
||||
data_array,
|
||||
course: CourseRead,
|
||||
activity: ActivityRead,
|
||||
isActivityEmpty: bool = False,
|
||||
):
|
||||
### Serialize the text to a format that is comprehensible by the AI
|
||||
|
||||
if isActivityEmpty:
|
||||
text = (
|
||||
"Use this as a context "
|
||||
+ 'This is a course about "'
|
||||
+ course.name
|
||||
+ '". '
|
||||
+ 'This is a lecture about "'
|
||||
+ activity.name
|
||||
+ '". '
|
||||
+ "There is no content yet in this lecture."
|
||||
)
|
||||
|
||||
return text
|
||||
|
||||
# Serialize Headings
|
||||
serialized_headings = ""
|
||||
|
|
@ -51,7 +71,6 @@ def serialize_activity_text_to_ai_comprehensible_text(
|
|||
|
||||
# Serialize Callouts
|
||||
serialized_callouts = ""
|
||||
|
||||
for callout in data_array[1]["Callouts"]:
|
||||
serialized_callouts += callout + " "
|
||||
|
||||
|
|
|
|||
|
|
@ -112,8 +112,17 @@ async def get_chapter(
|
|||
status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist"
|
||||
)
|
||||
|
||||
# get COurse
|
||||
statement = select(Course).where(Course.id == chapter.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, chapter.chapter_uuid, current_user, "read", db_session)
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Get activities for this chapter
|
||||
statement = (
|
||||
|
|
@ -225,7 +234,7 @@ async def get_course_chapters(
|
|||
chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters]
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session) # type: ignore
|
||||
|
||||
# Get activities for each chapter
|
||||
for chapter in chapters:
|
||||
|
|
@ -473,12 +482,15 @@ async def reorder_chapters_and_activities(
|
|||
db_session.delete(chapter_activity)
|
||||
db_session.commit()
|
||||
|
||||
|
||||
# If links do not exist, create them
|
||||
chapter_activity_map = {}
|
||||
for chapter_order in chapters_order.chapter_order_by_ids:
|
||||
for activity_order in chapter_order.activities_order_by_ids:
|
||||
if activity_order.activity_id in chapter_activity_map and chapter_activity_map[activity_order.activity_id] != chapter_order.chapter_id:
|
||||
if (
|
||||
activity_order.activity_id in chapter_activity_map
|
||||
and chapter_activity_map[activity_order.activity_id]
|
||||
!= chapter_order.chapter_id
|
||||
):
|
||||
continue
|
||||
|
||||
statement = (
|
||||
|
|
@ -547,7 +559,7 @@ async def rbac_check(
|
|||
res = await authorization_verify_if_element_is_public(
|
||||
request, course_uuid, action, db_session
|
||||
)
|
||||
print('res',res)
|
||||
print("res", res)
|
||||
return res
|
||||
else:
|
||||
res = await authorization_verify_based_on_roles_and_authorship(
|
||||
|
|
|
|||
|
|
@ -26,7 +26,10 @@ from fastapi import HTTPException, status, Request
|
|||
|
||||
|
||||
async def get_collection(
|
||||
request: Request, collection_uuid: str, current_user: PublicUser, db_session: Session
|
||||
request: Request,
|
||||
collection_uuid: str,
|
||||
current_user: PublicUser,
|
||||
db_session: Session,
|
||||
) -> CollectionRead:
|
||||
statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
|
||||
collection = db_session.exec(statement).first()
|
||||
|
|
@ -42,11 +45,23 @@ async def get_collection(
|
|||
)
|
||||
|
||||
# get courses in collection
|
||||
statement = (
|
||||
statement_all = (
|
||||
select(Course)
|
||||
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
|
||||
.distinct(Course.id)
|
||||
)
|
||||
|
||||
statement_public = (
|
||||
select(Course)
|
||||
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
|
||||
.where(CollectionCourse.org_id == collection.org_id, Course.public == True)
|
||||
)
|
||||
|
||||
if current_user.id == 0:
|
||||
statement = statement_public
|
||||
else:
|
||||
statement = statement_all
|
||||
|
||||
courses = db_session.exec(statement).all()
|
||||
|
||||
collection = CollectionRead(**collection.dict(), courses=courses)
|
||||
|
|
@ -180,7 +195,10 @@ async def update_collection(
|
|||
|
||||
|
||||
async def delete_collection(
|
||||
request: Request, collection_uuid: str, current_user: PublicUser, db_session: Session
|
||||
request: Request,
|
||||
collection_uuid: str,
|
||||
current_user: PublicUser,
|
||||
db_session: Session,
|
||||
):
|
||||
statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
|
||||
collection = db_session.exec(statement).first()
|
||||
|
|
@ -216,23 +234,40 @@ async def get_collections(
|
|||
page: int = 1,
|
||||
limit: int = 10,
|
||||
) -> List[CollectionRead]:
|
||||
# RBAC check
|
||||
await rbac_check(request, "collection_x", current_user, "read", db_session)
|
||||
|
||||
statement = (
|
||||
statement_public = select(Collection).where(
|
||||
Collection.org_id == org_id, Collection.public == True
|
||||
)
|
||||
statement_all = (
|
||||
select(Collection).where(Collection.org_id == org_id).distinct(Collection.id)
|
||||
)
|
||||
|
||||
if current_user.id == 0:
|
||||
statement = statement_public
|
||||
else:
|
||||
statement = statement_all
|
||||
|
||||
collections = db_session.exec(statement).all()
|
||||
|
||||
|
||||
|
||||
collections_with_courses = []
|
||||
|
||||
for collection in collections:
|
||||
statement = (
|
||||
statement_all = (
|
||||
select(Course)
|
||||
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
|
||||
.distinct(Course.id)
|
||||
)
|
||||
statement_public = (
|
||||
select(Course)
|
||||
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
|
||||
.where(CollectionCourse.org_id == org_id, Course.public == True)
|
||||
)
|
||||
if current_user.id == 0:
|
||||
statement = statement_public
|
||||
else:
|
||||
# RBAC check
|
||||
statement = statement_all
|
||||
|
||||
courses = db_session.exec(statement).all()
|
||||
|
||||
collection = CollectionRead(**collection.dict(), courses=courses)
|
||||
|
|
@ -256,8 +291,11 @@ async def rbac_check(
|
|||
res = await authorization_verify_if_element_is_public(
|
||||
request, collection_uuid, action, db_session
|
||||
)
|
||||
print('res',res)
|
||||
return res
|
||||
if res == False:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights : You are not allowed to read this collection",
|
||||
)
|
||||
else:
|
||||
res = await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, collection_uuid, db_session
|
||||
|
|
@ -276,4 +314,3 @@ async def rbac_check(
|
|||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
||||
|
|
|
|||
|
|
@ -146,6 +146,9 @@ async def create_course(
|
|||
)
|
||||
course.thumbnail_image = name_in_disk
|
||||
|
||||
else:
|
||||
course.thumbnail_image = ""
|
||||
|
||||
# Insert course
|
||||
db_session.add(course)
|
||||
db_session.commit()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
|
||||
from src.services.utils.upload_content import upload_content
|
||||
|
||||
|
||||
async def upload_thumbnail(thumbnail_file, name_in_disk, org_id, course_id):
|
||||
async def upload_thumbnail(thumbnail_file, name_in_disk, org_uuid, course_id):
|
||||
contents = thumbnail_file.file.read()
|
||||
try:
|
||||
await upload_content(
|
||||
f"courses/{course_id}/thumbnails",
|
||||
org_id,
|
||||
"orgs",
|
||||
org_uuid,
|
||||
contents,
|
||||
f"{name_in_disk}",
|
||||
)
|
||||
|
|
|
|||
253
apps/api/src/services/orgs/invites.py
Normal file
253
apps/api/src/services/orgs/invites.py
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import json
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
import redis
|
||||
from datetime import datetime, timedelta
|
||||
from sqlmodel import Session, select
|
||||
from config.config import get_learnhouse_config
|
||||
from src.services.orgs.orgs import rbac_check
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.db.organizations import (
|
||||
Organization,
|
||||
)
|
||||
from fastapi import HTTPException, Request
|
||||
|
||||
|
||||
async def create_invite_code(
|
||||
request: Request,
|
||||
org_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,
|
||||
"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(
|
||||
request: Request,
|
||||
org_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",
|
||||
)
|
||||
|
||||
# Get invite codes
|
||||
invite_codes = r.keys(f"org_invite_code_*:org:{org.org_uuid}:code:*")
|
||||
|
||||
invite_codes_list = []
|
||||
|
||||
for invite_code in invite_codes:
|
||||
invite_code = r.get(invite_code)
|
||||
invite_code = json.loads(invite_code)
|
||||
invite_codes_list.append(invite_code)
|
||||
|
||||
return invite_codes_list
|
||||
|
||||
async def get_invite_code(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
invite_code: str,
|
||||
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",
|
||||
)
|
||||
|
||||
|
||||
# Get invite code
|
||||
invite_code = r.keys(f"org_invite_code_*:org:{org.org_uuid}:code:{invite_code}") # type: ignore
|
||||
|
||||
if not invite_code:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Invite code not found",
|
||||
)
|
||||
|
||||
invite_code = r.get(invite_code[0]) # type: ignore
|
||||
invite_code = json.loads(invite_code)
|
||||
|
||||
return invite_code
|
||||
|
||||
|
||||
|
||||
async def delete_invite_code(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
invite_code_uuid: str,
|
||||
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",
|
||||
)
|
||||
|
||||
# Delete invite code
|
||||
keys = r.keys(f"{invite_code_uuid}:org:{org.org_uuid}:code:*")
|
||||
if keys:
|
||||
r.delete(*keys)
|
||||
|
||||
if not keys:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Invite code not found",
|
||||
)
|
||||
|
||||
return keys
|
||||
|
|
@ -9,6 +9,7 @@ async def upload_org_logo(logo_file, org_uuid):
|
|||
|
||||
await upload_content(
|
||||
"logos",
|
||||
"orgs",
|
||||
org_uuid,
|
||||
contents,
|
||||
name_in_disk,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from src.db.organization_config import (
|
|||
OrganizationConfigBase,
|
||||
)
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_based_on_org_admin_status,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
|
|
@ -169,7 +169,7 @@ async def create_org(
|
|||
limits_enabled=False,
|
||||
max_asks=0,
|
||||
),
|
||||
embeddings="all-MiniLM-L6-v2",
|
||||
embeddings="text-embedding-ada-002",
|
||||
ai_model="gpt-3.5-turbo",
|
||||
features=AIEnabledFeatures(
|
||||
editor=False,
|
||||
|
|
@ -438,12 +438,106 @@ async def get_orgs_by_user(
|
|||
return orgs
|
||||
|
||||
|
||||
# Config related
|
||||
async def update_org_signup_mechanism(
|
||||
request: Request,
|
||||
signup_mechanism: Literal["open", "inviteOnly"],
|
||||
org_id: int,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
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)
|
||||
|
||||
# Get org config
|
||||
statement = select(OrganizationConfig).where(OrganizationConfig.org_id == org.id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
org_config = result.first()
|
||||
|
||||
if org_config is None:
|
||||
logging.error(f"Organization {org_id} has no config")
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Organization config not found",
|
||||
)
|
||||
|
||||
updated_config = org_config.config
|
||||
|
||||
# Update config
|
||||
updated_config = OrganizationConfigBase(**updated_config)
|
||||
updated_config.GeneralConfig.users.signup_mechanism = signup_mechanism
|
||||
|
||||
# Update the database
|
||||
org_config.config = json.loads(updated_config.json())
|
||||
org_config.update_date = str(datetime.now())
|
||||
|
||||
db_session.add(org_config)
|
||||
db_session.commit()
|
||||
db_session.refresh(org_config)
|
||||
|
||||
return {"detail": "Signup mechanism updated"}
|
||||
|
||||
|
||||
async def get_org_join_mechanism(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
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, "read", db_session)
|
||||
|
||||
# Get org config
|
||||
statement = select(OrganizationConfig).where(OrganizationConfig.org_id == org.id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
org_config = result.first()
|
||||
|
||||
if org_config is None:
|
||||
logging.error(f"Organization {org_id} has no config")
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Organization config not found",
|
||||
)
|
||||
|
||||
config = org_config.config
|
||||
|
||||
# Get the signup mechanism
|
||||
config = OrganizationConfigBase(**config)
|
||||
signup_mechanism = config.GeneralConfig.users.signup_mechanism
|
||||
|
||||
return signup_mechanism
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
||||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
org_id: str,
|
||||
org_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
|
|
@ -453,11 +547,25 @@ async def rbac_check(
|
|||
return True
|
||||
|
||||
else:
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
isUserAnon = await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, org_id, db_session
|
||||
isAllowedOnOrgAdminStatus = (
|
||||
await authorization_verify_based_on_org_admin_status(
|
||||
request, current_user.id, action, org_uuid, db_session
|
||||
)
|
||||
)
|
||||
|
||||
if isUserAnon:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="You should be logged in to be able to achieve this action",
|
||||
)
|
||||
|
||||
if not isAllowedOnOrgAdminStatus:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User rights (admin status) : You don't have the right to perform this action",
|
||||
)
|
||||
|
||||
|
||||
## 🔒 RBAC Utils ##
|
||||
|
|
|
|||
410
apps/api/src/services/orgs/users.py
Normal file
410
apps/api/src/services/orgs/users.py
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import logging
|
||||
|
||||
import redis
|
||||
from fastapi import HTTPException, Request
|
||||
from sqlmodel import Session, select
|
||||
from config.config import get_learnhouse_config
|
||||
from src.services.orgs.orgs import rbac_check
|
||||
from src.db.roles import Role, RoleRead
|
||||
from src.db.users import AnonymousUser, PublicUser, User, UserRead
|
||||
from src.db.user_organizations import UserOrganization
|
||||
from src.db.organizations import (
|
||||
Organization,
|
||||
OrganizationUser,
|
||||
)
|
||||
|
||||
|
||||
async def get_organization_users(
|
||||
request: Request,
|
||||
org_id: str,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
) -> list[OrganizationUser]:
|
||||
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, "read", db_session)
|
||||
|
||||
statement = (
|
||||
select(User)
|
||||
.join(UserOrganization)
|
||||
.join(Organization)
|
||||
.where(Organization.id == org_id)
|
||||
)
|
||||
users = db_session.exec(statement)
|
||||
users = users.all()
|
||||
|
||||
org_users_list = []
|
||||
|
||||
for user in users:
|
||||
statement = select(UserOrganization).where(
|
||||
UserOrganization.user_id == user.id, UserOrganization.org_id == org_id
|
||||
)
|
||||
result = db_session.exec(statement)
|
||||
user_org = result.first()
|
||||
|
||||
if not user_org:
|
||||
logging.error(f"User {user.id} not found")
|
||||
|
||||
# skip this user
|
||||
continue
|
||||
|
||||
statement = select(Role).where(Role.id == user_org.role_id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
role = result.first()
|
||||
|
||||
if not role:
|
||||
logging.error(f"Role {user_org.role_id} not found")
|
||||
|
||||
# skip this user
|
||||
continue
|
||||
|
||||
statement = select(User).where(User.id == user_org.user_id)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
user = result.first()
|
||||
|
||||
if not user:
|
||||
logging.error(f"User {user_org.user_id} not found")
|
||||
|
||||
# skip this user
|
||||
continue
|
||||
|
||||
user = UserRead.from_orm(user)
|
||||
role = RoleRead.from_orm(role)
|
||||
|
||||
org_user = OrganizationUser(
|
||||
user=user,
|
||||
role=role,
|
||||
)
|
||||
|
||||
org_users_list.append(org_user)
|
||||
|
||||
return org_users_list
|
||||
|
||||
|
||||
async def remove_user_from_org(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
user_id: int,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
):
|
||||
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, "delete", db_session)
|
||||
|
||||
statement = select(UserOrganization).where(
|
||||
UserOrganization.user_id == user_id, UserOrganization.org_id == org.id
|
||||
)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
user_org = result.first()
|
||||
|
||||
if not user_org:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
# Check if user is the last admin
|
||||
statement = select(UserOrganization).where(
|
||||
UserOrganization.org_id == org.id, UserOrganization.role_id == 1
|
||||
)
|
||||
result = db_session.exec(statement)
|
||||
admins = result.all()
|
||||
|
||||
if len(admins) == 1 and admins[0].user_id == user_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="You can't remove the last admin of the organization",
|
||||
)
|
||||
|
||||
db_session.delete(user_org)
|
||||
db_session.commit()
|
||||
|
||||
return {"detail": "User removed from org"}
|
||||
|
||||
|
||||
async def update_user_role(
|
||||
request: Request,
|
||||
org_id: str,
|
||||
user_id: str,
|
||||
role_uuid: str,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
):
|
||||
# find role
|
||||
statement = select(Role).where(Role.role_uuid == role_uuid)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
role = result.first()
|
||||
|
||||
if not role:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Role not found",
|
||||
)
|
||||
|
||||
role_id = role.id
|
||||
|
||||
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)
|
||||
|
||||
# Check if user is the last admin and if the new role is not admin
|
||||
statement = select(UserOrganization).where(
|
||||
UserOrganization.org_id == org.id, UserOrganization.role_id == 1
|
||||
)
|
||||
result = db_session.exec(statement)
|
||||
admins = result.all()
|
||||
|
||||
if not admins:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="There is no admin in the organization",
|
||||
)
|
||||
|
||||
if (
|
||||
len(admins) == 1
|
||||
and int(admins[0].user_id) == int(user_id)
|
||||
and str(role_uuid) != "role_global_admin"
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Organization must have at least one admin",
|
||||
)
|
||||
|
||||
statement = select(UserOrganization).where(
|
||||
UserOrganization.user_id == user_id, UserOrganization.org_id == org.id
|
||||
)
|
||||
result = db_session.exec(statement)
|
||||
|
||||
user_org = result.first()
|
||||
|
||||
if not user_org:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
if role_id is not None:
|
||||
user_org.role_id = role_id
|
||||
|
||||
db_session.add(user_org)
|
||||
db_session.commit()
|
||||
db_session.refresh(user_org)
|
||||
|
||||
return {"detail": "User role updated"}
|
||||
|
||||
|
||||
async def invite_batch_users(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
emails: str,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
):
|
||||
# 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, "create", 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",
|
||||
)
|
||||
|
||||
invite_list = emails.split(",")
|
||||
|
||||
# invitations expire after 30 days
|
||||
ttl = int(timedelta(days=365).total_seconds())
|
||||
|
||||
for email in invite_list:
|
||||
email = email.strip()
|
||||
|
||||
# Check if user is already invited
|
||||
invited_user = r.get(f"invited_user:{email}:org:{org.org_uuid}")
|
||||
|
||||
if invited_user:
|
||||
logging.error(f"User {email} already invited")
|
||||
# skip this user
|
||||
continue
|
||||
|
||||
invited_user_object = {
|
||||
"email": email,
|
||||
"org_id": org.id,
|
||||
"pending": True,
|
||||
"email_sent": False,
|
||||
"expires": ttl,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"created_by": current_user.user_uuid,
|
||||
}
|
||||
|
||||
invited_user = r.set(
|
||||
f"invited_user:{email}:org:{org.org_uuid}",
|
||||
json.dumps(invited_user_object),
|
||||
ex=ttl,
|
||||
)
|
||||
|
||||
return {"detail": "Users invited"}
|
||||
|
||||
|
||||
async def get_list_of_invited_users(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
):
|
||||
# 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, "read", 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",
|
||||
)
|
||||
|
||||
invited_users = r.keys(f"invited_user:*:org:{org.org_uuid}")
|
||||
|
||||
invited_users_list = []
|
||||
|
||||
for user in invited_users:
|
||||
invited_user = r.get(user)
|
||||
if invited_user:
|
||||
invited_user = json.loads(invited_user.decode("utf-8"))
|
||||
invited_users_list.append(invited_user)
|
||||
|
||||
return invited_users_list
|
||||
|
||||
|
||||
async def remove_invited_user(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
email: str,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
):
|
||||
# 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, "delete", 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",
|
||||
)
|
||||
|
||||
invited_user = r.get(f"invited_user:{email}:org:{org.org_uuid}")
|
||||
|
||||
if not invited_user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
r.delete(f"invited_user:{email}:org:{org.org_uuid}")
|
||||
|
||||
return {"detail": "User removed"}
|
||||
|
|
@ -17,7 +17,9 @@ async def create_user_trail(
|
|||
trail_object: TrailCreate,
|
||||
db_session: Session,
|
||||
) -> Trail:
|
||||
statement = select(Trail).where(Trail.org_id == trail_object.org_id, Trail.user_id == user.id)
|
||||
statement = select(Trail).where(
|
||||
Trail.org_id == trail_object.org_id, Trail.user_id == user.id
|
||||
)
|
||||
trail = db_session.exec(statement).first()
|
||||
|
||||
if trail:
|
||||
|
|
@ -213,7 +215,7 @@ async def add_activity_to_trail(
|
|||
)
|
||||
|
||||
statement = select(TrailRun).where(
|
||||
TrailRun.trail_id == trail.id, TrailRun.course_id == course.id
|
||||
TrailRun.trail_id == trail.id, TrailRun.course_id == course.id, TrailRun.user_id == user.id
|
||||
)
|
||||
trailrun = db_session.exec(statement).first()
|
||||
|
||||
|
|
@ -231,7 +233,7 @@ async def add_activity_to_trail(
|
|||
db_session.refresh(trailrun)
|
||||
|
||||
statement = select(TrailStep).where(
|
||||
TrailStep.trailrun_id == trailrun.id, TrailStep.activity_id == activity.id
|
||||
TrailStep.trailrun_id == trailrun.id, TrailStep.activity_id == activity.id, TrailStep.user_id == user.id
|
||||
)
|
||||
trailstep = db_session.exec(statement).first()
|
||||
|
||||
|
|
@ -253,7 +255,7 @@ async def add_activity_to_trail(
|
|||
db_session.commit()
|
||||
db_session.refresh(trailstep)
|
||||
|
||||
statement = select(TrailRun).where(TrailRun.trail_id == trail.id)
|
||||
statement = select(TrailRun).where(TrailRun.trail_id == trail.id , TrailRun.user_id == user.id)
|
||||
trail_runs = db_session.exec(statement).all()
|
||||
|
||||
trail_runs = [
|
||||
|
|
@ -262,7 +264,7 @@ async def add_activity_to_trail(
|
|||
]
|
||||
|
||||
for trail_run in trail_runs:
|
||||
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id)
|
||||
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id, TrailStep.user_id == user.id)
|
||||
trail_steps = db_session.exec(statement).all()
|
||||
|
||||
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
|
||||
|
|
@ -296,7 +298,9 @@ async def add_course_to_trail(
|
|||
)
|
||||
|
||||
# check if run already exists
|
||||
statement = select(TrailRun).where(TrailRun.course_id == course.id)
|
||||
statement = select(TrailRun).where(
|
||||
TrailRun.course_id == course.id, TrailRun.user_id == user.id
|
||||
)
|
||||
trailrun = db_session.exec(statement).first()
|
||||
|
||||
if trailrun:
|
||||
|
|
@ -315,7 +319,7 @@ async def add_course_to_trail(
|
|||
)
|
||||
|
||||
statement = select(TrailRun).where(
|
||||
TrailRun.trail_id == trail.id, TrailRun.course_id == course.id
|
||||
TrailRun.trail_id == trail.id, TrailRun.course_id == course.id, TrailRun.user_id == user.id
|
||||
)
|
||||
trail_run = db_session.exec(statement).first()
|
||||
|
||||
|
|
@ -332,7 +336,7 @@ async def add_course_to_trail(
|
|||
db_session.commit()
|
||||
db_session.refresh(trail_run)
|
||||
|
||||
statement = select(TrailRun).where(TrailRun.trail_id == trail.id)
|
||||
statement = select(TrailRun).where(TrailRun.trail_id == trail.id, TrailRun.user_id == user.id)
|
||||
trail_runs = db_session.exec(statement).all()
|
||||
|
||||
trail_runs = [
|
||||
|
|
@ -341,7 +345,7 @@ async def add_course_to_trail(
|
|||
]
|
||||
|
||||
for trail_run in trail_runs:
|
||||
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id)
|
||||
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id , TrailStep.user_id == user.id)
|
||||
trail_steps = db_session.exec(statement).all()
|
||||
|
||||
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
|
||||
|
|
@ -385,7 +389,7 @@ async def remove_course_from_trail(
|
|||
)
|
||||
|
||||
statement = select(TrailRun).where(
|
||||
TrailRun.trail_id == trail.id, TrailRun.course_id == course.id
|
||||
TrailRun.trail_id == trail.id, TrailRun.course_id == course.id, TrailRun.user_id == user.id
|
||||
)
|
||||
trail_run = db_session.exec(statement).first()
|
||||
|
||||
|
|
@ -394,14 +398,14 @@ async def remove_course_from_trail(
|
|||
db_session.commit()
|
||||
|
||||
# Delete all trail steps for this course
|
||||
statement = select(TrailStep).where(TrailStep.course_id == course.id)
|
||||
statement = select(TrailStep).where(TrailStep.course_id == course.id, TrailStep.user_id == user.id)
|
||||
trail_steps = db_session.exec(statement).all()
|
||||
|
||||
for trail_step in trail_steps:
|
||||
db_session.delete(trail_step)
|
||||
db_session.commit()
|
||||
|
||||
statement = select(TrailRun).where(TrailRun.trail_id == trail.id)
|
||||
statement = select(TrailRun).where(TrailRun.trail_id == trail.id, TrailRun.user_id == user.id)
|
||||
trail_runs = db_session.exec(statement).all()
|
||||
|
||||
trail_runs = [
|
||||
|
|
@ -410,7 +414,7 @@ async def remove_course_from_trail(
|
|||
]
|
||||
|
||||
for trail_run in trail_runs:
|
||||
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id)
|
||||
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id, TrailStep.user_id == user.id)
|
||||
trail_steps = db_session.exec(statement).all()
|
||||
|
||||
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
|
||||
|
|
|
|||
16
apps/api/src/services/users/avatars.py
Normal file
16
apps/api/src/services/users/avatars.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from src.services.utils.upload_content import upload_content
|
||||
|
||||
|
||||
async def upload_avatar(avatar_file, name_in_disk, user_uuid):
|
||||
contents = avatar_file.file.read()
|
||||
try:
|
||||
await upload_content(
|
||||
"avatars",
|
||||
"users",
|
||||
user_uuid,
|
||||
contents,
|
||||
f"{name_in_disk}",
|
||||
)
|
||||
|
||||
except Exception:
|
||||
return {"message": "There was an error uploading the file"}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
from datetime import datetime
|
||||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
from fastapi import HTTPException, Request, status
|
||||
from fastapi import HTTPException, Request, UploadFile, status
|
||||
from sqlmodel import Session, select
|
||||
from src.services.orgs.invites import get_invite_code
|
||||
from src.services.users.avatars import upload_avatar
|
||||
from src.db.roles import Role, RoleRead
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
|
|
@ -102,6 +104,27 @@ async def create_user(
|
|||
|
||||
return user
|
||||
|
||||
async def create_user_with_invite(
|
||||
request: Request,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
user_object: UserCreate,
|
||||
org_id: int,
|
||||
invite_code: str,
|
||||
):
|
||||
|
||||
# Check if invite code exists
|
||||
isInviteCodeCorrect = await get_invite_code(request, org_id, invite_code, current_user, db_session)
|
||||
|
||||
if not isInviteCodeCorrect:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invite code is incorrect",
|
||||
)
|
||||
|
||||
user = await create_user(request, db_session, current_user, user_object, org_id)
|
||||
|
||||
return user
|
||||
|
||||
async def create_user_without_org(
|
||||
request: Request,
|
||||
|
|
@ -195,6 +218,49 @@ async def update_user(
|
|||
return user
|
||||
|
||||
|
||||
async def update_user_avatar(
|
||||
request: Request,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
avatar_file: UploadFile | None = None,
|
||||
):
|
||||
# Get user
|
||||
statement = select(User).where(User.id == current_user.id)
|
||||
user = db_session.exec(statement).first()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="User does not exist",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, current_user, "update", user.user_uuid, db_session)
|
||||
|
||||
# Upload thumbnail
|
||||
if avatar_file and avatar_file.filename:
|
||||
name_in_disk = f"{user.user_uuid}_avatar_{uuid4()}.{avatar_file.filename.split('.')[-1]}"
|
||||
await upload_avatar(avatar_file, name_in_disk, user.user_uuid)
|
||||
|
||||
# Update course
|
||||
if name_in_disk:
|
||||
user.avatar_image = name_in_disk
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Issue with Avatar upload",
|
||||
)
|
||||
|
||||
# Update user in database
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
db_session.refresh(user)
|
||||
|
||||
user = UserRead.from_orm(user)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def update_user_password(
|
||||
request: Request,
|
||||
db_session: Session,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from typing import Literal
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
import os
|
||||
|
|
@ -6,7 +7,11 @@ from config.config import get_learnhouse_config
|
|||
|
||||
|
||||
async def upload_content(
|
||||
directory: str, org_uuid: str, file_binary: bytes, file_and_format: str
|
||||
directory: str,
|
||||
type_of_dir: Literal["orgs", "users"],
|
||||
uuid: str, # org_uuid or user_uuid
|
||||
file_binary: bytes,
|
||||
file_and_format: str,
|
||||
):
|
||||
# Get Learnhouse Config
|
||||
learnhouse_config = get_learnhouse_config()
|
||||
|
|
@ -16,12 +21,12 @@ async def upload_content(
|
|||
|
||||
if content_delivery == "filesystem":
|
||||
# create folder for activity
|
||||
if not os.path.exists(f"content/{org_uuid}/{directory}"):
|
||||
if not os.path.exists(f"content/{type_of_dir}/{uuid}/{directory}"):
|
||||
# create folder for activity
|
||||
os.makedirs(f"content/{org_uuid}/{directory}")
|
||||
os.makedirs(f"content/{type_of_dir}/{uuid}/{directory}")
|
||||
# upload file to server
|
||||
with open(
|
||||
f"content/{org_uuid}/{directory}/{file_and_format}",
|
||||
f"content/{type_of_dir}/{uuid}/{directory}/{file_and_format}",
|
||||
"wb",
|
||||
) as f:
|
||||
f.write(file_binary)
|
||||
|
|
@ -37,13 +42,13 @@ async def upload_content(
|
|||
)
|
||||
|
||||
# Create folder for activity
|
||||
if not os.path.exists(f"content/{org_uuid}/{directory}"):
|
||||
if not os.path.exists(f"content/{type_of_dir}/{uuid}/{directory}"):
|
||||
# create folder for activity
|
||||
os.makedirs(f"content/{org_uuid}/{directory}")
|
||||
os.makedirs(f"content/{type_of_dir}/{uuid}/{directory}")
|
||||
|
||||
# Upload file to server
|
||||
with open(
|
||||
f"content/{org_uuid}/{directory}/{file_and_format}",
|
||||
f"content/{type_of_dir}/{uuid}/{directory}/{file_and_format}",
|
||||
"wb",
|
||||
) as f:
|
||||
f.write(file_binary)
|
||||
|
|
@ -52,9 +57,9 @@ async def upload_content(
|
|||
print("Uploading to s3 using boto3...")
|
||||
try:
|
||||
s3.upload_file(
|
||||
f"content/{org_uuid}/{directory}/{file_and_format}",
|
||||
f"content/{type_of_dir}/{uuid}/{directory}/{file_and_format}",
|
||||
"learnhouse-media",
|
||||
f"content/{org_uuid}/{directory}/{file_and_format}",
|
||||
f"content/{type_of_dir}/{uuid}/{directory}/{file_and_format}",
|
||||
)
|
||||
except ClientError as e:
|
||||
print(e)
|
||||
|
|
@ -63,7 +68,7 @@ async def upload_content(
|
|||
try:
|
||||
s3.head_object(
|
||||
Bucket="learnhouse-media",
|
||||
Key=f"content/{org_uuid}/{directory}/{file_and_format}",
|
||||
Key=f"content/{type_of_dir}/{uuid}/{directory}/{file_and_format}",
|
||||
)
|
||||
print("File upload successful!")
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
"rules": {
|
||||
"react/no-unescaped-entities": "off",
|
||||
"@next/next/no-page-custom-font": "off",
|
||||
"@next/next/no-img-element": "off"
|
||||
}
|
||||
"@next/next/no-img-element": "off",
|
||||
"unused-imports/no-unused-imports": "warn"
|
||||
},
|
||||
"plugins": ["unused-imports"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
FROM node:16-alpine
|
||||
FROM node:18-alpine
|
||||
|
||||
#
|
||||
WORKDIR /usr/learnhouse/front
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
|
|||
import { cookies } from "next/headers";
|
||||
import { Metadata } from "next";
|
||||
import { getActivityWithAuthHeader } from "@services/courses/activities";
|
||||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
|
||||
import { getOrganizationContextInfo, getOrganizationContextInfoWithId } from "@services/organizations/orgs";
|
||||
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
|
||||
import { getOrganizationContextInfoWithId } from "@services/organizations/orgs";
|
||||
import SessionProvider from "@components/Contexts/SessionContext";
|
||||
import EditorOptionsProvider from "@components/Contexts/Editor/EditorContext";
|
||||
import AIChatBotProvider from "@components/Contexts/AI/AIChatBotContext";
|
||||
import AIEditorProvider from "@components/Contexts/AI/AIEditorContext";
|
||||
|
||||
type MetadataProps = {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,24 @@
|
|||
'use client'
|
||||
import React, { use, useEffect } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { INSTALL_STEPS } from './steps/steps'
|
||||
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
|
||||
|
||||
import { Suspense } from 'react'
|
||||
|
||||
|
||||
function InstallClient() {
|
||||
return (
|
||||
<GeneralWrapperStyled>
|
||||
<Suspense>
|
||||
<>
|
||||
<Stepscomp />
|
||||
</>
|
||||
</Suspense>
|
||||
</GeneralWrapperStyled>
|
||||
)
|
||||
}
|
||||
|
||||
const Stepscomp = () => {
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const step: any = parseInt(searchParams.get('step') || '0');
|
||||
|
|
@ -24,7 +35,7 @@ function InstallClient() {
|
|||
}, [step])
|
||||
|
||||
return (
|
||||
<GeneralWrapperStyled>
|
||||
<div>
|
||||
<div className='flex justify-center '>
|
||||
<div className='grow'>
|
||||
<LearnHouseLogo />
|
||||
|
|
@ -54,7 +65,7 @@ function InstallClient() {
|
|||
{stepsState[stepNumber].component}
|
||||
</div>
|
||||
</div>
|
||||
</GeneralWrapperStyled>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"use client";
|
||||
import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input } from '@components/StyledElements/Form/Form'
|
||||
import FormLayout, { ButtonBlack, FormField, FormLabelAndMessage, Input } from '@components/StyledElements/Form/Form'
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import { createNewUserInstall, updateInstall } from '@services/install/install';
|
||||
|
|
@ -8,7 +8,7 @@ import { useFormik } from 'formik';
|
|||
import { useRouter } from 'next/navigation';
|
||||
import React from 'react'
|
||||
import { BarLoader } from 'react-spinners';
|
||||
import useSWR, { mutate } from "swr";
|
||||
import useSWR from "swr";
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import PageLoading from '@components/Objects/Loaders/PageLoading';
|
|||
import { getAPIUrl } from '@services/config/config';
|
||||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { use, useEffect } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
function GetStarted() {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
|
||||
import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input } from '@components/StyledElements/Form/Form'
|
||||
import FormLayout, { ButtonBlack, FormField, FormLabelAndMessage, Input } from '@components/StyledElements/Form/Form'
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import { useFormik } from 'formik';
|
||||
import { BarLoader } from 'react-spinners';
|
||||
import React from 'react'
|
||||
import { createNewOrganization } from '@services/organizations/orgs';
|
||||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import useSWR, { mutate } from "swr";
|
||||
import useSWR from "swr";
|
||||
import { createNewOrgInstall, updateInstall } from '@services/install/install';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Check } from 'lucide-react';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { createSampleDataInstall, updateInstall } from '@services/install/instal
|
|||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React from 'react'
|
||||
import useSWR, { mutate } from "swr";
|
||||
import useSWR from "swr";
|
||||
|
||||
function SampleData() {
|
||||
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
||||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
|
||||
import { getBackendUrl, getUriWithOrg } from "@services/config/config";
|
||||
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
|
||||
import { getUriWithOrg } from "@services/config/config";
|
||||
import { getCollectionByIdWithAuthHeader } from "@services/courses/collections";
|
||||
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
||||
|
|
|
|||
|
|
@ -1,29 +1,26 @@
|
|||
"use client";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { createCollection } from "@services/courses/collections";
|
||||
import useSWR from "swr";
|
||||
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
|
||||
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
|
||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
||||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
|
||||
function NewCollection(params: any) {
|
||||
const org = useOrg() as any;
|
||||
const orgslug = params.params.orgslug;
|
||||
const [name, setName] = React.useState("");
|
||||
const [org, setOrg] = React.useState({}) as any;
|
||||
const [description, setDescription] = React.useState("");
|
||||
const [selectedCourses, setSelectedCourses] = React.useState([]) as any;
|
||||
const router = useRouter();
|
||||
|
||||
const { data: courses, error: error } = useSWR(`${getAPIUrl()}courses/org_slug/${orgslug}/page/1/limit/10`, swrFetcher);
|
||||
const [isPublic, setIsPublic] = useState('true');
|
||||
|
||||
const handleVisibilityChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setIsPublic(e.target.value);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
async function getOrg() {
|
||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800 });
|
||||
setOrg(org);
|
||||
}
|
||||
getOrg();
|
||||
}, []);
|
||||
|
||||
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(event.target.value);
|
||||
|
|
@ -40,78 +37,89 @@ function NewCollection(params: any) {
|
|||
name: name,
|
||||
description: description,
|
||||
courses: selectedCourses,
|
||||
public: true,
|
||||
public: isPublic,
|
||||
org_id: org.id,
|
||||
};
|
||||
await createCollection(collection);
|
||||
await revalidateTags(["collections"], orgslug);
|
||||
await revalidateTags(["collections"], org.slug);
|
||||
// reload the page
|
||||
router.refresh();
|
||||
router.prefetch(getUriWithOrg(orgslug, "/collections"));
|
||||
router.push(getUriWithOrg(orgslug, "/collections"));
|
||||
|
||||
// wait for 2s before reloading the page
|
||||
setTimeout(() => {
|
||||
router.push(getUriWithOrg(orgslug, "/collections"));
|
||||
}
|
||||
, 1000);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-64 m-auto py-20">
|
||||
<div className="font-bold text-lg mb-4">Add new</div>
|
||||
<div className="font-bold text-lg mb-4">Add new</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
|
||||
{!courses ? (
|
||||
<select
|
||||
onChange={handleVisibilityChange}
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
defaultValue={isPublic}
|
||||
>
|
||||
<option value="false">Private Collection</option>
|
||||
<option value="true">Public Collection </option>
|
||||
</select>
|
||||
|
||||
|
||||
{!courses ? (
|
||||
<p className="text-gray-500">Loading...</p>
|
||||
) : (
|
||||
<div>
|
||||
<div className="space-y-4 p-3">
|
||||
<p>Courses</p>
|
||||
{courses.map((course: any) => (
|
||||
<div key={course.course_uuid} className="flex items-center mb-2">
|
||||
<div key={course.course_uuid} className="flex items-center space-x-2">
|
||||
|
||||
<input
|
||||
<input
|
||||
type="checkbox"
|
||||
id={course.id}
|
||||
name={course.name}
|
||||
value={course.id}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedCourses([...selectedCourses, course.id]);
|
||||
}
|
||||
else {
|
||||
setSelectedCourses(selectedCourses.filter((course_uuid: any) => course_uuid !== course.course_uuid));
|
||||
}
|
||||
}}
|
||||
className="text-blue-500 rounded focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
|
||||
type="checkbox"
|
||||
id={course.id}
|
||||
name={course.name}
|
||||
value={course.id}
|
||||
// id is an integer, not a string
|
||||
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedCourses([...selectedCourses, course.id]);
|
||||
}
|
||||
else {
|
||||
setSelectedCourses(selectedCourses.filter((course_uuid: any) => course_uuid !== course.course_uuid));
|
||||
}
|
||||
}
|
||||
}
|
||||
className="mr-2"
|
||||
/>
|
||||
|
||||
<label htmlFor={course.course_uuid} className="text-sm">{course.name}</label>
|
||||
<label htmlFor={course.course_uuid} className="text-sm text-gray-700">{course.name}</label>
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Description"
|
||||
value={description}
|
||||
onChange={handleDescriptionChange}
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Description"
|
||||
value={description}
|
||||
onChange={handleDescriptionChange}
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="px-6 py-3 text-white bg-black rounded-lg shadow-md hover:bg-black focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="px-6 py-3 text-white bg-black rounded-lg shadow-md hover:bg-black focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { cookies } from "next/headers";
|
|||
import ActivityClient from "./activity";
|
||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
||||
import { Metadata } from "next";
|
||||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
|
||||
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
|
||||
|
||||
|
||||
type MetadataProps = {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
"use client";
|
||||
import { removeCourse, startCourse } from "@services/courses/activity";
|
||||
import Link from "next/link";
|
||||
import React, { use, useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getUriWithOrg } from "@services/config/config";
|
||||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
||||
import { revalidateTags } from "@services/utils/ts/requests";
|
||||
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
|
||||
import { useRouter } from "next/navigation";
|
||||
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
||||
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
||||
import { ArrowRight, Check, File, Sparkles, Star, Video } from "lucide-react";
|
||||
import Avvvatars from "avvvatars-react";
|
||||
import { getUser } from "@services/users/users";
|
||||
import { getCourseThumbnailMediaDirectory, getUserAvatarMediaDirectory } from "@services/media/media";
|
||||
import { ArrowRight, Check, File, Sparkles, Video } from "lucide-react";
|
||||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
import UserAvatar from "@components/Objects/UserAvatar";
|
||||
|
||||
const CourseClient = (props: any) => {
|
||||
const [user, setUser] = useState<any>({});
|
||||
|
|
@ -25,7 +24,7 @@ const CourseClient = (props: any) => {
|
|||
|
||||
function getLearningTags() {
|
||||
// create array of learnings from a string object (comma separated)
|
||||
let learnings = course.learnings.split(",");
|
||||
let learnings = course?.learnings ? course?.learnings.split(",") : [];
|
||||
setLearnings(learnings);
|
||||
|
||||
}
|
||||
|
|
@ -56,13 +55,13 @@ const CourseClient = (props: any) => {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
getLearningTags();
|
||||
}
|
||||
, [org]);
|
||||
, [org, course]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!course ? (
|
||||
{!course && !org ? (
|
||||
<PageLoading></PageLoading>
|
||||
) : (
|
||||
<GeneralWrapperStyled>
|
||||
|
|
@ -73,9 +72,13 @@ const CourseClient = (props: any) => {
|
|||
</h1>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[300px] bg-cover bg-center mb-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org?.org_uuid, course.course_uuid, course.thumbnail_image)})` }}>
|
||||
</div>
|
||||
{props.course?.thumbnail_image && org ?
|
||||
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[400px] bg-cover bg-center mb-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org?.org_uuid, course?.course_uuid, course?.thumbnail_image)})` }}>
|
||||
</div>
|
||||
:
|
||||
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[400px] bg-cover bg-center mb-4" style={{ backgroundImage: `url('../empty_thumbnail.png')`, backgroundSize: 'auto' }}>
|
||||
</div>
|
||||
}
|
||||
|
||||
<ActivityIndicators course_uuid={props.course.course_uuid} orgslug={orgslug} course={course} />
|
||||
|
||||
|
|
@ -86,21 +89,25 @@ const CourseClient = (props: any) => {
|
|||
<p className="py-5 px-5">{course.description}</p>
|
||||
</div>
|
||||
|
||||
<h2 className="py-3 text-2xl font-bold">What you will learn</h2>
|
||||
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden px-5 py-5 space-y-2">
|
||||
{learnings.map((learning: any) => {
|
||||
return (
|
||||
<div key={learning}
|
||||
className="flex space-x-2 items-center font-semibold text-gray-500 capitalize">
|
||||
<div className="px-2 py-2 rounded-full">
|
||||
<Check className="text-gray-400" size={15} />
|
||||
</div>
|
||||
<p>{learning}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
{learnings.length > 0 && learnings[0] !== "null" &&
|
||||
<div>
|
||||
<h2 className="py-3 text-2xl font-bold">What you will learn</h2>
|
||||
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden px-5 py-5 space-y-2">
|
||||
{learnings.map((learning: any) => {
|
||||
return (
|
||||
<div key={learning}
|
||||
className="flex space-x-2 items-center font-semibold text-gray-500">
|
||||
<div className="px-2 py-2 rounded-full">
|
||||
<Check className="text-gray-400" size={15} />
|
||||
</div>
|
||||
<p>{learning}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<h2 className="py-3 text-2xl font-bold">Course Lessons</h2>
|
||||
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
|
||||
|
|
@ -185,15 +192,22 @@ const CourseClient = (props: any) => {
|
|||
</div>
|
||||
|
||||
</div>
|
||||
<div className="course_metadata_right space-y-3 w-64 antialiased flex flex-col ml-10 h-fit p-3 py-5 bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
|
||||
<div className="course_metadata_right space-y-3 w-72 antialiased flex flex-col ml-10 h-fit p-3 py-5 bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
|
||||
{user &&
|
||||
<div className="flex mx-auto space-x-3 px-2 py-2 items-center">
|
||||
<div className="">
|
||||
<Avvvatars border borderSize={5} borderColor="white" size={50} shadow value={course.authors[0].username} style='shape' />
|
||||
</div>
|
||||
<div className="flex flex-col mx-auto space-y-3 px-2 py-2 items-center">
|
||||
<UserAvatar border="border-8" avatar_url={getUserAvatarMediaDirectory(course.authors[0].user_uuid, course.authors[0].avatar_image)} width={100} />
|
||||
<div className="-space-y-2 ">
|
||||
<div className="text-[12px] text-neutral-400 font-semibold">Author</div>
|
||||
<div className="text-xl font-bold text-neutral-800">{course.authors[0].first_name} {course.authors[0].last_name} {(course.authors[0].first_name && course.authors[0].last_name) ? course.authors[0].first_name + ' ' + course.authors[0].last_name : course.authors[0].username}</div>
|
||||
<div className="text-xl font-bold text-neutral-800">
|
||||
{course.authors[0].first_name && course.authors[0].last_name && (
|
||||
<div className="flex space-x-2 items-center">
|
||||
<p>{course.authors[0].first_name + ' ' + course.authors[0].last_name}</p><span className="text-xs bg-neutral-100 p-1 px-3 rounded-full text-neutral-400 font-semibold"> @{course.authors[0].username}</span>
|
||||
</div>)}
|
||||
{!course.authors[0].first_name && !course.authors[0].last_name && (
|
||||
<div className="flex space-x-2 items-center">
|
||||
<p>@{course.authors[0].username}</p>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -214,12 +228,4 @@ const CourseClient = (props: any) => {
|
|||
};
|
||||
|
||||
|
||||
const StyledBox = (props: any) => (
|
||||
<div className="p-3 pl-10 bg-white w-[100%] h-auto ring-1 ring-inset ring-gray-400/10 rounded-lg shadow-sm">
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
);
|
||||
|
||||
|
||||
export default CourseClient;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { cookies } from 'next/headers';
|
|||
import { getCourseMetadataWithAuthHeader } from '@services/courses/courses';
|
||||
import { getOrganizationContextInfo } from '@services/organizations/orgs';
|
||||
import { Metadata } from 'next';
|
||||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from '@services/auth/auth';
|
||||
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth';
|
||||
|
||||
type MetadataProps = {
|
||||
params: { orgslug: string, courseuuid: string };
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { getOrgCoursesWithAuthHeader } from "@services/courses/courses";
|
|||
import { Metadata } from "next";
|
||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
||||
import { cookies } from "next/headers";
|
||||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
|
||||
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
|
||||
|
||||
type MetadataProps = {
|
||||
params: { orgslug: string };
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export default function Error({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<ErrorUI></ErrorUI>
|
||||
<ErrorUI ></ErrorUI>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -12,7 +12,6 @@ import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth';
|
|||
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
|
||||
import CollectionThumbnail from '@components/Objects/Thumbnails/CollectionThumbnail';
|
||||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
||||
import { Plus, PlusCircle } from 'lucide-react';
|
||||
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
|
||||
import NewCollectionButton from '@components/StyledElements/Buttons/NewCollectionButton';
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ import TrailCourseElement from "@components/Pages/Trail/TrailCourseElement";
|
|||
import TypeOfContentTitle from "@components/StyledElements/Titles/TypeOfContentTitle";
|
||||
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
||||
import { getAPIUrl } from "@services/config/config";
|
||||
import { removeCourse } from "@services/courses/activity";
|
||||
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
|
||||
import { swrFetcher } from "@services/utils/ts/requests";
|
||||
import React, { useEffect } from "react";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import useSWR from "swr";
|
||||
|
||||
function Trail(params: any) {
|
||||
let orgslug = params.orgslug;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
|
|||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
||||
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
|
||||
import Modal from '@components/StyledElements/Modal/Modal';
|
||||
import Link from 'next/link'
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import React from 'react'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,13 @@
|
|||
'use client';
|
||||
import EditCourseStructure from '../../../../../../../../components/Dashboard/Course/EditCourseStructure/EditCourseStructure'
|
||||
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
|
||||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
||||
import ClientComponentSkeleton from '@components/Utils/ClientComp';
|
||||
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
|
||||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
import React, { createContext, use, useEffect, useState } from 'react'
|
||||
import useSWR from 'swr';
|
||||
import { CourseProvider, useCourse } from '../../../../../../../../components/Contexts/CourseContext';
|
||||
import SaveState from '@components/Dashboard/UI/SaveState';
|
||||
import { getUriWithOrg } from '@services/config/config';
|
||||
import React from 'react'
|
||||
import { CourseProvider } from '../../../../../../../../components/Contexts/CourseContext';
|
||||
import Link from 'next/link';
|
||||
import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import { motion } from 'framer-motion';
|
||||
import EditCourseGeneral from '@components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral';
|
||||
import { GalleryVertical, GalleryVerticalEnd, Info } from 'lucide-react';
|
||||
import { GalleryVerticalEnd, Info } from 'lucide-react';
|
||||
|
||||
export type CourseOverviewParams = {
|
||||
orgslug: string,
|
||||
|
|
@ -32,9 +25,9 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='h-full w-full bg-[#f8f8f8]'>
|
||||
<div className='h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]'>
|
||||
<CourseProvider courseuuid={getEntireCourseUUID(params.courseuuid)}>
|
||||
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'>
|
||||
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]'>
|
||||
<CourseOverviewTop params={params} />
|
||||
<div className='flex space-x-5 font-black text-sm'>
|
||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/courses/course/${params.courseuuid}/general`}>
|
||||
|
|
@ -57,12 +50,12 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
|||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-6'></div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
|
||||
className='h-full overflow-y-auto'
|
||||
>
|
||||
{params.subpage == 'content' ? <EditCourseStructure orgslug={params.orgslug} /> : ''}
|
||||
{params.subpage == 'general' ? <EditCourseGeneral orgslug={params.orgslug} /> : ''}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
import SessionProvider from '@components/Contexts/SessionContext'
|
||||
import LeftMenu from '@components/Dashboard/UI/LeftMenu'
|
||||
import AdminAuthorization from '@components/Security/AdminAuthorization'
|
||||
import React from 'react'
|
||||
|
||||
function DashboardLayout({ children, params }: { children: React.ReactNode, params: any }) {
|
||||
return (
|
||||
<>
|
||||
<SessionProvider>
|
||||
<div className='flex'>
|
||||
<LeftMenu/>
|
||||
<div className='flex w-full'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<AdminAuthorization authorizationMode="page">
|
||||
<div className='flex'>
|
||||
<LeftMenu />
|
||||
<div className='flex w-full'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</AdminAuthorization>
|
||||
</SessionProvider>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,62 @@
|
|||
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||
import Image from 'next/image'
|
||||
import React from 'react'
|
||||
import learnhousetextlogo from '../../../../public/learnhouse_logo.png'
|
||||
import { BookCopy, School, Settings, Users } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import AdminAuthorization from '@components/Security/AdminAuthorization'
|
||||
|
||||
function DashboardHome() {
|
||||
return (
|
||||
<div className="flex items-center justify-center mx-auto min-h-screen flex-col space-x-3">
|
||||
<PageLoading />
|
||||
<div className='text-neutral-400 font-bold animate-pulse text-2xl'>This page is work in progress</div>
|
||||
<div className='mx-auto pb-10'>
|
||||
<Image alt='learnhouse logo' width={230} src={learnhousetextlogo}></Image>
|
||||
</div>
|
||||
<AdminAuthorization authorizationMode="component">
|
||||
<div className='flex space-x-10'>
|
||||
<Link href={`/dash/courses`} className='flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
|
||||
<div className='flex flex-col mx-auto space-y-2'>
|
||||
<BookCopy className='mx-auto text-gray-500' size={50}></BookCopy>
|
||||
<div className='text-center font-bold text-gray-500'>Courses</div>
|
||||
<p className='text-center text-sm text-gray-400'>Create and manage courses, chapters and ativities </p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href={`/dash/org/settings/general`} className='flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
|
||||
<div className='flex flex-col mx-auto space-y-2'>
|
||||
<School className='mx-auto text-gray-500' size={50}></School>
|
||||
<div className='text-center font-bold text-gray-500'>Organization</div>
|
||||
<p className='text-center text-sm text-gray-400'>Configure your Organization general settings </p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href={`/dash/users/settings/users`} className='flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
|
||||
<div className='flex flex-col mx-auto space-y-2'>
|
||||
<Users className='mx-auto text-gray-500' size={50}></Users>
|
||||
<div className='text-center font-bold text-gray-500'>Users</div>
|
||||
<p className='text-center text-sm text-gray-400'>Manage your Organization's users, roles </p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
</AdminAuthorization>
|
||||
<div className='flex flex-col space-y-10 '>
|
||||
<AdminAuthorization authorizationMode="component">
|
||||
<div className='h-1 w-[100px] bg-neutral-200 rounded-full mx-auto'></div>
|
||||
<div className="flex justify-center items-center">
|
||||
<Link href={'https://learn.learnhouse.io/'} className='flex mt-[40px] bg-black space-x-2 items-center py-3 px-7 rounded-lg shadow-lg hover:scale-105 transition-all ease-linear cursor-pointer'>
|
||||
<BookCopy className=' text-gray-100' size={20}></BookCopy>
|
||||
<div className=' text-sm font-bold text-gray-100'>Learn LearnHouse</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className='mx-auto mt-[40px] w-28 h-1 bg-neutral-200 rounded-full'></div>
|
||||
</AdminAuthorization>
|
||||
|
||||
<Link href={'/dash/user-account/settings/general'} className='flex bg-white shadow-lg p-[15px] items-center rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
|
||||
<div className='flex flex-row mx-auto space-x-3 items-center'>
|
||||
<Settings className=' text-gray-500' size={20}></Settings>
|
||||
<div className=' font-bold text-gray-500'>Account Settings</div>
|
||||
<p className=' text-sm text-gray-400'>Configure your personal settings, passwords, email</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
'use client';
|
||||
import React, { useEffect } from 'react'
|
||||
import { motion } from 'framer-motion';
|
||||
import UserEditGeneral from '@components/Dashboard/User/UserEditGeneral/UserEditGeneral';
|
||||
import UserEditPassword from '@components/Dashboard/User/UserEditPassword/UserEditPassword';
|
||||
import UserEditGeneral from '@components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral';
|
||||
import UserEditPassword from '@components/Dashboard/UserAccount/UserEditPassword/UserEditPassword';
|
||||
import Link from 'next/link';
|
||||
import { getUriWithOrg } from '@services/config/config';
|
||||
import { Info, Lock } from 'lucide-react';
|
||||
|
|
@ -24,7 +24,7 @@ function SettingsPage({ params }: { params: SettingsParams }) {
|
|||
|
||||
return (
|
||||
<div className='h-full w-full bg-[#f8f8f8]'>
|
||||
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'>
|
||||
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]'>
|
||||
<BreadCrumbs type='user' last_breadcrumb={session?.user?.username} ></BreadCrumbs>
|
||||
<div className='my-2 tracking-tighter'>
|
||||
<div className='w-100 flex justify-between'>
|
||||
|
|
@ -32,7 +32,7 @@ function SettingsPage({ params }: { params: SettingsParams }) {
|
|||
</div>
|
||||
</div>
|
||||
<div className='flex space-x-5 font-black text-sm'>
|
||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/user/settings/general`}>
|
||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/user-account/settings/general`}>
|
||||
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
||||
|
||||
<div className='flex items-center space-x-2.5 mx-2'>
|
||||
|
|
@ -41,7 +41,7 @@ function SettingsPage({ params }: { params: SettingsParams }) {
|
|||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/user/settings/security`}>
|
||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/user-account/settings/security`}>
|
||||
<div className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'security' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
||||
<div className='flex items-center space-x-2.5 mx-2'>
|
||||
<Lock size={16} />
|
||||
|
|
@ -58,6 +58,7 @@ function SettingsPage({ params }: { params: SettingsParams }) {
|
|||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
|
||||
className='h-full overflow-y-auto'
|
||||
>
|
||||
{params.subpage == 'general' ? <UserEditGeneral /> : ''}
|
||||
{params.subpage == 'security' ? <UserEditPassword /> : ''}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
'use client';
|
||||
import React, { useEffect } from 'react'
|
||||
import { motion } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
import { getUriWithOrg } from '@services/config/config';
|
||||
import { ScanEye, UserPlus, Users } from 'lucide-react';
|
||||
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs';
|
||||
import { useSession } from '@components/Contexts/SessionContext';
|
||||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
import OrgUsers from '@components/Dashboard/Users/OrgUsers/OrgUsers';
|
||||
import OrgAccess from '@components/Dashboard/Users/OrgAccess/OrgAccess';
|
||||
|
||||
export type SettingsParams = {
|
||||
subpage: string
|
||||
orgslug: string
|
||||
}
|
||||
|
||||
function UsersSettingsPage({ params }: { params: SettingsParams }) {
|
||||
const session = useSession() as any;
|
||||
const org = useOrg() as any;
|
||||
const [H1Label, setH1Label] = React.useState('')
|
||||
const [H2Label, setH2Label] = React.useState('')
|
||||
|
||||
function handleLabels() {
|
||||
if (params.subpage == 'users') {
|
||||
setH1Label('Users')
|
||||
setH2Label('Manage your organization users, assign roles and permissions')
|
||||
}
|
||||
if (params.subpage == 'signups') {
|
||||
setH1Label('Signup Access')
|
||||
setH2Label('Choose from where users can join your organization')
|
||||
}
|
||||
if (params.subpage == 'add') {
|
||||
setH1Label('Invite users')
|
||||
setH2Label('Invite users to join your organization')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
handleLabels()
|
||||
}
|
||||
, [session, org, params.subpage, params])
|
||||
|
||||
return (
|
||||
<div className='h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]'>
|
||||
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]'>
|
||||
<BreadCrumbs type='orgusers' ></BreadCrumbs>
|
||||
<div className='my-2 py-3'>
|
||||
<div className='w-100 flex flex-col space-y-1'>
|
||||
<div className='pt-3 flex font-bold text-4xl tracking-tighter'>{H1Label}</div>
|
||||
<div className='flex font-medium text-gray-400 text-md'>{H2Label} </div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex space-x-5 font-black text-sm'>
|
||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/users/settings/users`}>
|
||||
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'users' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
||||
|
||||
<div className='flex items-center space-x-2.5 mx-2'>
|
||||
<Users size={16} />
|
||||
<div>Users</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 users</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/users/settings/signups`}>
|
||||
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'signups' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
||||
<div className='flex items-center space-x-2.5 mx-2'>
|
||||
<ScanEye size={16} />
|
||||
<div>Signup Access</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
|
||||
className='h-full overflow-y-auto'
|
||||
>
|
||||
{params.subpage == 'users' ? <OrgUsers /> : ''}
|
||||
{params.subpage == 'signups' ? <OrgAccess /> : ''}
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UsersSettingsPage
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
"use client";
|
||||
"use client";;
|
||||
import learnhouseIcon from "public/learnhouse_bigicon_1.png";
|
||||
import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input } from '@components/StyledElements/Form/Form'
|
||||
import FormLayout, { FormField, FormLabelAndMessage, Input } from '@components/StyledElements/Form/Form';
|
||||
import Image from 'next/image';
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import { useFormik } from 'formik';
|
||||
import { getOrgLogoMediaDirectory } from "@services/media/media";
|
||||
import { BarLoader } from "react-spinners";
|
||||
import React from "react";
|
||||
import { loginAndGetToken } from "@services/auth/auth";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
|
|
@ -79,14 +78,14 @@ const LoginClient = (props: LoginClientProps) => {
|
|||
<div className="m-auto flex space-x-4 items-center flex-wrap">
|
||||
<div>Login to </div>
|
||||
<div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]" >
|
||||
{props.org?.logo ? (
|
||||
{props.org?.logo_image ? (
|
||||
<img
|
||||
src={`${getOrgLogoMediaDirectory(props.org.org_id, props.org?.logo)}`}
|
||||
src={`${getOrgLogoMediaDirectory(props.org.org_uuid, props.org?.logo_image)}`}
|
||||
alt="Learnhouse"
|
||||
style={{ width: "auto", height: 70 }}
|
||||
className="rounded-md shadow-xl inset-0 ring-1 ring-inset ring-black/10 bg-white"
|
||||
className="rounded-xl shadow-xl inset-0 ring-1 ring-inset ring-black/10 bg-white"
|
||||
/>
|
||||
) : (
|
||||
) : (
|
||||
<Image quality={100} width={70} height={70} src={learnhouseIcon} alt="" />
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export async function generateMetadata(
|
|||
): Promise<Metadata> {
|
||||
const orgslug = params.orgslug;
|
||||
// Get Org context information
|
||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
|
||||
|
||||
return {
|
||||
title: 'Login' + ` — ${org.name}`,
|
||||
|
|
@ -21,7 +21,7 @@ export async function generateMetadata(
|
|||
|
||||
const Login = async (params: any) => {
|
||||
const orgslug = params.params.orgslug;
|
||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
166
apps/web/app/orgs/[orgslug]/signup/InviteOnlySignUp.tsx
Normal file
166
apps/web/app/orgs/[orgslug]/signup/InviteOnlySignUp.tsx
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
"use client";
|
||||
import { useFormik } from 'formik';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useEffect } from 'react'
|
||||
import FormLayout, { FormField, FormLabelAndMessage, Input, Textarea } from '@components/StyledElements/Form/Form';
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import { AlertTriangle, Check, User } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { signUpWithInviteCode } from '@services/auth/auth';
|
||||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
|
||||
|
||||
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {};
|
||||
|
||||
if (!values.email) {
|
||||
errors.email = 'Required';
|
||||
}
|
||||
else if (
|
||||
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
|
||||
) {
|
||||
errors.email = 'Invalid email address';
|
||||
}
|
||||
|
||||
if (!values.password) {
|
||||
errors.password = 'Required';
|
||||
}
|
||||
else if (values.password.length < 8) {
|
||||
errors.password = 'Password must be at least 8 characters';
|
||||
}
|
||||
|
||||
if (!values.username) {
|
||||
errors.username = 'Required';
|
||||
}
|
||||
|
||||
if (!values.username || values.username.length < 4) {
|
||||
errors.username = 'Username must be at least 4 characters';
|
||||
}
|
||||
|
||||
if (!values.bio) {
|
||||
errors.bio = 'Required';
|
||||
}
|
||||
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
interface InviteOnlySignUpProps {
|
||||
inviteCode: string;
|
||||
}
|
||||
|
||||
function InviteOnlySignUpComponent(props : InviteOnlySignUpProps) {
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
const org = useOrg() as any;
|
||||
const router = useRouter();
|
||||
const [error, setError] = React.useState('');
|
||||
const [message, setMessage] = React.useState('');
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
org_slug: org?.slug,
|
||||
org_id: org?.id,
|
||||
email: '',
|
||||
password: '',
|
||||
username: '',
|
||||
bio: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
},
|
||||
validate,
|
||||
onSubmit: async values => {
|
||||
setError('')
|
||||
setMessage('')
|
||||
setIsSubmitting(true);
|
||||
let res = await signUpWithInviteCode(values, props.inviteCode);
|
||||
let message = await res.json();
|
||||
if (res.status == 200) {
|
||||
//router.push(`/login`);
|
||||
setMessage('Your account was successfully created')
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
else if (res.status == 401 || res.status == 400 || res.status == 404 || res.status == 409) {
|
||||
setError(message.detail);
|
||||
setIsSubmitting(false);
|
||||
|
||||
}
|
||||
else {
|
||||
setError("Something went wrong");
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}
|
||||
, [org]);
|
||||
|
||||
return (
|
||||
<div className="login-form m-auto w-72">
|
||||
{error && (
|
||||
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
|
||||
<AlertTriangle size={18} />
|
||||
<div className="font-bold text-sm">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
{message && (
|
||||
<div className="flex flex-col space-y-4 justify-center bg-green-200 rounded-md text-green-950 space-x-2 items-center p-4 transition-all shadow-sm">
|
||||
<div className='flex space-x-2'>
|
||||
<Check size={18} />
|
||||
<div className="font-bold text-sm">{message}</div>
|
||||
</div>
|
||||
<hr className='border-green-900/20 800 w-40 border' />
|
||||
<Link className='flex space-x-2 items-center' href={'/login'}><User size={14} /> <div>Login </div></Link>
|
||||
</div>
|
||||
)}
|
||||
<FormLayout onSubmit={formik.handleSubmit}>
|
||||
<FormField name="email">
|
||||
<FormLabelAndMessage label='Email' message={formik.errors.email} />
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
{/* for password */}
|
||||
<FormField name="password">
|
||||
<FormLabelAndMessage label='Password' message={formik.errors.password} />
|
||||
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.password} type="password" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
{/* for username */}
|
||||
<FormField name="username">
|
||||
<FormLabelAndMessage label='Username' message={formik.errors.username} />
|
||||
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.username} type="text" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
{/* for bio */}
|
||||
<FormField name="bio">
|
||||
<FormLabelAndMessage label='Bio' message={formik.errors.bio} />
|
||||
|
||||
<Form.Control asChild>
|
||||
<Textarea onChange={formik.handleChange} value={formik.values.bio} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<div className="flex py-4">
|
||||
<Form.Submit asChild>
|
||||
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer" >
|
||||
{isSubmitting ? "Loading..."
|
||||
: "Create an account & Join"}
|
||||
</button>
|
||||
</Form.Submit>
|
||||
</div>
|
||||
|
||||
</FormLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InviteOnlySignUpComponent
|
||||
163
apps/web/app/orgs/[orgslug]/signup/OpenSignup.tsx
Normal file
163
apps/web/app/orgs/[orgslug]/signup/OpenSignup.tsx
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
"use client";
|
||||
import { useFormik } from 'formik';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useEffect } from 'react'
|
||||
import FormLayout, { FormField, FormLabelAndMessage, Input, Textarea } from '@components/StyledElements/Form/Form';
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import { AlertTriangle, Check, User } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { signup } from '@services/auth/auth';
|
||||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
|
||||
|
||||
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {};
|
||||
|
||||
if (!values.email) {
|
||||
errors.email = 'Required';
|
||||
}
|
||||
else if (
|
||||
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
|
||||
) {
|
||||
errors.email = 'Invalid email address';
|
||||
}
|
||||
|
||||
if (!values.password) {
|
||||
errors.password = 'Required';
|
||||
}
|
||||
else if (values.password.length < 8) {
|
||||
errors.password = 'Password must be at least 8 characters';
|
||||
}
|
||||
|
||||
if (!values.username) {
|
||||
errors.username = 'Required';
|
||||
}
|
||||
|
||||
if (!values.username || values.username.length < 4) {
|
||||
errors.username = 'Username must be at least 4 characters';
|
||||
}
|
||||
|
||||
if (!values.bio) {
|
||||
errors.bio = 'Required';
|
||||
}
|
||||
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
|
||||
function OpenSignUpComponent() {
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
const org = useOrg() as any;
|
||||
const router = useRouter();
|
||||
const [error, setError] = React.useState('');
|
||||
const [message, setMessage] = React.useState('');
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
org_slug: org?.slug,
|
||||
org_id: org?.id,
|
||||
email: '',
|
||||
password: '',
|
||||
username: '',
|
||||
bio: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
},
|
||||
validate,
|
||||
onSubmit: async values => {
|
||||
setError('')
|
||||
setMessage('')
|
||||
setIsSubmitting(true);
|
||||
let res = await signup(values);
|
||||
let message = await res.json();
|
||||
if (res.status == 200) {
|
||||
//router.push(`/login`);
|
||||
setMessage('Your account was successfully created')
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
else if (res.status == 401 || res.status == 400 || res.status == 404 || res.status == 409) {
|
||||
setError(message.detail);
|
||||
setIsSubmitting(false);
|
||||
|
||||
}
|
||||
else {
|
||||
setError("Something went wrong");
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}
|
||||
, [org]);
|
||||
|
||||
return (
|
||||
<div className="login-form m-auto w-72">
|
||||
{error && (
|
||||
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
|
||||
<AlertTriangle size={18} />
|
||||
<div className="font-bold text-sm">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
{message && (
|
||||
<div className="flex flex-col space-y-4 justify-center bg-green-200 rounded-md text-green-950 space-x-2 items-center p-4 transition-all shadow-sm">
|
||||
<div className='flex space-x-2'>
|
||||
<Check size={18} />
|
||||
<div className="font-bold text-sm">{message}</div>
|
||||
</div>
|
||||
<hr className='border-green-900/20 800 w-40 border' />
|
||||
<Link className='flex space-x-2 items-center' href={'/login'}><User size={14} /> <div>Login </div></Link>
|
||||
</div>
|
||||
)}
|
||||
<FormLayout onSubmit={formik.handleSubmit}>
|
||||
<FormField name="email">
|
||||
<FormLabelAndMessage label='Email' message={formik.errors.email} />
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
{/* for password */}
|
||||
<FormField name="password">
|
||||
<FormLabelAndMessage label='Password' message={formik.errors.password} />
|
||||
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.password} type="password" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
{/* for username */}
|
||||
<FormField name="username">
|
||||
<FormLabelAndMessage label='Username' message={formik.errors.username} />
|
||||
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.username} type="text" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
{/* for bio */}
|
||||
<FormField name="bio">
|
||||
<FormLabelAndMessage label='Bio' message={formik.errors.bio} />
|
||||
|
||||
<Form.Control asChild>
|
||||
<Textarea onChange={formik.handleChange} value={formik.values.bio} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<div className="flex py-4">
|
||||
<Form.Submit asChild>
|
||||
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer" >
|
||||
{isSubmitting ? "Loading..."
|
||||
: "Create an account"}
|
||||
</button>
|
||||
</Form.Submit>
|
||||
</div>
|
||||
|
||||
</FormLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OpenSignUpComponent
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
|
||||
import React from "react";
|
||||
import SignUpClient from "./signup";
|
||||
import { Metadata } from "next";
|
||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
||||
import SignUpClient from "./signup";
|
||||
import { Suspense } from "react";
|
||||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
||||
|
||||
type MetadataProps = {
|
||||
params: { orgslug: string, courseid: string };
|
||||
|
|
@ -14,7 +15,7 @@ export async function generateMetadata(
|
|||
): Promise<Metadata> {
|
||||
const orgslug = params.orgslug;
|
||||
// Get Org context information
|
||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
|
||||
|
||||
return {
|
||||
title: 'Sign up' + ` — ${org.name}`,
|
||||
|
|
@ -23,12 +24,14 @@ export async function generateMetadata(
|
|||
|
||||
const SignUp = async (params: any) => {
|
||||
const orgslug = params.params.orgslug;
|
||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SignUpClient org={org}></SignUpClient>
|
||||
</div>
|
||||
<>
|
||||
<Suspense fallback={<PageLoading/>}>
|
||||
<SignUpClient org={org} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default SignUp;
|
||||
|
|
|
|||
|
|
@ -1,117 +1,62 @@
|
|||
"use client";
|
||||
import { useFormik } from 'formik';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import learnhouseIcon from "public/learnhouse_bigicon_1.png";
|
||||
import React from 'react'
|
||||
import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input, Textarea } from '@components/StyledElements/Form/Form'
|
||||
import Image from 'next/image';
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import { getOrgLogoMediaDirectory } from '@services/media/media';
|
||||
import { AlertTriangle, Check, User } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { signup } from '@services/auth/auth';
|
||||
import { getUriWithOrg } from '@services/config/config';
|
||||
|
||||
import { useSession } from "@components/Contexts/SessionContext";
|
||||
import React, { useEffect } from "react";
|
||||
import { MailWarning, Shield, UserPlus } from "lucide-react";
|
||||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
import UserAvatar from "@components/Objects/UserAvatar";
|
||||
import OpenSignUpComponent from "./OpenSignup";
|
||||
import InviteOnlySignUpComponent from "./InviteOnlySignUp";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { validateInviteCode } from "@services/organizations/invites";
|
||||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
||||
import Toast from "@components/StyledElements/Toast/Toast";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface SignUpClientProps {
|
||||
org: any;
|
||||
}
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {};
|
||||
|
||||
if (!values.email) {
|
||||
errors.email = 'Required';
|
||||
}
|
||||
else if (
|
||||
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
|
||||
) {
|
||||
errors.email = 'Invalid email address';
|
||||
}
|
||||
|
||||
if (!values.password) {
|
||||
errors.password = 'Required';
|
||||
}
|
||||
else if (values.password.length < 8) {
|
||||
errors.password = 'Password must be at least 8 characters';
|
||||
}
|
||||
|
||||
if (!values.username) {
|
||||
errors.username = 'Required';
|
||||
}
|
||||
|
||||
if (!values.username || values.username.length < 4) {
|
||||
errors.username = 'Username must be at least 4 characters';
|
||||
}
|
||||
|
||||
if (!values.bio) {
|
||||
errors.bio = 'Required';
|
||||
}
|
||||
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
|
||||
function SignUpClient(props: SignUpClientProps) {
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
const router = useRouter();
|
||||
const [error, setError] = React.useState('');
|
||||
const [message, setMessage] = React.useState('');
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
org_slug: props.org?.slug,
|
||||
org_id: props.org?.id,
|
||||
email: '',
|
||||
password: '',
|
||||
username: '',
|
||||
bio: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
},
|
||||
validate,
|
||||
onSubmit: async values => {
|
||||
setError('')
|
||||
setMessage('')
|
||||
setIsSubmitting(true);
|
||||
let res = await signup(values);
|
||||
let message = await res.json();
|
||||
if (res.status == 200) {
|
||||
//router.push(`/login`);
|
||||
setMessage('Your account was successfully created')
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
else if (res.status == 401 || res.status == 400 || res.status == 404 || res.status == 409) {
|
||||
setError(message.detail);
|
||||
setIsSubmitting(false);
|
||||
const session = useSession() as any;
|
||||
const [joinMethod, setJoinMethod] = React.useState('open');
|
||||
const [inviteCode, setInviteCode] = React.useState('');
|
||||
const searchParams = useSearchParams()
|
||||
const inviteCodeParam = searchParams.get('inviteCode')
|
||||
|
||||
}
|
||||
else {
|
||||
setError("Something went wrong");
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
useEffect(() => {
|
||||
if (props.org.config) {
|
||||
setJoinMethod(props.org?.config?.config?.GeneralConfig.users.signup_mechanism);
|
||||
console.log(props.org?.config?.config?.GeneralConfig.users.signup_mechanism)
|
||||
}
|
||||
if (inviteCodeParam) {
|
||||
setInviteCode(inviteCodeParam);
|
||||
}
|
||||
}
|
||||
, [props.org, inviteCodeParam]);
|
||||
|
||||
return (
|
||||
<div><div className='grid grid-flow-col justify-stretch h-screen'>
|
||||
<div className='grid grid-flow-col justify-stretch h-screen'>
|
||||
<div className="right-login-part" style={{ background: "linear-gradient(041.61deg, #202020 7.15%, #000000 90.96%)" }} >
|
||||
<div className='login-topbar m-10'>
|
||||
<Link prefetch href={getUriWithOrg(props.org.slug, "/")}>
|
||||
<Image quality={100} width={30} height={30} src={learnhouseIcon} alt="" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="ml-10 h-4/6 flex flex-row text-white">
|
||||
<div className="ml-10 h-3/4 flex flex-row text-white">
|
||||
<div className="m-auto flex space-x-4 items-center flex-wrap">
|
||||
<div>Join </div>
|
||||
<div>You've been invited to join </div>
|
||||
<div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]" >
|
||||
{props.org?.logo ? (
|
||||
{props.org?.logo_image ? (
|
||||
<img
|
||||
src={`${getOrgLogoMediaDirectory(props.org.org_id, props.org?.logo)}`}
|
||||
src={`${getOrgLogoMediaDirectory(props.org.org_uuid, props.org?.logo_image)}`}
|
||||
alt="Learnhouse"
|
||||
style={{ width: "auto", height: 70 }}
|
||||
className="rounded-md shadow-xl inset-0 ring-1 ring-inset ring-black/10 bg-white"
|
||||
className="rounded-xl shadow-xl inset-0 ring-1 ring-inset ring-black/10 bg-white"
|
||||
/>
|
||||
) : (
|
||||
<Image quality={100} width={70} height={70} src={learnhouseIcon} alt="" />
|
||||
|
|
@ -121,70 +66,113 @@ function SignUpClient(props: SignUpClientProps) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="left-login-part bg-white flex flex-row">
|
||||
<div className="login-form m-auto w-72">
|
||||
{error && (
|
||||
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
|
||||
<AlertTriangle size={18} />
|
||||
<div className="font-bold text-sm">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
{message && (
|
||||
<div className="flex flex-col space-y-4 justify-center bg-green-200 rounded-md text-green-950 space-x-2 items-center p-4 transition-all shadow-sm">
|
||||
<div className='flex space-x-2'>
|
||||
<Check size={18} />
|
||||
<div className="font-bold text-sm">{message}</div>
|
||||
</div>
|
||||
<hr className='border-green-900/20 800 w-40 border' />
|
||||
<Link className='flex space-x-2 items-center' href={'/login'}><User size={14} /> <div>Login </div></Link>
|
||||
</div>
|
||||
)}
|
||||
<FormLayout onSubmit={formik.handleSubmit}>
|
||||
<FormField name="email">
|
||||
<FormLabelAndMessage label='Email' message={formik.errors.email} />
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
{/* for password */}
|
||||
<FormField name="password">
|
||||
<FormLabelAndMessage label='Password' message={formik.errors.password} />
|
||||
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.password} type="password" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
{/* for username */}
|
||||
<FormField name="username">
|
||||
<FormLabelAndMessage label='Username' message={formik.errors.username} />
|
||||
|
||||
<Form.Control asChild>
|
||||
<Input onChange={formik.handleChange} value={formik.values.username} type="text" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
{/* for bio */}
|
||||
<FormField name="bio">
|
||||
<FormLabelAndMessage label='Bio' message={formik.errors.bio} />
|
||||
|
||||
<Form.Control asChild>
|
||||
<Textarea onChange={formik.handleChange} value={formik.values.bio} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<div className="flex py-4">
|
||||
<Form.Submit asChild>
|
||||
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer" >
|
||||
{isSubmitting ? "Loading..."
|
||||
: "Create an account"}
|
||||
</button>
|
||||
</Form.Submit>
|
||||
</div>
|
||||
|
||||
</FormLayout>
|
||||
</div>
|
||||
<div className="left-join-part bg-white flex flex-row">
|
||||
{joinMethod == 'open' && (
|
||||
session.isAuthenticated ? <LoggedInJoinScreen inviteCode={inviteCode} /> : <OpenSignUpComponent />
|
||||
)}
|
||||
{joinMethod == 'inviteOnly' && (
|
||||
inviteCode ? (
|
||||
session.isAuthenticated ? <LoggedInJoinScreen /> : <InviteOnlySignUpComponent inviteCode={inviteCode} />
|
||||
) : <NoTokenScreen />
|
||||
)}
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const LoggedInJoinScreen = (props: any) => {
|
||||
const session = useSession() as any;
|
||||
const org = useOrg() as any;
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (session && org) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
}
|
||||
, [org, session]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center mx-auto">
|
||||
<div className="flex space-y-7 flex-col justify-center items-center">
|
||||
<p className='pt-3 text-2xl font-semibold text-black/70 flex justify-center space-x-2 items-center'>
|
||||
<span className='items-center'>Hi</span>
|
||||
<span className='capitalize flex space-x-2 items-center'>
|
||||
<UserAvatar rounded='rounded-xl' border='border-4' width={35} />
|
||||
<span>{session.user.username},</span>
|
||||
</span>
|
||||
<span>join {org?.name} ?</span>
|
||||
</p>
|
||||
<button 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">
|
||||
<UserPlus size={18} />
|
||||
<p>Join </p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
const NoTokenScreen = (props: any) => {
|
||||
const session = useSession() as any;
|
||||
const org = useOrg() as any;
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [inviteCode, setInviteCode] = React.useState('');
|
||||
const [messsage, setMessage] = React.useState('bruh');
|
||||
|
||||
const handleInviteCodeChange = (e: any) => {
|
||||
setInviteCode(e.target.value);
|
||||
}
|
||||
|
||||
const validateCode = async () => {
|
||||
setIsLoading(true);
|
||||
let res = await validateInviteCode(org?.id, inviteCode);
|
||||
//wait for 1s
|
||||
if (res.success) {
|
||||
toast.success("Invite code is valid, you'll be redirected to the signup page in a few seconds");
|
||||
setTimeout(() => {
|
||||
router.push(`/signup?inviteCode=${inviteCode}`);
|
||||
}, 2000);
|
||||
}
|
||||
else {
|
||||
toast.error("Invite code is invalid");
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (session && org) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
}
|
||||
, [org, session]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center mx-auto">
|
||||
<Toast />
|
||||
{isLoading ? <div className="flex space-y-7 flex-col w-[300px] justify-center items-center"><PageLoading /></div> : <div className="flex space-y-7 flex-col justify-center items-center">
|
||||
|
||||
<p className="flex space-x-2 text-lg font-medium text-red-800 items-center">
|
||||
<MailWarning size={18} />
|
||||
<span>An invite code is required to join {org?.name}</span>
|
||||
</p>
|
||||
<input onChange={handleInviteCodeChange} className="bg-white outline-2 outline outline-gray-200 rounded-lg px-5 w-[300px] h-[50px]" placeholder="Please enter an invite code" type="text" />
|
||||
<button 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">
|
||||
<Shield size={18} />
|
||||
<p>Submit </p>
|
||||
</button>
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
"use client";
|
||||
import type { NextPage } from "next";
|
||||
import { motion } from "framer-motion";
|
||||
import styled from "styled-components";
|
||||
import learnhouseBigIcon from "public/learnhouse_bigicon.png";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import PageLoading from '@components/Objects/Loaders/PageLoading';
|
|||
import { getAPIUrl } from '@services/config/config';
|
||||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
import React, { createContext, useContext, useEffect, useReducer } from 'react'
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export const CourseContext = createContext(null) as any;
|
||||
export const CourseDispatchContext = createContext(null) as any;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
'use client';
|
||||
import { getNewAccessTokenUsingRefreshToken, getUserSession } from '@services/auth/auth';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import React, { useContext, createContext, useEffect } from 'react'
|
||||
import { useOrg } from './OrgContext';
|
||||
|
||||
export const SessionContext = createContext({}) as any;
|
||||
|
||||
const PATHS_THAT_REQUIRE_AUTH = ['/dash'];
|
||||
|
||||
type Session = {
|
||||
access_token: string;
|
||||
user: any;
|
||||
|
|
@ -18,10 +14,6 @@ type Session = {
|
|||
|
||||
function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||
const [session, setSession] = React.useState<Session>({ access_token: "", user: {}, roles: {}, isLoading: true, isAuthenticated: false });
|
||||
const org = useOrg() as any;
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
|
||||
|
||||
async function getNewAccessTokenUsingRefreshTokenUI() {
|
||||
let data = await getNewAccessTokenUsingRefreshToken();
|
||||
|
|
@ -39,6 +31,10 @@ function SessionProvider({ children }: { children: React.ReactNode }) {
|
|||
// Set session
|
||||
setSession({ access_token: access_token, user: user_session.user, roles: user_session.roles, isLoading: false, isAuthenticated: true });
|
||||
}
|
||||
|
||||
if (!access_token) {
|
||||
setSession({ access_token: "", user: {}, roles: {}, isLoading: false, isAuthenticated: false });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -47,8 +43,6 @@ function SessionProvider({ children }: { children: React.ReactNode }) {
|
|||
// Check session
|
||||
checkSession();
|
||||
|
||||
|
||||
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import * as Switch from '@radix-ui/react-switch';
|
|||
import * as Form from '@radix-ui/react-form';
|
||||
import React from 'react'
|
||||
import { useCourse, useCourseDispatch } from '../../../Contexts/CourseContext';
|
||||
import ThumbnailUpdate from './ThumbnailUpdate';
|
||||
|
||||
|
||||
type EditCourseStructureProps = {
|
||||
|
|
@ -84,71 +85,80 @@ function EditCourseGeneral(props: EditCourseStructureProps) {
|
|||
}, [course, formik.values, formik.initialValues]);
|
||||
|
||||
return (
|
||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
|
||||
<div> <div className="h-6"></div>
|
||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
|
||||
|
||||
{course.courseStructure && (
|
||||
<div className="editcourse-form">
|
||||
{error && (
|
||||
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
|
||||
<AlertTriangle size={18} />
|
||||
<div className="font-bold text-sm">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
<FormLayout onSubmit={formik.handleSubmit}>
|
||||
<FormField name="name">
|
||||
<FormLabelAndMessage label='Name' message={formik.errors.name} />
|
||||
<Form.Control asChild>
|
||||
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.name} type="text" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="description">
|
||||
<FormLabelAndMessage label='Description' message={formik.errors.description} />
|
||||
<Form.Control asChild>
|
||||
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.description} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="about">
|
||||
<FormLabelAndMessage label='About' message={formik.errors.about} />
|
||||
<Form.Control asChild>
|
||||
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.about} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="learnings">
|
||||
<FormLabelAndMessage label='Learnings' message={formik.errors.learnings} />
|
||||
<Form.Control asChild>
|
||||
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.learnings} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="tags">
|
||||
<FormLabelAndMessage label='Tags' message={formik.errors.tags} />
|
||||
<Form.Control asChild>
|
||||
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.tags} required />
|
||||
</Form.Control>
|
||||
</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>
|
||||
{course.courseStructure && (
|
||||
<div className="editcourse-form">
|
||||
{error && (
|
||||
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
|
||||
<AlertTriangle size={18} />
|
||||
<div className="font-bold text-sm">{error}</div>
|
||||
</div>
|
||||
</FormField>
|
||||
)}
|
||||
<FormLayout onSubmit={formik.handleSubmit}>
|
||||
<FormField name="name">
|
||||
<FormLabelAndMessage label='Name' message={formik.errors.name} />
|
||||
<Form.Control asChild>
|
||||
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.name} type="text" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
</FormLayout>
|
||||
</div>
|
||||
)}
|
||||
<FormField name="description">
|
||||
<FormLabelAndMessage label='Description' message={formik.errors.description} />
|
||||
<Form.Control asChild>
|
||||
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.description} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="about">
|
||||
<FormLabelAndMessage label='About' message={formik.errors.about} />
|
||||
<Form.Control asChild>
|
||||
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.about} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="learnings">
|
||||
<FormLabelAndMessage label='Learnings' message={formik.errors.learnings} />
|
||||
<Form.Control asChild>
|
||||
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.learnings} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="tags">
|
||||
<FormLabelAndMessage label='Tags' message={formik.errors.tags} />
|
||||
<Form.Control asChild>
|
||||
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.tags} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<FormField name="thumbnail">
|
||||
<FormLabelAndMessage label='Thumbnail' />
|
||||
<Form.Control asChild>
|
||||
<ThumbnailUpdate />
|
||||
</Form.Control>
|
||||
</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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
import { useCourse } from '@components/Contexts/CourseContext';
|
||||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import { updateCourseThumbnail } from '@services/courses/courses';
|
||||
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
|
||||
import { ArrowBigUpDash, UploadCloud } from 'lucide-react';
|
||||
import React from 'react'
|
||||
import { mutate } from 'swr';
|
||||
|
||||
function ThumbnailUpdate() {
|
||||
const course = useCourse() as any;
|
||||
const org = useOrg() as any;
|
||||
const [localThumbnail, setLocalThumbnail] = React.useState(null) as any;
|
||||
const [isLoading, setIsLoading] = React.useState(false) as any;
|
||||
const [error, setError] = React.useState('') as any;
|
||||
|
||||
|
||||
const handleFileChange = async (event: any) => {
|
||||
const file = event.target.files[0];
|
||||
setLocalThumbnail(file);
|
||||
setIsLoading(true);
|
||||
const res = await updateCourseThumbnail(course.courseStructure.course_uuid, file)
|
||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
||||
// wait for 1 second to show loading animation
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
if (res.success === false) {
|
||||
setError(res.HTTPmessage);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
setError('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow'>
|
||||
<div className='flex flex-col justify-center items-center h-full'>
|
||||
<div className='flex flex-col justify-center items-center'>
|
||||
<div className='flex flex-col justify-center items-center'>
|
||||
{error && (
|
||||
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-2 transition-all shadow-sm">
|
||||
<div className="text-sm font-semibold">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
{localThumbnail ? (
|
||||
<img src={URL.createObjectURL(localThumbnail)} className={`${isLoading ? 'animate-pulse' : ''} shadow w-[200px] h-[100px] rounded-md`} />
|
||||
|
||||
) : (
|
||||
<img src={`${getCourseThumbnailMediaDirectory(org?.org_uuid, course.courseStructure.course_uuid, course.courseStructure.thumbnail_image)}`} className='shadow w-[200px] h-[100px] rounded-md' />
|
||||
)}
|
||||
|
||||
</div>
|
||||
{isLoading ? (<div className='flex justify-center items-center'>
|
||||
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
|
||||
<div
|
||||
className='font-bold animate-pulse antialiased items-center bg-green-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex'
|
||||
>
|
||||
<ArrowBigUpDash size={16} className='mr-2' />
|
||||
<span>Uploading</span>
|
||||
</div>
|
||||
</div>) : (
|
||||
|
||||
<div className='flex justify-center items-center'>
|
||||
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
|
||||
<button
|
||||
className='font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex'
|
||||
onClick={() => document.getElementById('fileInput')?.click()}
|
||||
>
|
||||
<UploadCloud size={16} className='mr-2' />
|
||||
<span>Change Thumbnail</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThumbnailUpdate
|
||||
|
|
@ -5,9 +5,9 @@ import { getAPIUrl } from '@services/config/config';
|
|||
import { createActivity, createExternalVideoActivity, createFileActivity } from '@services/courses/activities';
|
||||
import { getOrganizationContextInfoWithoutCredentials } from '@services/organizations/orgs';
|
||||
import { revalidateTags } from '@services/utils/ts/requests';
|
||||
import { Layers, Sparkles } from 'lucide-react'
|
||||
import { Layers } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { use, useEffect } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { mutate } from 'swr';
|
||||
|
||||
type NewActivityButtonProps = {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
|
||||
import { Activity, Hexagon, MoreHorizontal, MoreVertical, Pencil, Save, Sparkles, X } from 'lucide-react';
|
||||
import { Hexagon, MoreHorizontal, MoreVertical, Pencil, Save, X } from 'lucide-react';
|
||||
import React from 'react'
|
||||
import ActivitiyElement from './ActivityElement';
|
||||
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import ActivityElement from './ActivityElement';
|
||||
import NewActivity from '../Buttons/NewActivityButton';
|
||||
import NewActivityButton from '../Buttons/NewActivityButton';
|
||||
import { deleteChapter, updateChapter } from '@services/courses/chapters';
|
||||
import { revalidateTags } from '@services/utils/ts/requests';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
'use client';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import { revalidateTags, swrFetcher } from '@services/utils/ts/requests';
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import { revalidateTags } from '@services/utils/ts/requests';
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
import { mutate } from 'swr';
|
||||
import ChapterElement from './DraggableElements/ChapterElement';
|
||||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
||||
import { createChapter, updateCourseOrderStructure } from '@services/courses/chapters';
|
||||
import { createChapter } from '@services/courses/chapters';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext';
|
||||
import { Hexagon } from 'lucide-react';
|
||||
|
|
@ -92,6 +92,7 @@ const EditCourseStructure = (props: EditCourseStructureProps) => {
|
|||
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
<div className="h-6"></div>
|
||||
{winReady ?
|
||||
<DragDropContext onDragEnd={updateStructure}>
|
||||
<Droppable type='chapter' droppableId='chapters'>
|
||||
|
|
@ -129,7 +130,7 @@ const EditCourseStructure = (props: EditCourseStructureProps) => {
|
|||
dialogTitle="Create chapter"
|
||||
dialogDescription="Add a new chapter to the course"
|
||||
dialogTrigger={
|
||||
<div className="mt-4 w-44 max-w-screen-2xl mx-auto bg-cyan-800 text-white rounded-xl shadow-sm px-6 items-center flex flex-row h-10">
|
||||
<div className="w-44 my-16 py-5 max-w-screen-2xl mx-auto bg-cyan-800 text-white rounded-xl shadow-sm px-6 items-center flex flex-row h-10">
|
||||
<div className='mx-auto flex space-x-2 items-center hover:cursor-pointer'>
|
||||
<Hexagon strokeWidth={3} size={16} className="text-white text-sm " />
|
||||
<div className='font-bold text-sm'>Add Chapter</div></div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"use client";
|
||||
import React, { use, useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { updateOrganization, uploadOrganizationLogo } from '@services/settings/org';
|
||||
import { UploadCloud } from 'lucide-react';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { useCourse } from '@components/Contexts/CourseContext'
|
||||
import { Book, ChevronRight, School, User } from 'lucide-react'
|
||||
import { Book, ChevronRight, School, User, Users } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import React, { use, useEffect } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
type BreadCrumbsProps = {
|
||||
type: 'courses' | 'user' | 'users' | 'org'
|
||||
type: 'courses' | 'user' | 'users' | 'org' | 'orgusers'
|
||||
last_breadcrumb?: string
|
||||
}
|
||||
|
||||
|
|
@ -17,7 +17,9 @@ function BreadCrumbs(props: BreadCrumbsProps) {
|
|||
<div className='text-gray-400 tracking-tight font-medium text-sm flex space-x-1'>
|
||||
<div className='flex items-center space-x-1'>
|
||||
{props.type == 'courses' ? <div className='flex space-x-2 items-center'> <Book className='text-gray' size={14}></Book><Link href='/dash/courses'>Courses</Link></div> : ''}
|
||||
{props.type == 'user' ? <div className='flex space-x-2 items-center'> <User className='text-gray' size={14}></User><Link href='/dash/user/settings/general'>Account Settings</Link></div> : ''}
|
||||
{props.type == 'user' ? <div className='flex space-x-2 items-center'> <User className='text-gray' size={14}></User><Link href='/dash/user-account/settings/general'>Account Settings</Link></div> : ''}
|
||||
{props.type == 'orgusers' ? <div className='flex space-x-2 items-center'> <Users className='text-gray' size={14}></Users><Link href='/dash/users/settings/users'>Organization users</Link></div> : ''}
|
||||
|
||||
{props.type == 'org' ? <div className='flex space-x-2 items-center'> <School className='text-gray' size={14}></School><Link href='/dash/users'>Organization Settings</Link></div> : ''}
|
||||
<div className='flex items-center space-x-1 first-letter:uppercase'>
|
||||
{props.last_breadcrumb ? <ChevronRight size={17} /> : ''}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { getUriWithOrg } from "@services/config/config";
|
|||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import EmptyThumbnailImage from '../../../public/empty_thumbnail.png';
|
||||
|
||||
export function CourseOverviewTop({ params }: { params: CourseOverviewParams }) {
|
||||
const course = useCourse() as any;
|
||||
|
|
@ -21,7 +23,10 @@ export function CourseOverviewTop({ params }: { params: CourseOverviewParams })
|
|||
<div className='flex'>
|
||||
<div className='flex py-5 grow items-center'>
|
||||
<Link href={getUriWithOrg(org?.slug, "") + `/course/${params.courseuuid}`}>
|
||||
{course?.courseStructure?.thumbnail_image ?
|
||||
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(org?.org_uuid, "course_" + params.courseuuid, course.courseStructure.thumbnail_image)}`} alt="" />
|
||||
:
|
||||
<Image width={100} className="h-[57px] rounded-md drop-shadow-md" src={EmptyThumbnailImage} alt="" />}
|
||||
</Link>
|
||||
<div className="flex flex-col course_metadata justify-center pl-5">
|
||||
<div className='text-gray-400 font-semibold text-sm'>Course</div>
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import { useSession } from '@components/Contexts/SessionContext';
|
|||
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
|
||||
import LearnHouseDashboardLogo from '@public/dashLogo.png';
|
||||
import { logout } from '@services/auth/auth';
|
||||
import Avvvatars from 'avvvatars-react';
|
||||
import { ArrowLeft, Book, BookCopy, Home, LogOut, School, Settings } from 'lucide-react'
|
||||
import { BookCopy, Home, LogOut, School, Settings, Users } from 'lucide-react'
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { use, useEffect } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import UserAvatar from '../../Objects/UserAvatar';
|
||||
import AdminAuthorization from '@components/Security/AdminAuthorization';
|
||||
|
||||
function LeftMenu() {
|
||||
const org = useOrg() as any;
|
||||
|
|
@ -42,8 +43,8 @@ function LeftMenu() {
|
|||
|
||||
return (
|
||||
<div
|
||||
style={{ background: "linear-gradient(0deg, rgba(0, 0, 0, 0.20) 0%, rgba(0, 0, 0, 0.20) 100%), radial-gradient(271.56% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0.00) 100%), #2E2D2D" }}
|
||||
className='flex flex-col w-28 bg-black h-screen text-white shadow-xl'>
|
||||
style={{ background: "linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(271.56% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(20 19 19)" }}
|
||||
className='flex flex-col w-[90px] bg-black h-screen text-white shadow-xl'>
|
||||
<div className='flex flex-col h-full'>
|
||||
<div className='flex h-20 mt-6'>
|
||||
<Link className='flex flex-col items-center mx-auto space-y-3' href={"/"}>
|
||||
|
|
@ -59,27 +60,32 @@ function LeftMenu() {
|
|||
{/* <ToolTip content={"Back to " + org?.name + "'s Home"} slateBlack sideOffset={8} side='right' >
|
||||
<Link className='bg-white text-black hover:text-white rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/`} ><ArrowLeft className='hover:text-white' size={18} /></Link>
|
||||
</ToolTip> */}
|
||||
<ToolTip content={"Home"} slateBlack sideOffset={8} side='right' >
|
||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash`} ><Home size={18} /></Link>
|
||||
</ToolTip>
|
||||
<ToolTip content={"Courses"} slateBlack sideOffset={8} side='right' >
|
||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/courses`} ><BookCopy size={18} /></Link>
|
||||
</ToolTip>
|
||||
<ToolTip content={"Organization"} slateBlack sideOffset={8} side='right' >
|
||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/org/settings/general`} ><School size={18} /></Link>
|
||||
</ToolTip>
|
||||
<AdminAuthorization authorizationMode="component">
|
||||
<ToolTip content={"Home"} slateBlack sideOffset={8} side='right' >
|
||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash`} ><Home size={18} /></Link>
|
||||
</ToolTip>
|
||||
<ToolTip content={"Courses"} slateBlack sideOffset={8} side='right' >
|
||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/courses`} ><BookCopy size={18} /></Link>
|
||||
</ToolTip>
|
||||
<ToolTip content={"Users"} slateBlack sideOffset={8} side='right' >
|
||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/users/settings/users`} ><Users size={18} /></Link>
|
||||
</ToolTip>
|
||||
<ToolTip content={"Organization"} slateBlack sideOffset={8} side='right' >
|
||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/org/settings/general`} ><School size={18} /></Link>
|
||||
</ToolTip>
|
||||
</AdminAuthorization>
|
||||
</div>
|
||||
<div className='flex flex-col mx-auto pb-7 space-y-2'>
|
||||
|
||||
<div className="flex items-center flex-col space-y-2">
|
||||
<ToolTip content={session.user.username} slateBlack sideOffset={8} side='right' >
|
||||
<div className="mx-auto shadow-lg">
|
||||
<Avvvatars radius={3} border borderColor='white' borderSize={3} size={35} value={session.user.user_uuid} style="shape" />
|
||||
<ToolTip content={'@' + session.user.username} slateBlack sideOffset={8} side='right' >
|
||||
<div className='mx-auto'>
|
||||
<UserAvatar border='border-4' width={35} />
|
||||
</div>
|
||||
</ToolTip>
|
||||
<div className='flex items-center flex-col space-y-1'>
|
||||
<ToolTip content={session.user.username + "'s Settings"} slateBlack sideOffset={8} side='right' >
|
||||
<Link href={'/dash/user/settings/general'} className='py-3'>
|
||||
<Link href={'/dash/user-account/settings/general'} className='py-3'>
|
||||
<Settings className='mx-auto text-neutral-400 cursor-pointer' size={18} />
|
||||
</Link>
|
||||
</ToolTip>
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
import { updateProfile } from '@services/settings/profile';
|
||||
import React, { useEffect } from 'react'
|
||||
import { Formik, Form, Field, ErrorMessage } from 'formik';
|
||||
import { useSession } from '@components/Contexts/SessionContext';
|
||||
|
||||
function UserEditGeneral() {
|
||||
const session = useSession() as any;
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
}
|
||||
, [session, session.user])
|
||||
|
||||
return (
|
||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
|
||||
{session.user && (
|
||||
<Formik
|
||||
enableReinitialize
|
||||
initialValues={{
|
||||
username: session.user.username,
|
||||
first_name: session.user.first_name,
|
||||
last_name: session.user.last_name,
|
||||
email: session.user.email,
|
||||
bio: session.user.bio,
|
||||
}}
|
||||
onSubmit={(values, { setSubmitting }) => {
|
||||
setTimeout(() => {
|
||||
|
||||
setSubmitting(false);
|
||||
updateProfile(values,session.user.id)
|
||||
}, 400);
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form className="max-w-md">
|
||||
<label className="block mb-2 font-bold" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
<Field
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
type="email"
|
||||
name="email"
|
||||
/>
|
||||
|
||||
<label className="block mb-2 font-bold" htmlFor="username">
|
||||
Username
|
||||
</label>
|
||||
<Field
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
type="username"
|
||||
name="username"
|
||||
/>
|
||||
|
||||
<label className="block mb-2 font-bold" htmlFor="first_name">
|
||||
First Name
|
||||
</label>
|
||||
|
||||
<Field
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
type="first_name"
|
||||
name="first_name"
|
||||
/>
|
||||
|
||||
<label className="block mb-2 font-bold" htmlFor="last_name">
|
||||
Last Name
|
||||
</label>
|
||||
|
||||
<Field
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
type="last_name"
|
||||
name="last_name"
|
||||
/>
|
||||
|
||||
<label className="block mb-2 font-bold" htmlFor="bio">
|
||||
Bio
|
||||
</label>
|
||||
|
||||
<Field
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
type="bio"
|
||||
name="bio"
|
||||
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="px-6 py-3 text-white bg-black rounded-lg shadow-md hover:bg-black focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</Form>
|
||||
|
||||
)}
|
||||
</Formik>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserEditGeneral
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
import { updateProfile } from '@services/settings/profile';
|
||||
import React, { useEffect } from 'react'
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import { useSession } from '@components/Contexts/SessionContext';
|
||||
import { ArrowBigUpDash, Check, FileWarning, Info, UploadCloud } from 'lucide-react';
|
||||
import UserAvatar from '@components/Objects/UserAvatar';
|
||||
import { updateUserAvatar } from '@services/users/users';
|
||||
|
||||
function UserEditGeneral() {
|
||||
const session = useSession() as any;
|
||||
const [localAvatar, setLocalAvatar] = React.useState(null) as any;
|
||||
const [isLoading, setIsLoading] = React.useState(false) as any;
|
||||
const [error, setError] = React.useState() as any;
|
||||
const [success, setSuccess] = React.useState('') as any;
|
||||
|
||||
const handleFileChange = async (event: any) => {
|
||||
const file = event.target.files[0];
|
||||
setLocalAvatar(file);
|
||||
setIsLoading(true);
|
||||
const res = await updateUserAvatar(session.user.user_uuid, file)
|
||||
// wait for 1 second to show loading animation
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
if (res.success === false) {
|
||||
setError(res.HTTPmessage);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
setError('');
|
||||
setSuccess('Avatar Updated');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
}
|
||||
, [session, session.user])
|
||||
|
||||
return (
|
||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
|
||||
{session.user && (
|
||||
<Formik
|
||||
enableReinitialize
|
||||
initialValues={{
|
||||
username: session.user.username,
|
||||
first_name: session.user.first_name,
|
||||
last_name: session.user.last_name,
|
||||
email: session.user.email,
|
||||
bio: session.user.bio,
|
||||
}}
|
||||
onSubmit={(values, { setSubmitting }) => {
|
||||
setTimeout(() => {
|
||||
|
||||
setSubmitting(false);
|
||||
updateProfile(values, session.user.id)
|
||||
}, 400);
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<div className='flex space-x-8'>
|
||||
|
||||
<Form className="max-w-md">
|
||||
|
||||
<label className="block mb-2 font-bold" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
<Field
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
type="email"
|
||||
name="email"
|
||||
/>
|
||||
|
||||
<label className="block mb-2 font-bold" htmlFor="username">
|
||||
Username
|
||||
</label>
|
||||
<Field
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
type="username"
|
||||
name="username"
|
||||
/>
|
||||
|
||||
<label className="block mb-2 font-bold" htmlFor="first_name">
|
||||
First Name
|
||||
</label>
|
||||
|
||||
<Field
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
type="first_name"
|
||||
name="first_name"
|
||||
/>
|
||||
|
||||
<label className="block mb-2 font-bold" htmlFor="last_name">
|
||||
Last Name
|
||||
</label>
|
||||
|
||||
<Field
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
type="last_name"
|
||||
name="last_name"
|
||||
/>
|
||||
|
||||
<label className="block mb-2 font-bold" htmlFor="bio">
|
||||
Bio
|
||||
</label>
|
||||
|
||||
<Field
|
||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
type="bio"
|
||||
name="bio"
|
||||
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="px-6 py-3 text-white bg-black rounded-lg shadow-md hover:bg-black focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</Form>
|
||||
<div className='flex flex-col grow justify-center align-middle space-y-3'>
|
||||
<label className="flex mx-auto mb-2 font-bold " >
|
||||
Avatar
|
||||
</label>
|
||||
{error && (
|
||||
<div className="flex justify-center mx-auto bg-red-200 rounded-md text-red-950 space-x-1 px-4 items-center p-2 transition-all shadow-sm">
|
||||
<FileWarning size={16} className='mr-2' />
|
||||
<div className="text-sm font-semibold first-letter:uppercase">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="flex justify-center mx-auto bg-green-200 rounded-md text-green-950 space-x-1 px-4 items-center p-2 transition-all shadow-sm">
|
||||
<Check size={16} className='mr-2' />
|
||||
<div className="text-sm font-semibold first-letter:uppercase">{success}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col space-y-3">
|
||||
|
||||
<div className='w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow mx-20'>
|
||||
|
||||
<div className='flex flex-col justify-center items-center mt-10'>
|
||||
|
||||
{localAvatar ? (
|
||||
<UserAvatar border='border-8' width={100} avatar_url={URL.createObjectURL(localAvatar)} />
|
||||
|
||||
) : (
|
||||
<UserAvatar border='border-8' width={100} />
|
||||
)}
|
||||
</div>
|
||||
{isLoading ? (<div className='flex justify-center items-center'>
|
||||
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
|
||||
<div
|
||||
className='font-bold animate-pulse antialiased items-center bg-green-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex'
|
||||
>
|
||||
<ArrowBigUpDash size={16} className='mr-2' />
|
||||
<span>Uploading</span>
|
||||
</div>
|
||||
</div>) : (
|
||||
<div className='flex justify-center items-center'>
|
||||
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
|
||||
<button
|
||||
className='font-bold antialiased items-center text-gray text-sm rounded-md px-4 py-2 mt-4 flex'
|
||||
onClick={() => document.getElementById('fileInput')?.click()}
|
||||
>
|
||||
<UploadCloud size={16} className='mr-2' />
|
||||
<span>Change Thumbnail</span>
|
||||
</button>
|
||||
</div> )}
|
||||
</div>
|
||||
<div className='flex text-xs space-x-2 items-center text-gray-500 justify-center'>
|
||||
<Info size={13} /><p>Recommended size 100x100</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
)}
|
||||
</Formik>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserEditGeneral
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { useSession } from '@components/Contexts/SessionContext';
|
||||
import { updatePassword } from '@services/settings/password';
|
||||
import { Formik, Form, Field, ErrorMessage } from 'formik';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
function UserEditPassword() {
|
||||
175
apps/web/components/Dashboard/Users/OrgAccess/OrgAccess.tsx
Normal file
175
apps/web/components/Dashboard/Users/OrgAccess/OrgAccess.tsx
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
|
||||
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
|
||||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
import { Globe, Shield, X } from 'lucide-react'
|
||||
import Link from 'next/link';
|
||||
import React, { useEffect } from 'react'
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import dayjs from 'dayjs';
|
||||
import { changeSignupMechanism, createInviteCode, deleteInviteCode } from '@services/organizations/invites';
|
||||
import Toast from '@components/StyledElements/Toast/Toast';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
function OrgAccess() {
|
||||
const org = useOrg() as any;
|
||||
const { data: invites } = useSWR(org ? `${getAPIUrl()}orgs/${org?.id}/invites` : null, swrFetcher);
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
const [joinMethod, setJoinMethod] = React.useState('closed')
|
||||
const router = useRouter()
|
||||
|
||||
async function getOrgJoinMethod() {
|
||||
if (org) {
|
||||
if (org.config.config.GeneralConfig.users.signup_mechanism == 'open') {
|
||||
setJoinMethod('open')
|
||||
}
|
||||
else {
|
||||
setJoinMethod('inviteOnly')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
let res = await deleteInviteCode(org.id, invite.invite_code_uuid)
|
||||
if (res.status == 200) {
|
||||
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
|
||||
}
|
||||
else {
|
||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function changeJoinMethod(method: 'open' | 'inviteOnly') {
|
||||
let res = await changeSignupMechanism(org.id, method)
|
||||
if (res.status == 200) {
|
||||
router.refresh()
|
||||
mutate(`${getAPIUrl()}orgs/slug/${org?.slug}`)
|
||||
}
|
||||
else {
|
||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (invites && org) {
|
||||
getOrgJoinMethod()
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
, [org, invites])
|
||||
|
||||
return (
|
||||
|
||||
<>
|
||||
<Toast></Toast>
|
||||
{!isLoading ? (<>
|
||||
<div className="h-6"></div>
|
||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4 anit '>
|
||||
<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'>Join method</h1>
|
||||
<h2 className='text-gray-500 text-md'> Choose how users can join your organization </h2>
|
||||
</div>
|
||||
<div className='flex space-x-2 mx-auto'>
|
||||
<ConfirmationModal
|
||||
confirmationButtonText='Change to open '
|
||||
confirmationMessage='Are you sure you want to change the signup mechanism to open ? This will allow users to join your organization freely.'
|
||||
dialogTitle={'Change to open ?'}
|
||||
dialogTrigger={
|
||||
<div className='w-full h-[160px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all'>
|
||||
{joinMethod == 'open' ? <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'>Open</div>
|
||||
<div className='text-gray-400 text-center'>Users can join freely from the signup page</div>
|
||||
</div>
|
||||
</div>}
|
||||
functionToExecute={() => { changeJoinMethod('open') }}
|
||||
status='info'
|
||||
></ConfirmationModal>
|
||||
<ConfirmationModal
|
||||
confirmationButtonText='Change to closed '
|
||||
confirmationMessage='Are you sure you want to change the signup mechanism to closed ? This will allow users to join your organization only by invitation.'
|
||||
dialogTitle={'Change to closed ?'}
|
||||
dialogTrigger={
|
||||
<div className='w-full h-[160px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all'>
|
||||
{joinMethod == 'inviteOnly' ? <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'>
|
||||
<Shield className='text-slate-400' size={40}></Shield>
|
||||
<div className='text-2xl text-slate-700 font-bold'>Closed</div>
|
||||
<div className='text-gray-400 text-center'>Users can join only by invitation</div>
|
||||
</div>
|
||||
</div>}
|
||||
functionToExecute={() => { changeJoinMethod('inviteOnly') }}
|
||||
status='info'
|
||||
></ConfirmationModal>
|
||||
|
||||
</div>
|
||||
<div className={joinMethod == 'open' ? 'opacity-20 pointer-events-none' : 'pointer-events-auto'}>
|
||||
<div className='flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mt-3 mb-3 '>
|
||||
<h1 className='font-bold text-xl text-gray-800'>Invite codes</h1>
|
||||
<h2 className='text-gray-500 text-md'>Invite codes can be copied and used to join 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'>Code</th>
|
||||
<th className='py-3 px-4'>Signup link</th>
|
||||
<th className='py-3 px-4'>Expiration date</th>
|
||||
<th className='py-3 px-4'>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<>
|
||||
<tbody className='mt-5 bg-white rounded-md' >
|
||||
{invites?.map((invite: any) => (
|
||||
<tr key={invite.invite_code_uuid} className='border-b border-gray-100 text-sm'>
|
||||
<td className='py-3 px-4'>{invite.invite_code}</td>
|
||||
<td className='py-3 px-4 '>
|
||||
<Link className='outline bg-gray-50 text-gray-600 px-2 py-1 rounded-md outline-gray-300 outline-dashed outline-1' target='_blank' href={getUriWithOrg(org?.slug, `/signup?inviteCode=${invite.invite_code}`)}>
|
||||
{getUriWithOrg(org?.slug, `/signup?inviteCode=${invite.invite_code}`)}
|
||||
</Link>
|
||||
</td>
|
||||
<td className='py-3 px-4'>{dayjs(invite.expiration_date).add(1, 'year').format('DD/MM/YYYY')} </td>
|
||||
<td className='py-3 px-4'>
|
||||
<ConfirmationModal
|
||||
confirmationButtonText='Delete Code'
|
||||
confirmationMessage='Are you sure you want remove this invite code ?'
|
||||
dialogTitle={'Delete code ?'}
|
||||
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 code</span>
|
||||
</button>}
|
||||
functionToExecute={() => { deleteInvite(invite) }}
|
||||
status='warning'
|
||||
></ConfirmationModal>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</>
|
||||
</table>
|
||||
<button onClick={() => createInvite()} className='mt-3 mr-2 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'>
|
||||
<Shield className='w-4 h-4' />
|
||||
<span> Create invite code</span>
|
||||
</button>
|
||||
</div>
|
||||
</div></>) : <PageLoading />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrgAccess
|
||||
120
apps/web/components/Dashboard/Users/OrgUsers/OrgUsers.tsx
Normal file
120
apps/web/components/Dashboard/Users/OrgUsers/OrgUsers.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
||||
import RolesUpdate from '@components/Objects/Modals/Dash/OrgUsers/RolesUpdate';
|
||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
|
||||
import Modal from '@components/StyledElements/Modal/Modal';
|
||||
import Toast from '@components/StyledElements/Toast/Toast';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import { removeUserFromOrg } from '@services/organizations/orgs';
|
||||
import { swrFetcher } from '@services/utils/ts/requests';
|
||||
import { KeyRound, LogOut } from 'lucide-react';
|
||||
import React, { useEffect } from 'react'
|
||||
import toast from 'react-hot-toast';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
|
||||
function OrgUsers() {
|
||||
const org = useOrg() as any;
|
||||
const { data: orgUsers } = useSWR(org ? `${getAPIUrl()}orgs/${org?.id}/users` : null, swrFetcher);
|
||||
const [rolesModal, setRolesModal] = React.useState(false);
|
||||
const [selectedUser, setSelectedUser] = React.useState(null) as any;
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
|
||||
const handleRolesModal = (user_uuid: any) => {
|
||||
setSelectedUser(user_uuid);
|
||||
setRolesModal(!rolesModal);
|
||||
}
|
||||
|
||||
const handleRemoveUser = async (user_id: any) => {
|
||||
const res = await removeUserFromOrg(org.id, user_id);
|
||||
if (res.status === 200) {
|
||||
await mutate(`${getAPIUrl()}orgs/${org.id}/users`);
|
||||
}
|
||||
else {
|
||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (orgUsers) {
|
||||
setIsLoading(false)
|
||||
console.log(orgUsers)
|
||||
}
|
||||
}, [org, orgUsers])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isLoading ? <div><PageLoading /></div> :
|
||||
|
||||
<>
|
||||
<Toast></Toast>
|
||||
<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'>Active users</h1>
|
||||
<h2 className='text-gray-500 text-md'> Manage your organization users, assign roles and permissions </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'>User</th>
|
||||
<th className='py-3 px-4'>Role</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'>
|
||||
<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'>{user.role.name}</td>
|
||||
<td className='py-3 px-4 flex space-x-2 items-end'>
|
||||
<Modal
|
||||
isDialogOpen={rolesModal && selectedUser === user.user.user_uuid}
|
||||
onOpenChange={() => handleRolesModal(user.user.user_uuid)}
|
||||
minHeight="no-min"
|
||||
dialogContent={
|
||||
<RolesUpdate
|
||||
alreadyAssignedRole={user.role.role_uuid}
|
||||
setRolesModal={setRolesModal}
|
||||
user={user} />
|
||||
}
|
||||
dialogTitle="Update Role"
|
||||
dialogDescription={"Update @" + user.user.username + "'s role"}
|
||||
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'>
|
||||
<KeyRound className='w-4 h-4' />
|
||||
<span> Edit Role</span>
|
||||
</button>}
|
||||
/>
|
||||
|
||||
|
||||
<ConfirmationModal
|
||||
confirmationButtonText='Remove User'
|
||||
confirmationMessage='Are you sure you want remove this user from the organization?'
|
||||
dialogTitle={'Delete ' + user.user.username + ' ?'}
|
||||
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'>
|
||||
<LogOut className='w-4 h-4' />
|
||||
<span> Remove from organization</span>
|
||||
</button>}
|
||||
functionToExecute={() => { handleRemoveUser(user.user.id) }}
|
||||
status='warning'
|
||||
></ConfirmationModal>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
))}
|
||||
</tbody>
|
||||
</>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrgUsers
|
||||
|
|
@ -1,18 +1,15 @@
|
|||
import { useSession } from '@components/Contexts/SessionContext'
|
||||
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
|
||||
import { AlertTriangle, BadgeInfo, NotebookTabs } from 'lucide-react';
|
||||
import Avvvatars from 'avvvatars-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { FlaskConical, Keyboard, MessageCircle, MessageSquareIcon, Sparkle, Sparkles, X } from 'lucide-react'
|
||||
import { FlaskConical, MessageCircle, X } from 'lucide-react'
|
||||
import Image from 'next/image';
|
||||
import { send } from 'process';
|
||||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
||||
import learnhouseAI_logo_black from "public/learnhouse_ai_black_logo.png";
|
||||
import React, { use, useEffect, useRef } from 'react'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { AIChatBotStateTypes, useAIChatBot, useAIChatBotDispatch } from '@components/Contexts/AI/AIChatBotContext';
|
||||
import FeedbackModal from '@components/Objects/Modals/Feedback/Feedback';
|
||||
import Modal from '@components/StyledElements/Modal/Modal';
|
||||
import useGetAIFeatures from '../../../AI/Hooks/useGetAIFeatures';
|
||||
import UserAvatar from '@components/Objects/UserAvatar';
|
||||
|
||||
|
||||
type AIActivityAskProps = {
|
||||
|
|
@ -172,7 +169,7 @@ function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
|
|||
</div>
|
||||
<div className='bg-white/5 text-white/40 py-0.5 px-3 flex space-x-1 rounded-full items-center'>
|
||||
<FlaskConical size={14} />
|
||||
<span className='text-xs font-semibold '>Experimental</span>
|
||||
<span className='text-xs font-semibold antialiased '>Experimental</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -204,7 +201,7 @@ function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
|
|||
}
|
||||
<div className='flex space-x-2 items-center'>
|
||||
<div className=''>
|
||||
<Avvvatars radius={3} border borderColor='white' borderSize={3} size={35} value={session.user.user_uuid} style="shape" />
|
||||
<UserAvatar rounded='rounded-lg' border='border-2' width={35} />
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<input onKeyDown={handleKeyDown} onChange={handleChange} disabled={aiChatBotState.isWaitingForResponse} value={aiChatBotState.chatInputValue} placeholder='Ask AI About this Lecture' type="text" className={inputClass} name="" id="" />
|
||||
|
|
@ -235,7 +232,11 @@ function AIMessage(props: AIMessageProps) {
|
|||
return (
|
||||
<div className='flex space-x-2 w-full antialiased font-medium'>
|
||||
<div className=''>
|
||||
<Avvvatars radius={3} border borderColor='white' borderSize={3} size={35} value={props.message.type == 'ai' ? 'ai' : session.user.user_uuid} style="shape" />
|
||||
{props.message.sender == 'ai' ? (
|
||||
<UserAvatar rounded='rounded-lg' border='border-2' predefined_avatar='ai' width={35} />
|
||||
) : (
|
||||
<UserAvatar rounded='rounded-lg' border='border-2' width={35} />
|
||||
)}
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<p className='w-full rounded-lg outline-none px-2 py-1 text-white text-md placeholder:text-white/30' id="">
|
||||
|
|
@ -277,7 +278,8 @@ const AIMessagePlaceHolder = (props: { activity_uuid: string, sendMessage: any }
|
|||
<Image width={100} className='mx-auto' src={learnhouseAI_logo_black} alt="" />
|
||||
<p className='pt-3 text-2xl font-semibold text-white/70 flex justify-center space-x-2 items-center'>
|
||||
<span className='items-center'>Hello</span>
|
||||
<span className='capitalize flex space-x-2 items-center'> <Avvvatars radius={3} border borderColor='white' borderSize={3} size={25} value={session.user.user_uuid} style="shape" />
|
||||
<span className='capitalize flex space-x-2 items-center'>
|
||||
<UserAvatar rounded='rounded-lg' border='border-2' width={35} />
|
||||
<span>{session.user.username},</span>
|
||||
</span>
|
||||
<span>how can we help today ?</span>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
import { getBackendUrl } from "@services/config/config";
|
||||
import { getActivityMediaDirectory } from "@services/media/media";
|
||||
import React from "react";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEditor, EditorContent, BubbleMenu, EditorProvider } from "@tiptap/react";
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import styled from "styled-components"
|
||||
import Youtube from "@tiptap/extension-youtube";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import { getBackendUrl } from "@services/config/config";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import YouTube from 'react-youtube';
|
||||
import { getActivityMediaDirectory } from "@services/media/media";
|
||||
import { useOrg } from "@components/Contexts/OrgContext";
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ import React from 'react'
|
|||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import Image from 'next/image';
|
||||
import { AlertTriangle, BetweenHorizontalStart, FastForward, Feather, FileStack, HelpCircle, Languages, MessageCircle, MoreVertical, Pen, X } from 'lucide-react';
|
||||
import { AlertTriangle, BetweenHorizontalStart, FastForward, Feather, FileStack, HelpCircle, Languages, MoreVertical, X } from 'lucide-react';
|
||||
import { Editor } from '@tiptap/react';
|
||||
import { AIChatBotStateTypes, useAIChatBot, useAIChatBotDispatch } from '@components/Contexts/AI/AIChatBotContext';
|
||||
import { AIEditorStateTypes, useAIEditor, useAIEditorDispatch } from '@components/Contexts/AI/AIEditorContext';
|
||||
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
|
||||
import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures';
|
||||
|
|
@ -57,7 +56,7 @@ function AIEditorToolkit(props: AIEditorToolkitProps) {
|
|||
<div className='pr-1'>
|
||||
<div className='flex w-full space-x-2 font-bold text-white/80 items-center'>
|
||||
<Image className='outline outline-1 outline-neutral-200/20 rounded-lg' width={24} src={learnhouseAI_icon} alt="" />
|
||||
<div >AI Editor</div>
|
||||
<div className='flex items-center'>AI Editor <span className='text-[10px] px-2 py-1 rounded-3xl ml-3 bg-white/10 uppercase'>PRE-ALPHA</span></div>
|
||||
<MoreVertical className='text-white/50' size={12} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
import React from "react";
|
||||
import { useEditor, EditorContent, BubbleMenu } from "@tiptap/react";
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import learnhouseIcon from "public/learnhouse_icon.png";
|
||||
import { ToolbarButtons } from "./Toolbar/ToolbarButtons";
|
||||
|
|
@ -8,7 +8,6 @@ import { motion } from "framer-motion";
|
|||
import Image from "next/image";
|
||||
import styled from "styled-components";
|
||||
import { DividerVerticalIcon, SlashIcon } from "@radix-ui/react-icons";
|
||||
import Avvvatars from "avvvatars-react";
|
||||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
||||
import { AIEditorStateTypes, useAIEditor, useAIEditorDispatch } from "@components/Contexts/AI/AIEditorContext";
|
||||
|
||||
|
|
@ -42,6 +41,7 @@ import { CourseProvider } from "@components/Contexts/CourseContext";
|
|||
import { useSession } from "@components/Contexts/SessionContext";
|
||||
import AIEditorToolkit from "./AI/AIEditorToolkit";
|
||||
import useGetAIFeatures from "@components/AI/Hooks/useGetAIFeatures";
|
||||
import UserAvatar from "../UserAvatar";
|
||||
|
||||
|
||||
interface Editor {
|
||||
|
|
@ -163,7 +163,7 @@ function Editor(props: Editor) {
|
|||
<Link href="/">
|
||||
<EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" />
|
||||
</Link>
|
||||
<Link target="_blank" href={`/course/${course_uuid}/edit`}>
|
||||
<Link target="_blank" href={`/course/${course_uuid}`}>
|
||||
<EditorInfoThumbnail src={`${getCourseThumbnailMediaDirectory(props.org?.org_uuid, props.course.course_uuid, props.course.thumbnail_image)}`} alt=""></EditorInfoThumbnail>
|
||||
</Link>
|
||||
<EditorInfoDocName>
|
||||
|
|
@ -207,7 +207,7 @@ function Editor(props: Editor) {
|
|||
|
||||
<EditorUserProfileWrapper>
|
||||
{!session.isAuthenticated && <span>Loading</span>}
|
||||
{session.isAuthenticated && <Avvvatars value={session.user.user_uuid} style="shape" />}
|
||||
{session.isAuthenticated && <UserAvatar width={40} border="border-4" rounded="rounded-full"/>}
|
||||
</EditorUserProfileWrapper>
|
||||
|
||||
</EditorUsersSection>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue