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:
|
jobs:
|
||||||
next-lint:
|
next-lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [18]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- uses: pnpm/action-setup@v2
|
||||||
- name: Use Node.js
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
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
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: pnpm install
|
||||||
working-directory: ./apps/web
|
working-directory: ./apps/web
|
||||||
- name: Lint code
|
- name: Lint
|
||||||
run: npm run lint
|
run: pnpm run lint
|
||||||
working-directory: ./apps/web
|
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
|
- 📄✨Dynamic notion-like pages
|
||||||
- 👨🎓 Easy to use
|
- 🏎️ Easy to use
|
||||||
- 👥 Multi-Organization
|
- 👥 Multi-Organization
|
||||||
- 📹 Supports Uploadable Videos and external videos like YouTube
|
- 📹 Supports Uploadable Videos and external videos like YouTube
|
||||||
- 📄 Supports documents like PDF
|
- 📄 Supports documents like PDF
|
||||||
- 🍱 Course Collections
|
- 🍱 Course Collections
|
||||||
|
- 👨🎓 Users Management
|
||||||
- 🙋 Quizzes
|
- 🙋 Quizzes
|
||||||
- 👟 Course progress
|
- 👟 Course progress
|
||||||
- ⚡ (Incoming) Live Collaboration
|
- ✨ LearnHouse AI : The Teachers and Students copilot
|
||||||
- More to come
|
- More to come
|
||||||
|
|
||||||
## Community
|
## 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:
|
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
|
- **TailwindCSS** - Styling
|
||||||
- **Radix UI** - Accessible UI Components
|
- **Radix UI** - Accessible UI Components
|
||||||
- **Tiptap** - An editor framework and headless wrapper around ProseMirror
|
- **Tiptap** - An editor framework and headless wrapper around ProseMirror
|
||||||
- **FastAPI** - A high performance, async API framework for Python
|
- **FastAPI** - A high performance, async API framework for Python
|
||||||
- **YJS** - Shared data types for building collaborative software
|
- **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
|
- **React** - duh
|
||||||
|
|
||||||
## A word
|
## A word
|
||||||
|
|
|
||||||
2
apps/api/.gitignore
vendored
2
apps/api/.gitignore
vendored
|
|
@ -10,7 +10,7 @@ __pycache__/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
# Learnhouse
|
# Learnhouse
|
||||||
content/org_*
|
content/*
|
||||||
|
|
||||||
# Flyio
|
# Flyio
|
||||||
fly.toml
|
fly.toml
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ from fastapi_jwt_auth.exceptions import AuthJWTException
|
||||||
from fastapi.middleware.gzip import GZipMiddleware
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# from src.services.mocks.initial import create_initial_data
|
# from src.services.mocks.initial import create_initial_data
|
||||||
|
|
||||||
########################
|
########################
|
||||||
|
|
@ -26,7 +25,6 @@ app = FastAPI(
|
||||||
title=learnhouse_config.site_name,
|
title=learnhouse_config.site_name,
|
||||||
description=learnhouse_config.site_description,
|
description=learnhouse_config.site_description,
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
root_path="/",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
|
|
@ -61,8 +59,8 @@ app.mount("/content", StaticFiles(directory="content"), name="content")
|
||||||
# Global Routes
|
# Global Routes
|
||||||
app.include_router(v1_router)
|
app.include_router(v1_router)
|
||||||
|
|
||||||
|
|
||||||
# General Routes
|
# General Routes
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
return {"Message": "Welcome to LearnHouse ✨"}
|
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]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.11"
|
python = "^3.11"
|
||||||
fastapi = "0.104.1"
|
fastapi = "0.109.1"
|
||||||
pydantic = {version = ">=1.8.0,<2.0.0", extras = ["email"]}
|
pydantic = {version = ">=1.8.0,<2.0.0", extras = ["email"]}
|
||||||
sqlmodel = "0.0.10"
|
sqlmodel = "0.0.10"
|
||||||
uvicorn = "0.23.2"
|
uvicorn = "0.23.2"
|
||||||
|
|
@ -34,7 +34,6 @@ langchain = "0.1.0"
|
||||||
tiktoken = "^0.5.2"
|
tiktoken = "^0.5.2"
|
||||||
openai = "^1.7.1"
|
openai = "^1.7.1"
|
||||||
chromadb = "^0.4.22"
|
chromadb = "^0.4.22"
|
||||||
sentence-transformers = "^2.2.2"
|
|
||||||
python-dotenv = "^1.0.0"
|
python-dotenv = "^1.0.0"
|
||||||
redis = "^5.0.1"
|
redis = "^5.0.1"
|
||||||
langchain-community = "^0.0.11"
|
langchain-community = "^0.0.11"
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,5 @@ langchain-openai
|
||||||
tiktoken
|
tiktoken
|
||||||
openai
|
openai
|
||||||
chromadb
|
chromadb
|
||||||
sentence-transformers
|
|
||||||
python-dotenv
|
python-dotenv
|
||||||
redis
|
redis
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from sqlalchemy import JSON, BigInteger, Column, ForeignKey
|
from sqlalchemy import JSON, Column, ForeignKey, Integer
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
@ -38,12 +38,12 @@ class ActivityBase(SQLModel):
|
||||||
|
|
||||||
class Activity(ActivityBase, table=True):
|
class Activity(ActivityBase, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=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(
|
course_id: int = Field(
|
||||||
default=None,
|
default=None,
|
||||||
sa_column=Column(
|
sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE")),
|
||||||
BigInteger, ForeignKey("course.id", ondelete="CASCADE")
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
activity_uuid: str = ""
|
activity_uuid: str = ""
|
||||||
creation_date: str = ""
|
creation_date: str = ""
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ class BlockBase(SQLModel):
|
||||||
class Block(BlockBase, table=True):
|
class Block(BlockBase, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
content: dict = Field(default={}, sa_column=Column(JSON))
|
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")))
|
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")))
|
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")))
|
activity_id: int = Field(sa_column= Column("activity_id", ForeignKey("activity.id", ondelete="CASCADE")))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from sqlalchemy import BigInteger, Column, ForeignKey
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -10,7 +11,9 @@ class CollectionBase(SQLModel):
|
||||||
|
|
||||||
class Collection(CollectionBase, table=True):
|
class Collection(CollectionBase, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=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 = ""
|
collection_uuid: str = ""
|
||||||
creation_date: str = ""
|
creation_date: str = ""
|
||||||
update_date: str = ""
|
update_date: str = ""
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from sqlalchemy import BigInteger, Column, ForeignKey
|
from sqlalchemy import Column, ForeignKey, Integer
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
class CollectionCourse(SQLModel, table=True):
|
class CollectionCourse(SQLModel, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
collection_id: int = Field(sa_column=Column(BigInteger, ForeignKey("collection.id", ondelete="CASCADE")))
|
collection_id: int = Field(sa_column=Column(Integer, ForeignKey("collection.id", ondelete="CASCADE")))
|
||||||
course_id: int = Field(sa_column=Column(BigInteger, ForeignKey("course.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")
|
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||||
creation_date: str
|
creation_date: str
|
||||||
update_date: str
|
update_date: str
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from sqlalchemy import BigInteger, Column, ForeignKey
|
from sqlalchemy import Column, ForeignKey, Integer
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -7,10 +7,10 @@ class CourseChapter(SQLModel, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
order: int
|
order: int
|
||||||
course_id: int = Field(
|
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(
|
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")
|
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||||
creation_date: str
|
creation_date: str
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
from sqlalchemy import Column, ForeignKey, Integer
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
from src.db.users import UserRead
|
from src.db.users import UserRead
|
||||||
from src.db.trails import TrailRead
|
from src.db.trails import TrailRead
|
||||||
|
|
@ -17,7 +18,9 @@ class CourseBase(SQLModel):
|
||||||
|
|
||||||
class Course(CourseBase, table=True):
|
class Course(CourseBase, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=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 = ""
|
course_uuid: str = ""
|
||||||
creation_date: str = ""
|
creation_date: str = ""
|
||||||
update_date: str = ""
|
update_date: str = ""
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ class AIConfig(BaseModel):
|
||||||
enabled : bool = True
|
enabled : bool = True
|
||||||
limits: AILimitsSettings = AILimitsSettings()
|
limits: AILimitsSettings = AILimitsSettings()
|
||||||
embeddings: Literal[
|
embeddings: Literal[
|
||||||
"text-embedding-ada-002", "all-MiniLM-L6-v2"
|
"text-embedding-ada-002",
|
||||||
] = "all-MiniLM-L6-v2"
|
] = "text-embedding-ada-002"
|
||||||
ai_model: Literal["gpt-3.5-turbo", "gpt-4-1106-preview"] = "gpt-3.5-turbo"
|
ai_model: Literal["gpt-3.5-turbo", "gpt-4-1106-preview"] = "gpt-3.5-turbo"
|
||||||
features: AIEnabledFeatures = AIEnabledFeatures()
|
features: AIEnabledFeatures = AIEnabledFeatures()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
from src.db.roles import RoleRead
|
||||||
|
|
||||||
from src.db.organization_config import OrganizationConfig
|
from src.db.organization_config import OrganizationConfig
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -32,3 +35,9 @@ class OrganizationRead(OrganizationBase):
|
||||||
config: Optional[OrganizationConfig | dict]
|
config: Optional[OrganizationConfig | dict]
|
||||||
creation_date: str
|
creation_date: str
|
||||||
update_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 enum import Enum
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from sqlalchemy import Column, ForeignKey, Integer
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -12,7 +13,9 @@ class ResourceAuthorshipEnum(str, Enum):
|
||||||
class ResourceAuthor(SQLModel, table=True):
|
class ResourceAuthor(SQLModel, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
resource_uuid: str
|
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
|
authorship: ResourceAuthorshipEnum = ResourceAuthorshipEnum.CREATOR
|
||||||
creation_date: str = ""
|
creation_date: str = ""
|
||||||
update_date: str = ""
|
update_date: str = ""
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import JSON, Column
|
from sqlalchemy import JSON, Column, ForeignKey, Integer
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -45,7 +45,10 @@ class RoleBase(SQLModel):
|
||||||
|
|
||||||
class Role(RoleBase, table=True):
|
class Role(RoleBase, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=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_type: RoleTypeEnum = RoleTypeEnum.TYPE_GLOBAL
|
||||||
role_uuid: str = ""
|
role_uuid: str = ""
|
||||||
creation_date: str = ""
|
creation_date: str = ""
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import JSON, Column
|
from sqlalchemy import JSON, Column, ForeignKey, Integer
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
@ -23,10 +23,18 @@ class TrailRun(SQLModel, table=True):
|
||||||
data: dict = Field(default={}, sa_column=Column(JSON))
|
data: dict = Field(default={}, sa_column=Column(JSON))
|
||||||
status: StatusEnum = StatusEnum.STATUS_IN_PROGRESS
|
status: StatusEnum = StatusEnum.STATUS_IN_PROGRESS
|
||||||
# foreign keys
|
# foreign keys
|
||||||
trail_id: int = Field(default=None, foreign_key="trail.id")
|
trail_id: int = Field(
|
||||||
course_id: int = Field(default=None, foreign_key="course.id")
|
sa_column=Column(Integer, ForeignKey("trail.id", ondelete="CASCADE"))
|
||||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
)
|
||||||
user_id: int = Field(default=None, foreign_key="user.id")
|
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
|
# timestamps
|
||||||
creation_date: str
|
creation_date: str
|
||||||
update_date: str
|
update_date: str
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
from sqlalchemy import BigInteger, ForeignKey, JSON, Column
|
from sqlalchemy import ForeignKey, JSON, Column, Integer
|
||||||
|
|
||||||
|
|
||||||
class TrailStepTypeEnum(str, Enum):
|
class TrailStepTypeEnum(str, Enum):
|
||||||
|
|
@ -18,13 +18,23 @@ class TrailStep(SQLModel, table=True):
|
||||||
data: dict = Field(default={}, sa_column=Column(JSON))
|
data: dict = Field(default={}, sa_column=Column(JSON))
|
||||||
# foreign keys
|
# foreign keys
|
||||||
trailrun_id: int = Field(
|
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
|
# timestamps
|
||||||
creation_date: str
|
creation_date: str
|
||||||
update_date: str
|
update_date: str
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy import Column, ForeignKey, Integer
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
from src.db.trail_runs import TrailRunRead
|
from src.db.trail_runs import TrailRunRead
|
||||||
|
|
||||||
|
|
@ -24,8 +25,12 @@ class TrailCreate(TrailBase):
|
||||||
class TrailRead(BaseModel):
|
class TrailRead(BaseModel):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
trail_uuid: Optional[str]
|
trail_uuid: Optional[str]
|
||||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
org_id: int = Field(
|
||||||
user_id: int = Field(default=None, foreign_key="user.id")
|
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]
|
creation_date: Optional[str]
|
||||||
update_date: Optional[str]
|
update_date: Optional[str]
|
||||||
runs: list[TrailRunRead]
|
runs: list[TrailRunRead]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from sqlalchemy import BigInteger, Column, ForeignKey
|
from sqlalchemy import Column, ForeignKey, Integer
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -7,7 +7,7 @@ class UserOrganization(SQLModel, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
user_id: int = Field(default=None, foreign_key="user.id")
|
user_id: int = Field(default=None, foreign_key="user.id")
|
||||||
org_id: int = Field(
|
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")
|
role_id: int = Field(default=None, foreign_key="role.id")
|
||||||
creation_date: str
|
creation_date: str
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel, EmailStr
|
from pydantic import BaseModel, EmailStr
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
from src.db.roles import RoleRead
|
from src.db.roles import RoleRead
|
||||||
from src.db.organizations import OrganizationRead
|
|
||||||
|
|
||||||
|
|
||||||
class UserBase(SQLModel):
|
class UserBase(SQLModel):
|
||||||
|
|
@ -45,6 +44,7 @@ class PublicUser(UserRead):
|
||||||
|
|
||||||
|
|
||||||
class UserRoleWithOrg(BaseModel):
|
class UserRoleWithOrg(BaseModel):
|
||||||
|
from src.db.organizations import OrganizationRead
|
||||||
role: RoleRead
|
role: RoleRead
|
||||||
org: OrganizationRead
|
org: OrganizationRead
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from datetime import timedelta
|
||||||
from fastapi import Depends, APIRouter, HTTPException, Response, status, Request
|
from fastapi import Depends, APIRouter, HTTPException, Response, status, Request
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
|
|
@ -10,7 +11,7 @@ from src.security.auth import AuthJWT, authenticate_user
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/refresh")
|
@router.get("/refresh")
|
||||||
def refresh(response: Response, Authorize: AuthJWT = Depends()):
|
def refresh(response: Response, Authorize: AuthJWT = Depends()):
|
||||||
"""
|
"""
|
||||||
The jwt_refresh_token_required() function insures a valid refresh
|
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,
|
value=new_access_token,
|
||||||
httponly=False,
|
httponly=False,
|
||||||
domain=get_learnhouse_config().hosting_config.cookie_config.domain,
|
domain=get_learnhouse_config().hosting_config.cookie_config.domain,
|
||||||
|
expires=int(timedelta(hours=8).total_seconds()),
|
||||||
)
|
)
|
||||||
return {"access_token": new_access_token}
|
return {"access_token": new_access_token}
|
||||||
|
|
||||||
|
|
@ -53,14 +55,16 @@ async def login(
|
||||||
access_token = Authorize.create_access_token(subject=form_data.username)
|
access_token = Authorize.create_access_token(subject=form_data.username)
|
||||||
refresh_token = Authorize.create_refresh_token(subject=form_data.username)
|
refresh_token = Authorize.create_refresh_token(subject=form_data.username)
|
||||||
Authorize.set_refresh_cookies(refresh_token)
|
Authorize.set_refresh_cookies(refresh_token)
|
||||||
|
|
||||||
# set cookies using fastapi
|
# set cookies using fastapi
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
key="access_token_cookie",
|
key="access_token_cookie",
|
||||||
value=access_token,
|
value=access_token,
|
||||||
httponly=False,
|
httponly=False,
|
||||||
domain=get_learnhouse_config().hosting_config.cookie_config.domain,
|
domain=get_learnhouse_config().hosting_config.cookie_config.domain,
|
||||||
|
expires=int(timedelta(hours=8).total_seconds()),
|
||||||
)
|
)
|
||||||
|
|
||||||
user = UserRead.from_orm(user)
|
user = UserRead.from_orm(user)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ async def api_create_course(
|
||||||
name: str = Form(),
|
name: str = Form(),
|
||||||
description: str = Form(),
|
description: str = Form(),
|
||||||
public: bool = Form(),
|
public: bool = Form(),
|
||||||
learnings: str = Form(),
|
learnings: str = Form(None),
|
||||||
tags: str = Form(),
|
tags: str = Form(None),
|
||||||
about: str = Form(),
|
about: str = Form(),
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
db_session: Session = Depends(get_db_session),
|
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 fastapi import APIRouter, Depends, Request, UploadFile
|
||||||
from sqlmodel import Session
|
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.organization_config import OrganizationConfigBase
|
||||||
from src.db.users import PublicUser
|
from src.db.users import PublicUser
|
||||||
from src.db.organizations import (
|
from src.db.organizations import (
|
||||||
|
|
@ -8,6 +22,7 @@ from src.db.organizations import (
|
||||||
OrganizationCreate,
|
OrganizationCreate,
|
||||||
OrganizationRead,
|
OrganizationRead,
|
||||||
OrganizationUpdate,
|
OrganizationUpdate,
|
||||||
|
OrganizationUser,
|
||||||
)
|
)
|
||||||
from src.core.events.database import get_db_session
|
from src.core.events.database import get_db_session
|
||||||
from src.security.auth import get_current_user
|
from src.security.auth import get_current_user
|
||||||
|
|
@ -20,6 +35,7 @@ from src.services.orgs.orgs import (
|
||||||
get_orgs_by_user,
|
get_orgs_by_user,
|
||||||
update_org,
|
update_org,
|
||||||
update_org_logo,
|
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)
|
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}")
|
@router.get("/slug/{org_slug}")
|
||||||
async def api_get_org_by_slug(
|
async def api_get_org_by_slug(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
|
from src.services.orgs.orgs import get_org_join_mechanism
|
||||||
from src.security.auth import get_current_user
|
from src.security.auth import get_current_user
|
||||||
from src.core.events.database import get_db_session
|
from src.core.events.database import get_db_session
|
||||||
|
|
||||||
|
|
@ -16,12 +17,14 @@ from src.db.users import (
|
||||||
from src.services.users.users import (
|
from src.services.users.users import (
|
||||||
authorize_user_action,
|
authorize_user_action,
|
||||||
create_user,
|
create_user,
|
||||||
|
create_user_with_invite,
|
||||||
create_user_without_org,
|
create_user_without_org,
|
||||||
delete_user_by_id,
|
delete_user_by_id,
|
||||||
get_user_session,
|
get_user_session,
|
||||||
read_user_by_id,
|
read_user_by_id,
|
||||||
read_user_by_uuid,
|
read_user_by_uuid,
|
||||||
update_user,
|
update_user,
|
||||||
|
update_user_avatar,
|
||||||
update_user_password,
|
update_user_password,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -77,7 +80,48 @@ async def api_create_user_with_orgid(
|
||||||
"""
|
"""
|
||||||
Create User with Org ID
|
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"])
|
@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)
|
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"])
|
@router.put("/change_password/{user_id}", response_model=UserRead, tags=["users"])
|
||||||
async def api_update_user_password(
|
async def api_update_user_password(
|
||||||
*,
|
*,
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ class Settings(BaseModel):
|
||||||
authjwt_secret_key: str = "secret" if isDevModeEnabled() else SECRET_KEY
|
authjwt_secret_key: str = "secret" if isDevModeEnabled() else SECRET_KEY
|
||||||
authjwt_token_location = {"cookies", "headers"}
|
authjwt_token_location = {"cookies", "headers"}
|
||||||
authjwt_cookie_csrf_protect = False
|
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_samesite = "lax"
|
||||||
authjwt_cookie_secure = True
|
authjwt_cookie_secure = True
|
||||||
authjwt_cookie_domain = get_learnhouse_config().hosting_config.cookie_config.domain
|
authjwt_cookie_domain = get_learnhouse_config().hosting_config.cookie_config.domain
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,11 @@ async def authorization_verify_if_element_is_public(
|
||||||
element_uuid: str,
|
element_uuid: str,
|
||||||
action: Literal["read"],
|
action: Literal["read"],
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
):
|
):
|
||||||
element_nature = await check_element_type(element_uuid)
|
element_nature = await check_element_type(element_uuid)
|
||||||
# Verifies if the element is public
|
# 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":
|
if element_nature == "courses":
|
||||||
print("looking for course")
|
|
||||||
statement = select(Course).where(
|
statement = select(Course).where(
|
||||||
Course.public == True, Course.course_uuid == element_uuid
|
Course.public == True, Course.course_uuid == element_uuid
|
||||||
)
|
)
|
||||||
|
|
@ -29,20 +28,29 @@ async def authorization_verify_if_element_is_public(
|
||||||
if course:
|
if course:
|
||||||
return True
|
return True
|
||||||
else:
|
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(
|
statement = select(Collection).where(
|
||||||
Collection.public == True, Collection.collection_uuid == element_uuid
|
Collection.public == True, Collection.collection_uuid == element_uuid
|
||||||
)
|
)
|
||||||
collection = db_session.exec(statement).first()
|
collection = db_session.exec(statement).first()
|
||||||
|
|
||||||
if collection:
|
if collection:
|
||||||
return True
|
return True
|
||||||
else:
|
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:
|
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
|
# Tested and working
|
||||||
|
|
@ -106,6 +114,34 @@ async def authorization_verify_based_on_roles(
|
||||||
return False
|
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
|
# Tested and working
|
||||||
async def authorization_verify_based_on_roles_and_authorship(
|
async def authorization_verify_based_on_roles_and_authorship(
|
||||||
request: Request,
|
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
|
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_"):
|
if element_id.startswith("course_"):
|
||||||
return "courses"
|
return "courses"
|
||||||
elif element_id.startswith("user_"):
|
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
|
# Serialize Activity Content Blocks to a text comprehensible by the AI
|
||||||
structured = structure_activity_content_by_type(content)
|
structured = structure_activity_content_by_type(content)
|
||||||
|
|
||||||
|
isEmpty = structured == []
|
||||||
|
|
||||||
ai_friendly_text = serialize_activity_text_to_ai_comprehensible_text(
|
ai_friendly_text = serialize_activity_text_to_ai_comprehensible_text(
|
||||||
structured, course, activity
|
structured, course, activity, isActivityEmpty=isEmpty
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get Activity Organization
|
# Get Activity Organization
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from langchain.agents import AgentExecutor
|
from langchain.agents import AgentExecutor
|
||||||
from langchain.text_splitter import CharacterTextSplitter
|
from langchain.text_splitter import CharacterTextSplitter
|
||||||
from langchain.embeddings.sentence_transformer import SentenceTransformerEmbeddings
|
|
||||||
from langchain_community.vectorstores import Chroma
|
from langchain_community.vectorstores import Chroma
|
||||||
from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent
|
from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent
|
||||||
from langchain.prompts import MessagesPlaceholder
|
from langchain.prompts import MessagesPlaceholder
|
||||||
|
|
@ -45,7 +44,6 @@ def ask_ai(
|
||||||
texts = text_splitter.split_documents(documents)
|
texts = text_splitter.split_documents(documents)
|
||||||
|
|
||||||
embedding_models = {
|
embedding_models = {
|
||||||
"all-MiniLM-L6-v2": SentenceTransformerEmbeddings,
|
|
||||||
"text-embedding-ada-002": OpenAIEmbeddings,
|
"text-embedding-ada-002": OpenAIEmbeddings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,11 +51,11 @@ def ask_ai(
|
||||||
|
|
||||||
if embedding_model_name in embedding_models:
|
if embedding_model_name in embedding_models:
|
||||||
if embedding_model_name == "text-embedding-ada-002":
|
if embedding_model_name == "text-embedding-ada-002":
|
||||||
embedding_function = embedding_models[embedding_model_name](model=embedding_model_name, api_key=openai_api_key)
|
embedding_function = embedding_models[embedding_model_name](
|
||||||
if embedding_model_name == "all-MiniLM-L6-v2":
|
model=embedding_model_name, api_key=openai_api_key
|
||||||
embedding_function = embedding_models[embedding_model_name](model_name=embedding_model_name)
|
)
|
||||||
else:
|
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
|
# load it into Chroma and use it as a retriever
|
||||||
db = Chroma.from_documents(texts, embedding_function)
|
db = Chroma.from_documents(texts, embedding_function)
|
||||||
|
|
@ -75,7 +73,10 @@ def ask_ai(
|
||||||
memory_key = "history"
|
memory_key = "history"
|
||||||
|
|
||||||
memory = AgentTokenBufferMemory(
|
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))
|
system_message = SystemMessage(content=(message_for_the_prompt))
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@ async def upload_file_and_return_file_object(
|
||||||
|
|
||||||
await upload_content(
|
await upload_content(
|
||||||
f"courses/{course_uuid}/activities/{activity_uuid}/dynamic/blocks/{type_of_block}/{block_id}",
|
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_binary=file_binary,
|
||||||
file_and_format=f"{file_id}.{file_format}",
|
file_and_format=f"{file_id}.{file_format}",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
from src.db.courses import Course
|
||||||
from src.db.chapters import Chapter
|
from src.db.chapters import Chapter
|
||||||
from src.security.rbac.rbac import (
|
from src.security.rbac.rbac import (
|
||||||
authorization_verify_based_on_roles_and_authorship,
|
authorization_verify_based_on_roles_and_authorship,
|
||||||
|
|
@ -25,7 +26,6 @@ async def create_activity(
|
||||||
current_user: PublicUser | AnonymousUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
):
|
):
|
||||||
activity = Activity.from_orm(activity_object)
|
|
||||||
|
|
||||||
# CHeck if org exists
|
# CHeck if org exists
|
||||||
statement = select(Chapter).where(Chapter.id == activity_object.chapter_id)
|
statement = select(Chapter).where(Chapter.id == activity_object.chapter_id)
|
||||||
|
|
@ -40,6 +40,9 @@ async def create_activity(
|
||||||
# RBAC check
|
# RBAC check
|
||||||
await rbac_check(request, chapter.chapter_uuid, current_user, "create", db_session)
|
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.activity_uuid = str(f"activity_{uuid4()}")
|
||||||
activity.creation_date = str(datetime.now())
|
activity.creation_date = str(datetime.now())
|
||||||
activity.update_date = str(datetime.now())
|
activity.update_date = str(datetime.now())
|
||||||
|
|
@ -96,8 +99,18 @@ async def get_activity(
|
||||||
detail="Activity not found",
|
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
|
# 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)
|
activity = ActivityRead.from_orm(activity)
|
||||||
|
|
||||||
|
|
@ -223,7 +236,6 @@ async def rbac_check(
|
||||||
res = await authorization_verify_if_element_is_public(
|
res = await authorization_verify_if_element_is_public(
|
||||||
request, course_uuid, action, db_session
|
request, course_uuid, action, db_session
|
||||||
)
|
)
|
||||||
print('res',res)
|
|
||||||
return res
|
return res
|
||||||
else:
|
else:
|
||||||
res = await authorization_verify_based_on_roles_and_authorship(
|
res = await authorization_verify_based_on_roles_and_authorship(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ async def upload_pdf(pdf_file, activity_uuid, org_uuid, course_uuid):
|
||||||
try:
|
try:
|
||||||
await upload_content(
|
await upload_content(
|
||||||
f"courses/{course_uuid}/activities/{activity_uuid}/documentpdf",
|
f"courses/{course_uuid}/activities/{activity_uuid}/documentpdf",
|
||||||
|
"orgs",
|
||||||
org_uuid,
|
org_uuid,
|
||||||
contents,
|
contents,
|
||||||
f"documentpdf.{pdf_format}",
|
f"documentpdf.{pdf_format}",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ async def upload_video(video_file, activity_uuid, org_uuid, course_uuid):
|
||||||
try:
|
try:
|
||||||
await upload_content(
|
await upload_content(
|
||||||
f"courses/{course_uuid}/activities/{activity_uuid}/video",
|
f"courses/{course_uuid}/activities/{activity_uuid}/video",
|
||||||
|
'orgs',
|
||||||
org_uuid,
|
org_uuid,
|
||||||
contents,
|
contents,
|
||||||
f"video.{video_format}",
|
f"video.{video_format}",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@ from src.db.courses import CourseRead
|
||||||
|
|
||||||
def structure_activity_content_by_type(activity):
|
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
|
### 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"]
|
content = activity["content"]
|
||||||
|
|
||||||
headings = []
|
headings = []
|
||||||
|
|
@ -11,10 +15,12 @@ def structure_activity_content_by_type(activity):
|
||||||
paragraphs = []
|
paragraphs = []
|
||||||
|
|
||||||
for item in content:
|
for item in content:
|
||||||
if 'content' in item:
|
if "content" in item:
|
||||||
if item["type"] == "heading" and "text" in item["content"][0]:
|
if item["type"] == "heading" and "text" in item["content"][0]:
|
||||||
headings.append(item["content"][0]["text"])
|
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(
|
callouts.append(
|
||||||
"".join([text_item["text"] for text_item in item["content"]])
|
"".join([text_item["text"] for text_item in item["content"]])
|
||||||
)
|
)
|
||||||
|
|
@ -34,15 +40,29 @@ def structure_activity_content_by_type(activity):
|
||||||
# Add Paragraphs
|
# Add Paragraphs
|
||||||
data_array.append({"Paragraphs": paragraphs})
|
data_array.append({"Paragraphs": paragraphs})
|
||||||
|
|
||||||
print(data_array)
|
|
||||||
|
|
||||||
return data_array
|
return data_array
|
||||||
|
|
||||||
|
|
||||||
def serialize_activity_text_to_ai_comprehensible_text(
|
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
|
# Serialize Headings
|
||||||
serialized_headings = ""
|
serialized_headings = ""
|
||||||
|
|
@ -51,7 +71,6 @@ def serialize_activity_text_to_ai_comprehensible_text(
|
||||||
|
|
||||||
# Serialize Callouts
|
# Serialize Callouts
|
||||||
serialized_callouts = ""
|
serialized_callouts = ""
|
||||||
|
|
||||||
for callout in data_array[1]["Callouts"]:
|
for callout in data_array[1]["Callouts"]:
|
||||||
serialized_callouts += callout + " "
|
serialized_callouts += callout + " "
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,8 +112,17 @@ async def get_chapter(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist"
|
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
|
# 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
|
# Get activities for this chapter
|
||||||
statement = (
|
statement = (
|
||||||
|
|
@ -208,7 +217,7 @@ async def get_course_chapters(
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
) -> List[ChapterRead]:
|
) -> List[ChapterRead]:
|
||||||
|
|
||||||
statement = select(Course).where(Course.id == course_id)
|
statement = select(Course).where(Course.id == course_id)
|
||||||
course = db_session.exec(statement).first()
|
course = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
|
@ -225,7 +234,7 @@ async def get_course_chapters(
|
||||||
chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters]
|
chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters]
|
||||||
|
|
||||||
# RBAC check
|
# 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
|
# Get activities for each chapter
|
||||||
for chapter in chapters:
|
for chapter in chapters:
|
||||||
|
|
@ -473,12 +482,15 @@ async def reorder_chapters_and_activities(
|
||||||
db_session.delete(chapter_activity)
|
db_session.delete(chapter_activity)
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
|
|
||||||
# If links do not exist, create them
|
# If links do not exist, create them
|
||||||
chapter_activity_map = {}
|
chapter_activity_map = {}
|
||||||
for chapter_order in chapters_order.chapter_order_by_ids:
|
for chapter_order in chapters_order.chapter_order_by_ids:
|
||||||
for activity_order in chapter_order.activities_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
|
continue
|
||||||
|
|
||||||
statement = (
|
statement = (
|
||||||
|
|
@ -547,7 +559,7 @@ async def rbac_check(
|
||||||
res = await authorization_verify_if_element_is_public(
|
res = await authorization_verify_if_element_is_public(
|
||||||
request, course_uuid, action, db_session
|
request, course_uuid, action, db_session
|
||||||
)
|
)
|
||||||
print('res',res)
|
print("res", res)
|
||||||
return res
|
return res
|
||||||
else:
|
else:
|
||||||
res = await authorization_verify_based_on_roles_and_authorship(
|
res = await authorization_verify_based_on_roles_and_authorship(
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,10 @@ from fastapi import HTTPException, status, Request
|
||||||
|
|
||||||
|
|
||||||
async def get_collection(
|
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:
|
) -> CollectionRead:
|
||||||
statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
|
statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
|
||||||
collection = db_session.exec(statement).first()
|
collection = db_session.exec(statement).first()
|
||||||
|
|
@ -42,11 +45,23 @@ async def get_collection(
|
||||||
)
|
)
|
||||||
|
|
||||||
# get courses in collection
|
# get courses in collection
|
||||||
statement = (
|
statement_all = (
|
||||||
select(Course)
|
select(Course)
|
||||||
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
|
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
|
||||||
.distinct(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()
|
courses = db_session.exec(statement).all()
|
||||||
|
|
||||||
collection = CollectionRead(**collection.dict(), courses=courses)
|
collection = CollectionRead(**collection.dict(), courses=courses)
|
||||||
|
|
@ -180,7 +195,10 @@ async def update_collection(
|
||||||
|
|
||||||
|
|
||||||
async def delete_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)
|
statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
|
||||||
collection = db_session.exec(statement).first()
|
collection = db_session.exec(statement).first()
|
||||||
|
|
@ -216,23 +234,40 @@ async def get_collections(
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
) -> List[CollectionRead]:
|
) -> 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)
|
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 = db_session.exec(statement).all()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
collections_with_courses = []
|
collections_with_courses = []
|
||||||
|
|
||||||
for collection in collections:
|
for collection in collections:
|
||||||
statement = (
|
statement_all = (
|
||||||
select(Course)
|
select(Course)
|
||||||
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
|
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
|
||||||
.distinct(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()
|
courses = db_session.exec(statement).all()
|
||||||
|
|
||||||
collection = CollectionRead(**collection.dict(), courses=courses)
|
collection = CollectionRead(**collection.dict(), courses=courses)
|
||||||
|
|
@ -256,8 +291,11 @@ async def rbac_check(
|
||||||
res = await authorization_verify_if_element_is_public(
|
res = await authorization_verify_if_element_is_public(
|
||||||
request, collection_uuid, action, db_session
|
request, collection_uuid, action, db_session
|
||||||
)
|
)
|
||||||
print('res',res)
|
if res == False:
|
||||||
return res
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="User rights : You are not allowed to read this collection",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
res = await authorization_verify_based_on_roles_and_authorship(
|
res = await authorization_verify_based_on_roles_and_authorship(
|
||||||
request, current_user.id, action, collection_uuid, db_session
|
request, current_user.id, action, collection_uuid, db_session
|
||||||
|
|
@ -276,4 +314,3 @@ async def rbac_check(
|
||||||
|
|
||||||
|
|
||||||
## 🔒 RBAC Utils ##
|
## 🔒 RBAC Utils ##
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,9 @@ async def create_course(
|
||||||
)
|
)
|
||||||
course.thumbnail_image = name_in_disk
|
course.thumbnail_image = name_in_disk
|
||||||
|
|
||||||
|
else:
|
||||||
|
course.thumbnail_image = ""
|
||||||
|
|
||||||
# Insert course
|
# Insert course
|
||||||
db_session.add(course)
|
db_session.add(course)
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
|
|
||||||
from src.services.utils.upload_content import upload_content
|
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()
|
contents = thumbnail_file.file.read()
|
||||||
try:
|
try:
|
||||||
await upload_content(
|
await upload_content(
|
||||||
f"courses/{course_id}/thumbnails",
|
f"courses/{course_id}/thumbnails",
|
||||||
org_id,
|
"orgs",
|
||||||
|
org_uuid,
|
||||||
contents,
|
contents,
|
||||||
f"{name_in_disk}",
|
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(
|
await upload_content(
|
||||||
"logos",
|
"logos",
|
||||||
|
"orgs",
|
||||||
org_uuid,
|
org_uuid,
|
||||||
contents,
|
contents,
|
||||||
name_in_disk,
|
name_in_disk,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from src.db.organization_config import (
|
||||||
OrganizationConfigBase,
|
OrganizationConfigBase,
|
||||||
)
|
)
|
||||||
from src.security.rbac.rbac import (
|
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,
|
authorization_verify_if_user_is_anon,
|
||||||
)
|
)
|
||||||
from src.db.users import AnonymousUser, PublicUser
|
from src.db.users import AnonymousUser, PublicUser
|
||||||
|
|
@ -169,7 +169,7 @@ async def create_org(
|
||||||
limits_enabled=False,
|
limits_enabled=False,
|
||||||
max_asks=0,
|
max_asks=0,
|
||||||
),
|
),
|
||||||
embeddings="all-MiniLM-L6-v2",
|
embeddings="text-embedding-ada-002",
|
||||||
ai_model="gpt-3.5-turbo",
|
ai_model="gpt-3.5-turbo",
|
||||||
features=AIEnabledFeatures(
|
features=AIEnabledFeatures(
|
||||||
editor=False,
|
editor=False,
|
||||||
|
|
@ -438,12 +438,106 @@ async def get_orgs_by_user(
|
||||||
return orgs
|
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 ##
|
## 🔒 RBAC Utils ##
|
||||||
|
|
||||||
|
|
||||||
async def rbac_check(
|
async def rbac_check(
|
||||||
request: Request,
|
request: Request,
|
||||||
org_id: str,
|
org_uuid: str,
|
||||||
current_user: PublicUser | AnonymousUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
action: Literal["create", "read", "update", "delete"],
|
action: Literal["create", "read", "update", "delete"],
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
|
|
@ -453,11 +547,25 @@ async def rbac_check(
|
||||||
return True
|
return True
|
||||||
|
|
||||||
else:
|
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(
|
isAllowedOnOrgAdminStatus = (
|
||||||
request, current_user.id, action, org_id, db_session
|
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 ##
|
## 🔒 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,
|
trail_object: TrailCreate,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
) -> Trail:
|
) -> 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()
|
trail = db_session.exec(statement).first()
|
||||||
|
|
||||||
if trail:
|
if trail:
|
||||||
|
|
@ -124,7 +126,7 @@ async def check_trail_presence(
|
||||||
async def get_user_trail_with_orgid(
|
async def get_user_trail_with_orgid(
|
||||||
request: Request, user: PublicUser | AnonymousUser, org_id: int, db_session: Session
|
request: Request, user: PublicUser | AnonymousUser, org_id: int, db_session: Session
|
||||||
) -> TrailRead:
|
) -> TrailRead:
|
||||||
|
|
||||||
if isinstance(user, AnonymousUser):
|
if isinstance(user, AnonymousUser):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
|
@ -151,7 +153,7 @@ async def get_user_trail_with_orgid(
|
||||||
for trail_run in trail_runs:
|
for trail_run in trail_runs:
|
||||||
statement = select(Course).where(Course.id == trail_run.course_id)
|
statement = select(Course).where(Course.id == trail_run.course_id)
|
||||||
course = db_session.exec(statement).first()
|
course = db_session.exec(statement).first()
|
||||||
trail_run.course = course
|
trail_run.course = course
|
||||||
|
|
||||||
# Add number of activities (steps) in a course
|
# Add number of activities (steps) in a course
|
||||||
statement = select(ChapterActivity).where(
|
statement = select(ChapterActivity).where(
|
||||||
|
|
@ -213,7 +215,7 @@ async def add_activity_to_trail(
|
||||||
)
|
)
|
||||||
|
|
||||||
statement = select(TrailRun).where(
|
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()
|
trailrun = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
|
@ -231,7 +233,7 @@ async def add_activity_to_trail(
|
||||||
db_session.refresh(trailrun)
|
db_session.refresh(trailrun)
|
||||||
|
|
||||||
statement = select(TrailStep).where(
|
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()
|
trailstep = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
|
@ -253,7 +255,7 @@ async def add_activity_to_trail(
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
db_session.refresh(trailstep)
|
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 = db_session.exec(statement).all()
|
||||||
|
|
||||||
trail_runs = [
|
trail_runs = [
|
||||||
|
|
@ -262,7 +264,7 @@ async def add_activity_to_trail(
|
||||||
]
|
]
|
||||||
|
|
||||||
for trail_run in trail_runs:
|
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 = db_session.exec(statement).all()
|
||||||
|
|
||||||
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
|
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
|
# 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()
|
trailrun = db_session.exec(statement).first()
|
||||||
|
|
||||||
if trailrun:
|
if trailrun:
|
||||||
|
|
@ -315,7 +319,7 @@ async def add_course_to_trail(
|
||||||
)
|
)
|
||||||
|
|
||||||
statement = select(TrailRun).where(
|
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()
|
trail_run = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
|
@ -332,7 +336,7 @@ async def add_course_to_trail(
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
db_session.refresh(trail_run)
|
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 = db_session.exec(statement).all()
|
||||||
|
|
||||||
trail_runs = [
|
trail_runs = [
|
||||||
|
|
@ -341,7 +345,7 @@ async def add_course_to_trail(
|
||||||
]
|
]
|
||||||
|
|
||||||
for trail_run in trail_runs:
|
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 = db_session.exec(statement).all()
|
||||||
|
|
||||||
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
|
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(
|
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()
|
trail_run = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
|
@ -394,14 +398,14 @@ async def remove_course_from_trail(
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
# Delete all trail steps for this course
|
# 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()
|
trail_steps = db_session.exec(statement).all()
|
||||||
|
|
||||||
for trail_step in trail_steps:
|
for trail_step in trail_steps:
|
||||||
db_session.delete(trail_step)
|
db_session.delete(trail_step)
|
||||||
db_session.commit()
|
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 = db_session.exec(statement).all()
|
||||||
|
|
||||||
trail_runs = [
|
trail_runs = [
|
||||||
|
|
@ -410,7 +414,7 @@ async def remove_course_from_trail(
|
||||||
]
|
]
|
||||||
|
|
||||||
for trail_run in trail_runs:
|
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 = db_session.exec(statement).all()
|
||||||
|
|
||||||
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
|
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,13 +1,15 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from fastapi import HTTPException, Request, status
|
from fastapi import HTTPException, Request, UploadFile, status
|
||||||
from sqlmodel import Session, select
|
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.db.roles import Role, RoleRead
|
||||||
from src.security.rbac.rbac import (
|
from src.security.rbac.rbac import (
|
||||||
authorization_verify_based_on_roles_and_authorship,
|
authorization_verify_based_on_roles_and_authorship,
|
||||||
authorization_verify_if_user_is_anon,
|
authorization_verify_if_user_is_anon,
|
||||||
)
|
)
|
||||||
from src.db.organizations import Organization, OrganizationRead
|
from src.db.organizations import Organization, OrganizationRead
|
||||||
from src.db.users import (
|
from src.db.users import (
|
||||||
AnonymousUser,
|
AnonymousUser,
|
||||||
|
|
@ -102,6 +104,27 @@ async def create_user(
|
||||||
|
|
||||||
return 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(
|
async def create_user_without_org(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
@ -195,6 +218,49 @@ async def update_user(
|
||||||
return 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(
|
async def update_user_password(
|
||||||
request: Request,
|
request: Request,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from typing import Literal
|
||||||
import boto3
|
import boto3
|
||||||
from botocore.exceptions import ClientError
|
from botocore.exceptions import ClientError
|
||||||
import os
|
import os
|
||||||
|
|
@ -6,7 +7,11 @@ from config.config import get_learnhouse_config
|
||||||
|
|
||||||
|
|
||||||
async def upload_content(
|
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
|
# Get Learnhouse Config
|
||||||
learnhouse_config = get_learnhouse_config()
|
learnhouse_config = get_learnhouse_config()
|
||||||
|
|
@ -16,12 +21,12 @@ async def upload_content(
|
||||||
|
|
||||||
if content_delivery == "filesystem":
|
if content_delivery == "filesystem":
|
||||||
# create folder for activity
|
# 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
|
# create folder for activity
|
||||||
os.makedirs(f"content/{org_uuid}/{directory}")
|
os.makedirs(f"content/{type_of_dir}/{uuid}/{directory}")
|
||||||
# upload file to server
|
# upload file to server
|
||||||
with open(
|
with open(
|
||||||
f"content/{org_uuid}/{directory}/{file_and_format}",
|
f"content/{type_of_dir}/{uuid}/{directory}/{file_and_format}",
|
||||||
"wb",
|
"wb",
|
||||||
) as f:
|
) as f:
|
||||||
f.write(file_binary)
|
f.write(file_binary)
|
||||||
|
|
@ -37,13 +42,13 @@ async def upload_content(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create folder for activity
|
# 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
|
# create folder for activity
|
||||||
os.makedirs(f"content/{org_uuid}/{directory}")
|
os.makedirs(f"content/{type_of_dir}/{uuid}/{directory}")
|
||||||
|
|
||||||
# Upload file to server
|
# Upload file to server
|
||||||
with open(
|
with open(
|
||||||
f"content/{org_uuid}/{directory}/{file_and_format}",
|
f"content/{type_of_dir}/{uuid}/{directory}/{file_and_format}",
|
||||||
"wb",
|
"wb",
|
||||||
) as f:
|
) as f:
|
||||||
f.write(file_binary)
|
f.write(file_binary)
|
||||||
|
|
@ -52,9 +57,9 @@ async def upload_content(
|
||||||
print("Uploading to s3 using boto3...")
|
print("Uploading to s3 using boto3...")
|
||||||
try:
|
try:
|
||||||
s3.upload_file(
|
s3.upload_file(
|
||||||
f"content/{org_uuid}/{directory}/{file_and_format}",
|
f"content/{type_of_dir}/{uuid}/{directory}/{file_and_format}",
|
||||||
"learnhouse-media",
|
"learnhouse-media",
|
||||||
f"content/{org_uuid}/{directory}/{file_and_format}",
|
f"content/{type_of_dir}/{uuid}/{directory}/{file_and_format}",
|
||||||
)
|
)
|
||||||
except ClientError as e:
|
except ClientError as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
@ -63,7 +68,7 @@ async def upload_content(
|
||||||
try:
|
try:
|
||||||
s3.head_object(
|
s3.head_object(
|
||||||
Bucket="learnhouse-media",
|
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!")
|
print("File upload successful!")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
"rules": {
|
"rules": {
|
||||||
"react/no-unescaped-entities": "off",
|
"react/no-unescaped-entities": "off",
|
||||||
"@next/next/no-page-custom-font": "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
|
WORKDIR /usr/learnhouse/front
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,10 @@ import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { getActivityWithAuthHeader } from "@services/courses/activities";
|
import { getActivityWithAuthHeader } from "@services/courses/activities";
|
||||||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
|
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
|
||||||
import { getOrganizationContextInfo, getOrganizationContextInfoWithId } from "@services/organizations/orgs";
|
import { getOrganizationContextInfoWithId } from "@services/organizations/orgs";
|
||||||
import SessionProvider from "@components/Contexts/SessionContext";
|
import SessionProvider from "@components/Contexts/SessionContext";
|
||||||
import EditorOptionsProvider from "@components/Contexts/Editor/EditorContext";
|
import EditorOptionsProvider from "@components/Contexts/Editor/EditorContext";
|
||||||
import AIChatBotProvider from "@components/Contexts/AI/AIChatBotContext";
|
|
||||||
import AIEditorProvider from "@components/Contexts/AI/AIEditorContext";
|
import AIEditorProvider from "@components/Contexts/AI/AIEditorContext";
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,24 @@
|
||||||
'use client'
|
'use client'
|
||||||
import React, { use, useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { INSTALL_STEPS } from './steps/steps'
|
import { INSTALL_STEPS } from './steps/steps'
|
||||||
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
|
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
|
import { Suspense } from 'react'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function InstallClient() {
|
function InstallClient() {
|
||||||
|
return (
|
||||||
|
<GeneralWrapperStyled>
|
||||||
|
<Suspense>
|
||||||
|
<>
|
||||||
|
<Stepscomp />
|
||||||
|
</>
|
||||||
|
</Suspense>
|
||||||
|
</GeneralWrapperStyled>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Stepscomp = () => {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const step: any = parseInt(searchParams.get('step') || '0');
|
const step: any = parseInt(searchParams.get('step') || '0');
|
||||||
|
|
@ -24,7 +35,7 @@ function InstallClient() {
|
||||||
}, [step])
|
}, [step])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GeneralWrapperStyled>
|
<div>
|
||||||
<div className='flex justify-center '>
|
<div className='flex justify-center '>
|
||||||
<div className='grow'>
|
<div className='grow'>
|
||||||
<LearnHouseLogo />
|
<LearnHouseLogo />
|
||||||
|
|
@ -54,7 +65,7 @@ function InstallClient() {
|
||||||
{stepsState[stepNumber].component}
|
{stepsState[stepNumber].component}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GeneralWrapperStyled>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"use client";
|
"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 * as Form from '@radix-ui/react-form';
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config';
|
||||||
import { createNewUserInstall, updateInstall } from '@services/install/install';
|
import { createNewUserInstall, updateInstall } from '@services/install/install';
|
||||||
|
|
@ -8,7 +8,7 @@ import { useFormik } from 'formik';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { BarLoader } from 'react-spinners';
|
import { BarLoader } from 'react-spinners';
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
const validate = (values: any) => {
|
const validate = (values: any) => {
|
||||||
const errors: any = {};
|
const errors: any = {};
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import PageLoading from '@components/Objects/Loaders/PageLoading';
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config';
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
import { swrFetcher } from '@services/utils/ts/requests';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import React, { use, useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
function GetStarted() {
|
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 * as Form from '@radix-ui/react-form';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { BarLoader } from 'react-spinners';
|
import { BarLoader } from 'react-spinners';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { createNewOrganization } from '@services/organizations/orgs';
|
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
import { swrFetcher } from '@services/utils/ts/requests';
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config';
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR from "swr";
|
||||||
import { createNewOrgInstall, updateInstall } from '@services/install/install';
|
import { createNewOrgInstall, updateInstall } from '@services/install/install';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Check } from 'lucide-react';
|
import { Check } from 'lucide-react';
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { createSampleDataInstall, updateInstall } from '@services/install/instal
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
import { swrFetcher } from '@services/utils/ts/requests';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
function SampleData() {
|
function SampleData() {
|
||||||
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
|
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
||||||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
|
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
|
||||||
import { getBackendUrl, getUriWithOrg } from "@services/config/config";
|
import { getUriWithOrg } from "@services/config/config";
|
||||||
import { getCollectionByIdWithAuthHeader } from "@services/courses/collections";
|
import { getCollectionByIdWithAuthHeader } from "@services/courses/collections";
|
||||||
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
||||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,26 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { createCollection } from "@services/courses/collections";
|
import { createCollection } from "@services/courses/collections";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
|
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
|
||||||
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
|
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
|
||||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
import { useOrg } from "@components/Contexts/OrgContext";
|
||||||
|
|
||||||
function NewCollection(params: any) {
|
function NewCollection(params: any) {
|
||||||
|
const org = useOrg() as any;
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug;
|
||||||
const [name, setName] = React.useState("");
|
const [name, setName] = React.useState("");
|
||||||
const [org, setOrg] = React.useState({}) as any;
|
|
||||||
const [description, setDescription] = React.useState("");
|
const [description, setDescription] = React.useState("");
|
||||||
const [selectedCourses, setSelectedCourses] = React.useState([]) as any;
|
const [selectedCourses, setSelectedCourses] = React.useState([]) as any;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { data: courses, error: error } = useSWR(`${getAPIUrl()}courses/org_slug/${orgslug}/page/1/limit/10`, swrFetcher);
|
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>) => {
|
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setName(event.target.value);
|
setName(event.target.value);
|
||||||
|
|
@ -35,83 +32,94 @@ function NewCollection(params: any) {
|
||||||
|
|
||||||
const handleSubmit = async (e: any) => {
|
const handleSubmit = async (e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const collection = {
|
const collection = {
|
||||||
name: name,
|
name: name,
|
||||||
description: description,
|
description: description,
|
||||||
courses: selectedCourses,
|
courses: selectedCourses,
|
||||||
public: true,
|
public: isPublic,
|
||||||
org_id: org.id,
|
org_id: org.id,
|
||||||
};
|
};
|
||||||
await createCollection(collection);
|
await createCollection(collection);
|
||||||
await revalidateTags(["collections"], orgslug);
|
await revalidateTags(["collections"], org.slug);
|
||||||
|
// reload the page
|
||||||
router.refresh();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-64 m-auto py-20">
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={handleNameChange}
|
onChange={handleNameChange}
|
||||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
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>
|
<p className="text-gray-500">Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div className="space-y-4 p-3">
|
||||||
|
<p>Courses</p>
|
||||||
{courses.map((course: any) => (
|
{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"
|
<label htmlFor={course.course_uuid} className="text-sm text-gray-700">{course.name}</label>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Description"
|
placeholder="Description"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={handleDescriptionChange}
|
onChange={handleDescriptionChange}
|
||||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleSubmit}
|
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"
|
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
|
Submit
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { cookies } from "next/headers";
|
||||||
import ActivityClient from "./activity";
|
import ActivityClient from "./activity";
|
||||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
|
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
|
||||||
|
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { removeCourse, startCourse } from "@services/courses/activity";
|
import { removeCourse, startCourse } from "@services/courses/activity";
|
||||||
import Link from "next/link";
|
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 { getUriWithOrg } from "@services/config/config";
|
||||||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
||||||
import { revalidateTags } from "@services/utils/ts/requests";
|
import { revalidateTags } from "@services/utils/ts/requests";
|
||||||
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
|
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
||||||
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
import { getCourseThumbnailMediaDirectory, getUserAvatarMediaDirectory } from "@services/media/media";
|
||||||
import { ArrowRight, Check, File, Sparkles, Star, Video } from "lucide-react";
|
import { ArrowRight, Check, File, Sparkles, Video } from "lucide-react";
|
||||||
import Avvvatars from "avvvatars-react";
|
|
||||||
import { getUser } from "@services/users/users";
|
|
||||||
import { useOrg } from "@components/Contexts/OrgContext";
|
import { useOrg } from "@components/Contexts/OrgContext";
|
||||||
|
import UserAvatar from "@components/Objects/UserAvatar";
|
||||||
|
|
||||||
const CourseClient = (props: any) => {
|
const CourseClient = (props: any) => {
|
||||||
const [user, setUser] = useState<any>({});
|
const [user, setUser] = useState<any>({});
|
||||||
|
|
@ -25,7 +24,7 @@ const CourseClient = (props: any) => {
|
||||||
|
|
||||||
function getLearningTags() {
|
function getLearningTags() {
|
||||||
// create array of learnings from a string object (comma separated)
|
// create array of learnings from a string object (comma separated)
|
||||||
let learnings = course.learnings.split(",");
|
let learnings = course?.learnings ? course?.learnings.split(",") : [];
|
||||||
setLearnings(learnings);
|
setLearnings(learnings);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -56,13 +55,13 @@ const CourseClient = (props: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
getLearningTags();
|
||||||
}
|
}
|
||||||
, [org]);
|
, [org, course]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!course ? (
|
{!course && !org ? (
|
||||||
<PageLoading></PageLoading>
|
<PageLoading></PageLoading>
|
||||||
) : (
|
) : (
|
||||||
<GeneralWrapperStyled>
|
<GeneralWrapperStyled>
|
||||||
|
|
@ -73,9 +72,13 @@ const CourseClient = (props: any) => {
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</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-[300px] bg-cover bg-center mb-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org?.org_uuid, course.course_uuid, course.thumbnail_image)})` }}>
|
<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>
|
||||||
|
:
|
||||||
|
<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} />
|
<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>
|
<p className="py-5 px-5">{course.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="py-3 text-2xl font-bold">What you will learn</h2>
|
{learnings.length > 0 && learnings[0] !== "null" &&
|
||||||
<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">
|
<div>
|
||||||
{learnings.map((learning: any) => {
|
<h2 className="py-3 text-2xl font-bold">What you will learn</h2>
|
||||||
return (
|
<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">
|
||||||
<div key={learning}
|
{learnings.map((learning: any) => {
|
||||||
className="flex space-x-2 items-center font-semibold text-gray-500 capitalize">
|
return (
|
||||||
<div className="px-2 py-2 rounded-full">
|
<div key={learning}
|
||||||
<Check className="text-gray-400" size={15} />
|
className="flex space-x-2 items-center font-semibold text-gray-500">
|
||||||
</div>
|
<div className="px-2 py-2 rounded-full">
|
||||||
<p>{learning}</p>
|
<Check className="text-gray-400" size={15} />
|
||||||
</div>
|
</div>
|
||||||
);
|
<p>{learning}</p>
|
||||||
}
|
</div>
|
||||||
)}
|
);
|
||||||
</div>
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<h2 className="py-3 text-2xl font-bold">Course Lessons</h2>
|
<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">
|
<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>
|
</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 &&
|
{user &&
|
||||||
<div className="flex mx-auto space-x-3 px-2 py-2 items-center">
|
<div className="flex flex-col mx-auto space-y-3 px-2 py-2 items-center">
|
||||||
<div className="">
|
<UserAvatar border="border-8" avatar_url={getUserAvatarMediaDirectory(course.authors[0].user_uuid, course.authors[0].avatar_image)} width={100} />
|
||||||
<Avvvatars border borderSize={5} borderColor="white" size={50} shadow value={course.authors[0].username} style='shape' />
|
|
||||||
</div>
|
|
||||||
<div className="-space-y-2 ">
|
<div className="-space-y-2 ">
|
||||||
<div className="text-[12px] text-neutral-400 font-semibold">Author</div>
|
<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>
|
||||||
</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;
|
export default CourseClient;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { cookies } from 'next/headers';
|
||||||
import { getCourseMetadataWithAuthHeader } from '@services/courses/courses';
|
import { getCourseMetadataWithAuthHeader } from '@services/courses/courses';
|
||||||
import { getOrganizationContextInfo } from '@services/organizations/orgs';
|
import { getOrganizationContextInfo } from '@services/organizations/orgs';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from '@services/auth/auth';
|
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth';
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string, courseuuid: string };
|
params: { orgslug: string, courseuuid: string };
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { getOrgCoursesWithAuthHeader } from "@services/courses/courses";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
|
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string };
|
params: { orgslug: string };
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export default function Error({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ErrorUI></ErrorUI>
|
<ErrorUI ></ErrorUI>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -12,7 +12,6 @@ import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth';
|
||||||
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
|
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
|
||||||
import CollectionThumbnail from '@components/Objects/Thumbnails/CollectionThumbnail';
|
import CollectionThumbnail from '@components/Objects/Thumbnails/CollectionThumbnail';
|
||||||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
||||||
import { Plus, PlusCircle } from 'lucide-react';
|
|
||||||
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
|
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
|
||||||
import NewCollectionButton from '@components/StyledElements/Buttons/NewCollectionButton';
|
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 TypeOfContentTitle from "@components/StyledElements/Titles/TypeOfContentTitle";
|
||||||
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
||||||
import { getAPIUrl } from "@services/config/config";
|
import { getAPIUrl } from "@services/config/config";
|
||||||
import { removeCourse } from "@services/courses/activity";
|
import { swrFetcher } from "@services/utils/ts/requests";
|
||||||
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
function Trail(params: any) {
|
function Trail(params: any) {
|
||||||
let orgslug = params.orgslug;
|
let orgslug = params.orgslug;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
|
||||||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
||||||
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
|
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
|
||||||
import Modal from '@components/StyledElements/Modal/Modal';
|
import Modal from '@components/StyledElements/Modal/Modal';
|
||||||
import Link from 'next/link'
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,13 @@
|
||||||
'use client';
|
'use client';
|
||||||
import EditCourseStructure from '../../../../../../../../components/Dashboard/Course/EditCourseStructure/EditCourseStructure'
|
import EditCourseStructure from '../../../../../../../../components/Dashboard/Course/EditCourseStructure/EditCourseStructure'
|
||||||
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
|
import { getUriWithOrg } from '@services/config/config';
|
||||||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
import React from 'react'
|
||||||
import ClientComponentSkeleton from '@components/Utils/ClientComp';
|
import { CourseProvider } from '../../../../../../../../components/Contexts/CourseContext';
|
||||||
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 Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop';
|
import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop';
|
||||||
import { CSSTransition } from 'react-transition-group';
|
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import EditCourseGeneral from '@components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral';
|
import EditCourseGeneral from '@components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral';
|
||||||
import { GalleryVertical, GalleryVerticalEnd, Info } from 'lucide-react';
|
import { GalleryVerticalEnd, Info } from 'lucide-react';
|
||||||
|
|
||||||
export type CourseOverviewParams = {
|
export type CourseOverviewParams = {
|
||||||
orgslug: string,
|
orgslug: string,
|
||||||
|
|
@ -32,9 +25,9 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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)}>
|
<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} />
|
<CourseOverviewTop params={params} />
|
||||||
<div className='flex space-x-5 font-black text-sm'>
|
<div className='flex space-x-5 font-black text-sm'>
|
||||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/courses/course/${params.courseuuid}/general`}>
|
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/courses/course/${params.courseuuid}/general`}>
|
||||||
|
|
@ -57,12 +50,12 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='h-6'></div>
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, }}
|
initial={{ opacity: 0, }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
|
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
|
||||||
|
className='h-full overflow-y-auto'
|
||||||
>
|
>
|
||||||
{params.subpage == 'content' ? <EditCourseStructure orgslug={params.orgslug} /> : ''}
|
{params.subpage == 'content' ? <EditCourseStructure orgslug={params.orgslug} /> : ''}
|
||||||
{params.subpage == 'general' ? <EditCourseGeneral orgslug={params.orgslug} /> : ''}
|
{params.subpage == 'general' ? <EditCourseGeneral orgslug={params.orgslug} /> : ''}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,20 @@
|
||||||
import SessionProvider from '@components/Contexts/SessionContext'
|
import SessionProvider from '@components/Contexts/SessionContext'
|
||||||
import LeftMenu from '@components/Dashboard/UI/LeftMenu'
|
import LeftMenu from '@components/Dashboard/UI/LeftMenu'
|
||||||
|
import AdminAuthorization from '@components/Security/AdminAuthorization'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
function DashboardLayout({ children, params }: { children: React.ReactNode, params: any }) {
|
function DashboardLayout({ children, params }: { children: React.ReactNode, params: any }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<div className='flex'>
|
<AdminAuthorization authorizationMode="page">
|
||||||
<LeftMenu/>
|
<div className='flex'>
|
||||||
<div className='flex w-full'>
|
<LeftMenu />
|
||||||
{children}
|
<div className='flex w-full'>
|
||||||
</div>
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</AdminAuthorization>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,62 @@
|
||||||
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
import Image from 'next/image'
|
||||||
import React from 'react'
|
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() {
|
function DashboardHome() {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center mx-auto min-h-screen flex-col space-x-3">
|
<div className="flex items-center justify-center mx-auto min-h-screen flex-col space-x-3">
|
||||||
<PageLoading />
|
<div className='mx-auto pb-10'>
|
||||||
<div className='text-neutral-400 font-bold animate-pulse text-2xl'>This page is work in progress</div>
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
'use client';
|
'use client';
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import UserEditGeneral from '@components/Dashboard/User/UserEditGeneral/UserEditGeneral';
|
import UserEditGeneral from '@components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral';
|
||||||
import UserEditPassword from '@components/Dashboard/User/UserEditPassword/UserEditPassword';
|
import UserEditPassword from '@components/Dashboard/UserAccount/UserEditPassword/UserEditPassword';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { getUriWithOrg } from '@services/config/config';
|
import { getUriWithOrg } from '@services/config/config';
|
||||||
import { Info, Lock } from 'lucide-react';
|
import { Info, Lock } from 'lucide-react';
|
||||||
|
|
@ -24,7 +24,7 @@ function SettingsPage({ params }: { params: SettingsParams }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full w-full bg-[#f8f8f8]'>
|
<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>
|
<BreadCrumbs type='user' last_breadcrumb={session?.user?.username} ></BreadCrumbs>
|
||||||
<div className='my-2 tracking-tighter'>
|
<div className='my-2 tracking-tighter'>
|
||||||
<div className='w-100 flex justify-between'>
|
<div className='w-100 flex justify-between'>
|
||||||
|
|
@ -32,7 +32,7 @@ function SettingsPage({ params }: { params: SettingsParams }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex space-x-5 font-black text-sm'>
|
<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={`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'>
|
<div className='flex items-center space-x-2.5 mx-2'>
|
||||||
|
|
@ -41,7 +41,7 @@ function SettingsPage({ params }: { params: SettingsParams }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</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 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'>
|
<div className='flex items-center space-x-2.5 mx-2'>
|
||||||
<Lock size={16} />
|
<Lock size={16} />
|
||||||
|
|
@ -58,6 +58,7 @@ function SettingsPage({ params }: { params: SettingsParams }) {
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
|
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
|
||||||
|
className='h-full overflow-y-auto'
|
||||||
>
|
>
|
||||||
{params.subpage == 'general' ? <UserEditGeneral /> : ''}
|
{params.subpage == 'general' ? <UserEditGeneral /> : ''}
|
||||||
{params.subpage == 'security' ? <UserEditPassword /> : ''}
|
{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 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 Image from 'next/image';
|
||||||
import * as Form from '@radix-ui/react-form';
|
import * as Form from '@radix-ui/react-form';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { getOrgLogoMediaDirectory } from "@services/media/media";
|
import { getOrgLogoMediaDirectory } from "@services/media/media";
|
||||||
import { BarLoader } from "react-spinners";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { loginAndGetToken } from "@services/auth/auth";
|
import { loginAndGetToken } from "@services/auth/auth";
|
||||||
import { AlertTriangle } from "lucide-react";
|
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 className="m-auto flex space-x-4 items-center flex-wrap">
|
||||||
<div>Login to </div>
|
<div>Login to </div>
|
||||||
<div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]" >
|
<div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]" >
|
||||||
{props.org?.logo ? (
|
{props.org?.logo_image ? (
|
||||||
<img
|
<img
|
||||||
src={`${getOrgLogoMediaDirectory(props.org.org_id, props.org?.logo)}`}
|
src={`${getOrgLogoMediaDirectory(props.org.org_uuid, props.org?.logo_image)}`}
|
||||||
alt="Learnhouse"
|
alt="Learnhouse"
|
||||||
style={{ width: "auto", height: 70 }}
|
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="" />
|
<Image quality={100} width={70} height={70} src={learnhouseIcon} alt="" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export async function generateMetadata(
|
||||||
): Promise<Metadata> {
|
): Promise<Metadata> {
|
||||||
const orgslug = params.orgslug;
|
const orgslug = params.orgslug;
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: 'Login' + ` — ${org.name}`,
|
title: 'Login' + ` — ${org.name}`,
|
||||||
|
|
@ -21,7 +21,7 @@ export async function generateMetadata(
|
||||||
|
|
||||||
const Login = async (params: any) => {
|
const Login = async (params: any) => {
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug;
|
||||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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 { Metadata } from "next";
|
||||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
||||||
|
import SignUpClient from "./signup";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string, courseid: string };
|
params: { orgslug: string, courseid: string };
|
||||||
|
|
@ -14,7 +15,7 @@ export async function generateMetadata(
|
||||||
): Promise<Metadata> {
|
): Promise<Metadata> {
|
||||||
const orgslug = params.orgslug;
|
const orgslug = params.orgslug;
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: 'Sign up' + ` — ${org.name}`,
|
title: 'Sign up' + ` — ${org.name}`,
|
||||||
|
|
@ -23,12 +24,14 @@ export async function generateMetadata(
|
||||||
|
|
||||||
const SignUp = async (params: any) => {
|
const SignUp = async (params: any) => {
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug;
|
||||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<SignUpClient org={org}></SignUpClient>
|
<Suspense fallback={<PageLoading/>}>
|
||||||
</div>
|
<SignUpClient org={org} />
|
||||||
|
</Suspense>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default SignUp;
|
export default SignUp;
|
||||||
|
|
|
||||||
|
|
@ -1,117 +1,62 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { useFormik } from 'formik';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import learnhouseIcon from "public/learnhouse_bigicon_1.png";
|
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 Image from 'next/image';
|
||||||
import * as Form from '@radix-ui/react-form';
|
|
||||||
import { getOrgLogoMediaDirectory } from '@services/media/media';
|
import { getOrgLogoMediaDirectory } from '@services/media/media';
|
||||||
import { AlertTriangle, Check, User } from 'lucide-react';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { signup } from '@services/auth/auth';
|
|
||||||
import { getUriWithOrg } from '@services/config/config';
|
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 {
|
interface SignUpClientProps {
|
||||||
org: any;
|
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) {
|
function SignUpClient(props: SignUpClientProps) {
|
||||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
const session = useSession() as any;
|
||||||
const router = useRouter();
|
const [joinMethod, setJoinMethod] = React.useState('open');
|
||||||
const [error, setError] = React.useState('');
|
const [inviteCode, setInviteCode] = React.useState('');
|
||||||
const [message, setMessage] = React.useState('');
|
const searchParams = useSearchParams()
|
||||||
const formik = useFormik({
|
const inviteCodeParam = searchParams.get('inviteCode')
|
||||||
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);
|
|
||||||
|
|
||||||
}
|
|
||||||
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 (
|
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="right-login-part" style={{ background: "linear-gradient(041.61deg, #202020 7.15%, #000000 90.96%)" }} >
|
||||||
<div className='login-topbar m-10'>
|
<div className='login-topbar m-10'>
|
||||||
<Link prefetch href={getUriWithOrg(props.org.slug, "/")}>
|
<Link prefetch href={getUriWithOrg(props.org.slug, "/")}>
|
||||||
<Image quality={100} width={30} height={30} src={learnhouseIcon} alt="" />
|
<Image quality={100} width={30} height={30} src={learnhouseIcon} alt="" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</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 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)]" >
|
<div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]" >
|
||||||
{props.org?.logo ? (
|
{props.org?.logo_image ? (
|
||||||
<img
|
<img
|
||||||
src={`${getOrgLogoMediaDirectory(props.org.org_id, props.org?.logo)}`}
|
src={`${getOrgLogoMediaDirectory(props.org.org_uuid, props.org?.logo_image)}`}
|
||||||
alt="Learnhouse"
|
alt="Learnhouse"
|
||||||
style={{ width: "auto", height: 70 }}
|
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="" />
|
<Image quality={100} width={70} height={70} src={learnhouseIcon} alt="" />
|
||||||
|
|
@ -121,70 +66,113 @@ function SignUpClient(props: SignUpClientProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="left-login-part bg-white flex flex-row">
|
<div className="left-join-part bg-white flex flex-row">
|
||||||
<div className="login-form m-auto w-72">
|
{joinMethod == 'open' && (
|
||||||
{error && (
|
session.isAuthenticated ? <LoggedInJoinScreen inviteCode={inviteCode} /> : <OpenSignUpComponent />
|
||||||
<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} />
|
{joinMethod == 'inviteOnly' && (
|
||||||
<div className="font-bold text-sm">{error}</div>
|
inviteCode ? (
|
||||||
</div>
|
session.isAuthenticated ? <LoggedInJoinScreen /> : <InviteOnlySignUpComponent inviteCode={inviteCode} />
|
||||||
)}
|
) : <NoTokenScreen />
|
||||||
{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>
|
</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";
|
"use client";
|
||||||
import type { NextPage } from "next";
|
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import learnhouseBigIcon from "public/learnhouse_bigicon.png";
|
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 { getAPIUrl } from '@services/config/config';
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
import { swrFetcher } from '@services/utils/ts/requests';
|
||||||
import React, { createContext, useContext, useEffect, useReducer } from 'react'
|
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 CourseContext = createContext(null) as any;
|
||||||
export const CourseDispatchContext = createContext(null) as any;
|
export const CourseDispatchContext = createContext(null) as any;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { getNewAccessTokenUsingRefreshToken, getUserSession } from '@services/auth/auth';
|
import { getNewAccessTokenUsingRefreshToken, getUserSession } from '@services/auth/auth';
|
||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
|
||||||
import React, { useContext, createContext, useEffect } from 'react'
|
import React, { useContext, createContext, useEffect } from 'react'
|
||||||
import { useOrg } from './OrgContext';
|
|
||||||
|
|
||||||
export const SessionContext = createContext({}) as any;
|
export const SessionContext = createContext({}) as any;
|
||||||
|
|
||||||
const PATHS_THAT_REQUIRE_AUTH = ['/dash'];
|
|
||||||
|
|
||||||
type Session = {
|
type Session = {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
user: any;
|
user: any;
|
||||||
|
|
@ -18,10 +14,6 @@ type Session = {
|
||||||
|
|
||||||
function SessionProvider({ children }: { children: React.ReactNode }) {
|
function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [session, setSession] = React.useState<Session>({ access_token: "", user: {}, roles: {}, isLoading: true, isAuthenticated: false });
|
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() {
|
async function getNewAccessTokenUsingRefreshTokenUI() {
|
||||||
let data = await getNewAccessTokenUsingRefreshToken();
|
let data = await getNewAccessTokenUsingRefreshToken();
|
||||||
|
|
@ -39,6 +31,10 @@ function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||||
// Set session
|
// Set session
|
||||||
setSession({ access_token: access_token, user: user_session.user, roles: user_session.roles, isLoading: false, isAuthenticated: true });
|
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
|
// Check session
|
||||||
checkSession();
|
checkSession();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import * as Switch from '@radix-ui/react-switch';
|
||||||
import * as Form from '@radix-ui/react-form';
|
import * as Form from '@radix-ui/react-form';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useCourse, useCourseDispatch } from '../../../Contexts/CourseContext';
|
import { useCourse, useCourseDispatch } from '../../../Contexts/CourseContext';
|
||||||
|
import ThumbnailUpdate from './ThumbnailUpdate';
|
||||||
|
|
||||||
|
|
||||||
type EditCourseStructureProps = {
|
type EditCourseStructureProps = {
|
||||||
|
|
@ -84,71 +85,80 @@ function EditCourseGeneral(props: EditCourseStructureProps) {
|
||||||
}, [course, formik.values, formik.initialValues]);
|
}, [course, formik.values, formik.initialValues]);
|
||||||
|
|
||||||
return (
|
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 && (
|
{course.courseStructure && (
|
||||||
<div className="editcourse-form">
|
<div className="editcourse-form">
|
||||||
{error && (
|
{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">
|
<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} />
|
<AlertTriangle size={18} />
|
||||||
<div className="font-bold text-sm">{error}</div>
|
<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>
|
|
||||||
</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>
|
<FormField name="description">
|
||||||
</div>
|
<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>
|
</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 { createActivity, createExternalVideoActivity, createFileActivity } from '@services/courses/activities';
|
||||||
import { getOrganizationContextInfoWithoutCredentials } from '@services/organizations/orgs';
|
import { getOrganizationContextInfoWithoutCredentials } from '@services/organizations/orgs';
|
||||||
import { revalidateTags } from '@services/utils/ts/requests';
|
import { revalidateTags } from '@services/utils/ts/requests';
|
||||||
import { Layers, Sparkles } from 'lucide-react'
|
import { Layers } from 'lucide-react'
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import React, { use, useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { mutate } from 'swr';
|
import { mutate } from 'swr';
|
||||||
|
|
||||||
type NewActivityButtonProps = {
|
type NewActivityButtonProps = {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
|
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 React from 'react'
|
||||||
import ActivitiyElement from './ActivityElement';
|
|
||||||
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
||||||
import ActivityElement from './ActivityElement';
|
import ActivityElement from './ActivityElement';
|
||||||
import NewActivity from '../Buttons/NewActivityButton';
|
|
||||||
import NewActivityButton from '../Buttons/NewActivityButton';
|
import NewActivityButton from '../Buttons/NewActivityButton';
|
||||||
import { deleteChapter, updateChapter } from '@services/courses/chapters';
|
import { deleteChapter, updateChapter } from '@services/courses/chapters';
|
||||||
import { revalidateTags } from '@services/utils/ts/requests';
|
import { revalidateTags } from '@services/utils/ts/requests';
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config';
|
||||||
import { revalidateTags, swrFetcher } from '@services/utils/ts/requests';
|
import { revalidateTags } from '@services/utils/ts/requests';
|
||||||
import React, { useContext, useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
|
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||||
import useSWR, { mutate } from 'swr';
|
import { mutate } from 'swr';
|
||||||
import ChapterElement from './DraggableElements/ChapterElement';
|
import ChapterElement from './DraggableElements/ChapterElement';
|
||||||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
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 { useRouter } from 'next/navigation';
|
||||||
import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext';
|
import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext';
|
||||||
import { Hexagon } from 'lucide-react';
|
import { Hexagon } from 'lucide-react';
|
||||||
|
|
@ -92,6 +92,7 @@ const EditCourseStructure = (props: EditCourseStructureProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
|
<div className="h-6"></div>
|
||||||
{winReady ?
|
{winReady ?
|
||||||
<DragDropContext onDragEnd={updateStructure}>
|
<DragDropContext onDragEnd={updateStructure}>
|
||||||
<Droppable type='chapter' droppableId='chapters'>
|
<Droppable type='chapter' droppableId='chapters'>
|
||||||
|
|
@ -129,7 +130,7 @@ const EditCourseStructure = (props: EditCourseStructureProps) => {
|
||||||
dialogTitle="Create chapter"
|
dialogTitle="Create chapter"
|
||||||
dialogDescription="Add a new chapter to the course"
|
dialogDescription="Add a new chapter to the course"
|
||||||
dialogTrigger={
|
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'>
|
<div className='mx-auto flex space-x-2 items-center hover:cursor-pointer'>
|
||||||
<Hexagon strokeWidth={3} size={16} className="text-white text-sm " />
|
<Hexagon strokeWidth={3} size={16} className="text-white text-sm " />
|
||||||
<div className='font-bold text-sm'>Add Chapter</div></div>
|
<div className='font-bold text-sm'>Add Chapter</div></div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
import React, { use, useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import { updateOrganization, uploadOrganizationLogo } from '@services/settings/org';
|
import { updateOrganization, uploadOrganizationLogo } from '@services/settings/org';
|
||||||
import { UploadCloud } from 'lucide-react';
|
import { UploadCloud } from 'lucide-react';
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { useCourse } from '@components/Contexts/CourseContext'
|
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 Link from 'next/link'
|
||||||
import React, { use, useEffect } from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
type BreadCrumbsProps = {
|
type BreadCrumbsProps = {
|
||||||
type: 'courses' | 'user' | 'users' | 'org'
|
type: 'courses' | 'user' | 'users' | 'org' | 'orgusers'
|
||||||
last_breadcrumb?: string
|
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='text-gray-400 tracking-tight font-medium text-sm flex space-x-1'>
|
||||||
<div className='flex items-center 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 == '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> : ''}
|
{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'>
|
<div className='flex items-center space-x-1 first-letter:uppercase'>
|
||||||
{props.last_breadcrumb ? <ChevronRight size={17} /> : ''}
|
{props.last_breadcrumb ? <ChevronRight size={17} /> : ''}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { getUriWithOrg } from "@services/config/config";
|
||||||
import { useOrg } from "@components/Contexts/OrgContext";
|
import { useOrg } from "@components/Contexts/OrgContext";
|
||||||
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
import EmptyThumbnailImage from '../../../public/empty_thumbnail.png';
|
||||||
|
|
||||||
export function CourseOverviewTop({ params }: { params: CourseOverviewParams }) {
|
export function CourseOverviewTop({ params }: { params: CourseOverviewParams }) {
|
||||||
const course = useCourse() as any;
|
const course = useCourse() as any;
|
||||||
|
|
@ -21,7 +23,10 @@ export function CourseOverviewTop({ params }: { params: CourseOverviewParams })
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<div className='flex py-5 grow items-center'>
|
<div className='flex py-5 grow items-center'>
|
||||||
<Link href={getUriWithOrg(org?.slug, "") + `/course/${params.courseuuid}`}>
|
<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="" />
|
<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>
|
</Link>
|
||||||
<div className="flex flex-col course_metadata justify-center pl-5">
|
<div className="flex flex-col course_metadata justify-center pl-5">
|
||||||
<div className='text-gray-400 font-semibold text-sm'>Course</div>
|
<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 ToolTip from '@components/StyledElements/Tooltip/Tooltip'
|
||||||
import LearnHouseDashboardLogo from '@public/dashLogo.png';
|
import LearnHouseDashboardLogo from '@public/dashLogo.png';
|
||||||
import { logout } from '@services/auth/auth';
|
import { logout } from '@services/auth/auth';
|
||||||
import Avvvatars from 'avvvatars-react';
|
import { BookCopy, Home, LogOut, School, Settings, Users } from 'lucide-react'
|
||||||
import { ArrowLeft, Book, BookCopy, Home, LogOut, School, Settings } from 'lucide-react'
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/navigation';
|
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() {
|
function LeftMenu() {
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any;
|
||||||
|
|
@ -42,8 +43,8 @@ function LeftMenu() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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" }}
|
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-28 bg-black h-screen text-white shadow-xl'>
|
className='flex flex-col w-[90px] bg-black h-screen text-white shadow-xl'>
|
||||||
<div className='flex flex-col h-full'>
|
<div className='flex flex-col h-full'>
|
||||||
<div className='flex h-20 mt-6'>
|
<div className='flex h-20 mt-6'>
|
||||||
<Link className='flex flex-col items-center mx-auto space-y-3' href={"/"}>
|
<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' >
|
{/* <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>
|
<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> */}
|
||||||
<ToolTip content={"Home"} slateBlack sideOffset={8} side='right' >
|
<AdminAuthorization authorizationMode="component">
|
||||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash`} ><Home size={18} /></Link>
|
<ToolTip content={"Home"} slateBlack sideOffset={8} side='right' >
|
||||||
</ToolTip>
|
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash`} ><Home size={18} /></Link>
|
||||||
<ToolTip content={"Courses"} slateBlack sideOffset={8} side='right' >
|
</ToolTip>
|
||||||
<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 content={"Courses"} slateBlack sideOffset={8} side='right' >
|
||||||
</ToolTip>
|
<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 content={"Organization"} slateBlack sideOffset={8} side='right' >
|
</ToolTip>
|
||||||
<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 content={"Users"} slateBlack sideOffset={8} side='right' >
|
||||||
</ToolTip>
|
<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>
|
||||||
<div className='flex flex-col mx-auto pb-7 space-y-2'>
|
<div className='flex flex-col mx-auto pb-7 space-y-2'>
|
||||||
|
|
||||||
<div className="flex items-center flex-col space-y-2">
|
<div className="flex items-center flex-col space-y-2">
|
||||||
<ToolTip content={session.user.username} slateBlack sideOffset={8} side='right' >
|
<ToolTip content={'@' + session.user.username} slateBlack sideOffset={8} side='right' >
|
||||||
<div className="mx-auto shadow-lg">
|
<div className='mx-auto'>
|
||||||
<Avvvatars radius={3} border borderColor='white' borderSize={3} size={35} value={session.user.user_uuid} style="shape" />
|
<UserAvatar border='border-4' width={35} />
|
||||||
</div>
|
</div>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
<div className='flex items-center flex-col space-y-1'>
|
<div className='flex items-center flex-col space-y-1'>
|
||||||
<ToolTip content={session.user.username + "'s Settings"} slateBlack sideOffset={8} side='right' >
|
<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} />
|
<Settings className='mx-auto text-neutral-400 cursor-pointer' size={18} />
|
||||||
</Link>
|
</Link>
|
||||||
</ToolTip>
|
</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 { useSession } from '@components/Contexts/SessionContext';
|
||||||
import { updatePassword } from '@services/settings/password';
|
import { updatePassword } from '@services/settings/password';
|
||||||
import { Formik, Form, Field, ErrorMessage } from 'formik';
|
import { Formik, Form, Field } from 'formik';
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
function UserEditPassword() {
|
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 { useSession } from '@components/Contexts/SessionContext'
|
||||||
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
|
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
|
||||||
import { AlertTriangle, BadgeInfo, NotebookTabs } from 'lucide-react';
|
import { AlertTriangle, BadgeInfo, NotebookTabs } from 'lucide-react';
|
||||||
import Avvvatars from 'avvvatars-react';
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
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 Image from 'next/image';
|
||||||
import { send } from 'process';
|
|
||||||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
||||||
import learnhouseAI_logo_black from "public/learnhouse_ai_black_logo.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 { 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 useGetAIFeatures from '../../../AI/Hooks/useGetAIFeatures';
|
||||||
|
import UserAvatar from '@components/Objects/UserAvatar';
|
||||||
|
|
||||||
|
|
||||||
type AIActivityAskProps = {
|
type AIActivityAskProps = {
|
||||||
|
|
@ -172,7 +169,7 @@ function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
|
||||||
</div>
|
</div>
|
||||||
<div className='bg-white/5 text-white/40 py-0.5 px-3 flex space-x-1 rounded-full items-center'>
|
<div className='bg-white/5 text-white/40 py-0.5 px-3 flex space-x-1 rounded-full items-center'>
|
||||||
<FlaskConical size={14} />
|
<FlaskConical size={14} />
|
||||||
<span className='text-xs font-semibold '>Experimental</span>
|
<span className='text-xs font-semibold antialiased '>Experimental</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -204,7 +201,7 @@ function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
|
||||||
}
|
}
|
||||||
<div className='flex space-x-2 items-center'>
|
<div className='flex space-x-2 items-center'>
|
||||||
<div className=''>
|
<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>
|
||||||
<div className='w-full'>
|
<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="" />
|
<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 (
|
return (
|
||||||
<div className='flex space-x-2 w-full antialiased font-medium'>
|
<div className='flex space-x-2 w-full antialiased font-medium'>
|
||||||
<div className=''>
|
<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>
|
||||||
<div className='w-full'>
|
<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="">
|
<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="" />
|
<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'>
|
<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='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>{session.user.username},</span>
|
||||||
</span>
|
</span>
|
||||||
<span>how can we help today ?</span>
|
<span>how can we help today ?</span>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { useOrg } from "@components/Contexts/OrgContext";
|
import { useOrg } from "@components/Contexts/OrgContext";
|
||||||
import { getBackendUrl } from "@services/config/config";
|
|
||||||
import { getActivityMediaDirectory } from "@services/media/media";
|
import { getActivityMediaDirectory } from "@services/media/media";
|
||||||
import React from "react";
|
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 StarterKit from "@tiptap/starter-kit";
|
||||||
import styled from "styled-components"
|
import styled from "styled-components"
|
||||||
import Youtube from "@tiptap/extension-youtube";
|
import Youtube from "@tiptap/extension-youtube";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import { getBackendUrl } from "@services/config/config";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
|
||||||
import YouTube from 'react-youtube';
|
import YouTube from 'react-youtube';
|
||||||
import { getActivityMediaDirectory } from "@services/media/media";
|
import { getActivityMediaDirectory } from "@services/media/media";
|
||||||
import { useOrg } from "@components/Contexts/OrgContext";
|
import { useOrg } from "@components/Contexts/OrgContext";
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@ import React from 'react'
|
||||||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import Image from 'next/image';
|
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 { Editor } from '@tiptap/react';
|
||||||
import { AIChatBotStateTypes, useAIChatBot, useAIChatBotDispatch } from '@components/Contexts/AI/AIChatBotContext';
|
|
||||||
import { AIEditorStateTypes, useAIEditor, useAIEditorDispatch } from '@components/Contexts/AI/AIEditorContext';
|
import { AIEditorStateTypes, useAIEditor, useAIEditorDispatch } from '@components/Contexts/AI/AIEditorContext';
|
||||||
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
|
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
|
||||||
import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures';
|
import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures';
|
||||||
|
|
@ -57,7 +56,7 @@ function AIEditorToolkit(props: AIEditorToolkitProps) {
|
||||||
<div className='pr-1'>
|
<div className='pr-1'>
|
||||||
<div className='flex w-full space-x-2 font-bold text-white/80 items-center'>
|
<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="" />
|
<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} />
|
<MoreVertical className='text-white/50' size={12} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useEditor, EditorContent, BubbleMenu } from "@tiptap/react";
|
import { useEditor, EditorContent } from "@tiptap/react";
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
import learnhouseIcon from "public/learnhouse_icon.png";
|
import learnhouseIcon from "public/learnhouse_icon.png";
|
||||||
import { ToolbarButtons } from "./Toolbar/ToolbarButtons";
|
import { ToolbarButtons } from "./Toolbar/ToolbarButtons";
|
||||||
|
|
@ -8,7 +8,6 @@ import { motion } from "framer-motion";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { DividerVerticalIcon, SlashIcon } from "@radix-ui/react-icons";
|
import { DividerVerticalIcon, SlashIcon } from "@radix-ui/react-icons";
|
||||||
import Avvvatars from "avvvatars-react";
|
|
||||||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
||||||
import { AIEditorStateTypes, useAIEditor, useAIEditorDispatch } from "@components/Contexts/AI/AIEditorContext";
|
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 { useSession } from "@components/Contexts/SessionContext";
|
||||||
import AIEditorToolkit from "./AI/AIEditorToolkit";
|
import AIEditorToolkit from "./AI/AIEditorToolkit";
|
||||||
import useGetAIFeatures from "@components/AI/Hooks/useGetAIFeatures";
|
import useGetAIFeatures from "@components/AI/Hooks/useGetAIFeatures";
|
||||||
|
import UserAvatar from "../UserAvatar";
|
||||||
|
|
||||||
|
|
||||||
interface Editor {
|
interface Editor {
|
||||||
|
|
@ -163,7 +163,7 @@ function Editor(props: Editor) {
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" />
|
<EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" />
|
||||||
</Link>
|
</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>
|
<EditorInfoThumbnail src={`${getCourseThumbnailMediaDirectory(props.org?.org_uuid, props.course.course_uuid, props.course.thumbnail_image)}`} alt=""></EditorInfoThumbnail>
|
||||||
</Link>
|
</Link>
|
||||||
<EditorInfoDocName>
|
<EditorInfoDocName>
|
||||||
|
|
@ -207,7 +207,7 @@ function Editor(props: Editor) {
|
||||||
|
|
||||||
<EditorUserProfileWrapper>
|
<EditorUserProfileWrapper>
|
||||||
{!session.isAuthenticated && <span>Loading</span>}
|
{!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>
|
</EditorUserProfileWrapper>
|
||||||
|
|
||||||
</EditorUsersSection>
|
</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