Merge pull request #260 from learnhouse/feat/use-next-auth

New Auth Mechanism + Google OAuth
This commit is contained in:
Badr B 2024-06-07 15:12:21 +01:00 committed by GitHub
commit ddbd413539
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
140 changed files with 2833 additions and 1919 deletions

3
.npmrc
View file

@ -1 +1,2 @@
shared-workspace-lockfile=false
shared-workspace-lockfile=false
package-manager-strict=false

View file

@ -1,11 +1,14 @@
from datetime import timedelta
from typing import Literal, Optional
from fastapi import Depends, APIRouter, HTTPException, Response, status, Request
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel, EmailStr
from sqlmodel import Session
from src.db.users import UserRead
from src.db.users import AnonymousUser, UserRead
from src.core.events.database import get_db_session
from config.config import get_learnhouse_config
from src.security.auth import AuthJWT, authenticate_user
from src.security.auth import AuthJWT, authenticate_user, get_current_user
from src.services.auth.utils import signWithGoogle
router = APIRouter()
@ -74,6 +77,58 @@ async def login(
return result
class ThirdPartyLogin(BaseModel):
email: EmailStr
provider: Literal["google"]
access_token: str
@router.post("/oauth")
async def third_party_login(
request: Request,
response: Response,
body: ThirdPartyLogin,
org_id: Optional[int] = None,
current_user: AnonymousUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
Authorize: AuthJWT = Depends(),
):
# Google
if body.provider == "google":
user = await signWithGoogle(
request, body.access_token, body.email, org_id, current_user, db_session
)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect Email or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = Authorize.create_access_token(subject=user.email)
refresh_token = Authorize.create_refresh_token(subject=user.email)
Authorize.set_refresh_cookies(refresh_token)
# set cookies using fastapi
response.set_cookie(
key="access_token_cookie",
value=access_token,
httponly=False,
domain=get_learnhouse_config().hosting_config.cookie_config.domain,
expires=int(timedelta(hours=8).total_seconds()),
)
user = UserRead.model_validate(user)
result = {
"user": user,
"tokens": {"access_token": access_token, "refresh_token": refresh_token},
}
return result
@router.delete("/logout")
def logout(Authorize: AuthJWT = Depends()):
"""

View file

@ -8,6 +8,7 @@ from src.services.orgs.invites import (
get_invite_code,
get_invite_codes,
)
from src.services.orgs.join import JoinOrg, join_org
from src.services.orgs.users import (
get_list_of_invited_users,
get_organization_users,
@ -99,6 +100,19 @@ async def api_get_org_users(
return await get_organization_users(request, org_id, db_session, current_user)
@router.post("/join")
async def api_join_an_org(
request: Request,
args: JoinOrg,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Get single Org by ID
"""
return await join_org(request, args, current_user, db_session)
@router.put("/{org_id}/users/{user_id}/role/{role_uuid}")
async def api_update_user_role(
request: Request,

View file

@ -85,7 +85,6 @@ async def api_create_user_with_orgid(
"""
Create User with 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 (

View file

@ -0,0 +1,70 @@
import random
from typing import Optional
from fastapi import Depends, HTTPException, Request
import httpx
from sqlmodel import Session, select
from src.core.events.database import get_db_session
from src.db.users import User, UserCreate, UserRead
from src.security.auth import get_current_user
from src.services.users.users import create_user, create_user_without_org
async def get_google_user_info(access_token: str):
url = "https://www.googleapis.com/oauth2/v3/userinfo"
headers = {"Authorization": f"Bearer {access_token}"}
async with httpx.AsyncClient() as client:
response = await client.get(url, headers=headers)
if response.status_code != 200:
raise HTTPException(
status_code=response.status_code,
detail="Failed to fetch user info from Google",
)
return response.json()
async def signWithGoogle(
request: Request,
access_token: str,
email: str,
org_id: Optional[int] = None,
current_user=Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
# Google
google_user = await get_google_user_info(access_token)
user = db_session.exec(
select(User).where(User.email == google_user["email"])
).first()
if not user:
username = (
google_user["given_name"]
+ google_user["family_name"]
+ str(random.randint(10, 99))
)
user_object = UserCreate(
email=google_user["email"],
username=username,
password="",
first_name=google_user["given_name"],
last_name=google_user["family_name"],
avatar_image=google_user["picture"],
)
if org_id is not None:
user = await create_user(
request, db_session, current_user, user_object, org_id
)
return user
else:
user = await create_user_without_org(
request, db_session, current_user, user_object
)
return user
return UserRead.model_validate(user)

View file

@ -0,0 +1,119 @@
from datetime import datetime
from typing import Optional
from fastapi import HTTPException, Request
from pydantic import BaseModel
from sqlmodel import Session, select
from src.db.organizations import Organization
from src.db.user_organizations import UserOrganization
from src.db.users import AnonymousUser, PublicUser, User
from src.services.orgs.invites import get_invite_code
from src.services.orgs.orgs import get_org_join_mechanism
class JoinOrg(BaseModel):
org_id: int
user_id: str
invite_code: Optional[str]
async def join_org(
request: Request,
args: JoinOrg,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Organization).where(Organization.id == args.org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
join_method = await get_org_join_mechanism(
request, args.org_id, current_user, db_session
)
# Get User
statement = select(User).where(User.id == args.user_id)
result = db_session.exec(statement)
user = result.first()
# Check if User isn't already part of the org
statement = select(UserOrganization).where(
UserOrganization.user_id == args.user_id, UserOrganization.org_id == args.org_id
)
result = db_session.exec(statement)
userorg = result.first()
if userorg:
raise HTTPException(
status_code=400, detail="User is already part of that organization"
)
if join_method == "inviteOnly" and user and org and args.invite_code:
if user.id is not None and org.id is not None:
# Check if invite code exists
inviteCode = await get_invite_code(
request, org.id, args.invite_code, current_user, db_session
)
if not inviteCode:
raise HTTPException(
status_code=400,
detail="Invite code is incorrect",
)
# Link user and organization
user_organization = UserOrganization(
user_id=user.id,
org_id=org.id,
role_id=3,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
db_session.add(user_organization)
db_session.commit()
return "Great, You're part of the Organization"
else:
raise HTTPException(
status_code=403,
detail="Something wrong, try later.",
)
if join_method == "open" and user and org:
if user.id is not None and org.id is not None:
# Link user and organization
user_organization = UserOrganization(
user_id=user.id,
org_id=org.id,
role_id=3,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
db_session.add(user_organization)
db_session.commit()
return "Great, You're part of the Organization"
else:
raise HTTPException(
status_code=403,
detail="Something wrong, try later.",
)
else:
raise HTTPException(
status_code=403,
detail="Something wrong, try later.",
)

View file

@ -436,8 +436,7 @@ async def get_orgs_by_user(
orgs = result.all()
return orgs
return orgs #type:ignore
# Config related
async def update_org_signup_mechanism(

View file

@ -17,7 +17,7 @@ def send_account_creation_email(
<body>
<p>Hello {user.username}</p>
<p>Welcome to LearnHouse! , get started by creating your own organization or join a one.</p>
<p>Need some help to get started ? <a href="https://learn.learnhouse.io">LearnHouse Academy</a></p>
<p>Need some help to get started ? <a href="https://university.learnhouse.io">LearnHouse Academy</a></p>
</body>
</html>
""",

View file

@ -10,7 +10,7 @@ importers:
dependencies:
'@hocuspocus/server':
specifier: ^2.11.3
version: 2.11.3(y-protocols@1.0.6)(yjs@13.6.14)
version: 2.11.3(y-protocols@1.0.6(yjs@13.6.14))(yjs@13.6.14)
bun:
specifier: ^1.0.36
version: 1.1.1
@ -80,7 +80,6 @@ packages:
bun@1.1.1:
resolution: {integrity: sha512-gV90TkJgHvI50X9BoKQ3zVpPEY6YP0vqOww2uZmsOyckZSRlcFYWhXZwFj6PV8KCFINYs8VZ65m59U2RuFYfWw==}
cpu: [arm64, x64]
os: [darwin, linux, win32]
hasBin: true
@ -133,7 +132,7 @@ snapshots:
dependencies:
lib0: 0.2.93
'@hocuspocus/server@2.11.3(y-protocols@1.0.6)(yjs@13.6.14)':
'@hocuspocus/server@2.11.3(y-protocols@1.0.6(yjs@13.6.14))(yjs@13.6.14)':
dependencies:
'@hocuspocus/common': 2.11.3
async-lock: 1.4.1

View file

@ -4,7 +4,8 @@
"react/no-unescaped-entities": "off",
"@next/next/no-page-custom-font": "off",
"@next/next/no-img-element": "off",
"unused-imports/no-unused-imports": "warn"
"unused-imports/no-unused-imports": "warn",
"no-console": "warn"
},
"plugins": ["unused-imports"]
}

2
apps/web/.gitignore vendored
View file

@ -43,3 +43,5 @@ next.config.original.js
# Sentry Config File
.sentryclirc
certificates

View file

@ -0,0 +1,6 @@
import NextAuth from 'next-auth'
import { nextAuthOptions } from 'app/auth/options'
const handler = NextAuth(nextAuthOptions)
export { handler as GET, handler as POST }

View file

@ -0,0 +1,19 @@
'use client'
import { OrgProvider } from '@components/Contexts/OrgContext'
import ErrorUI from '@components/StyledElements/Error/Error'
import { useSearchParams } from 'next/navigation'
export default function AuthLayout({
children,
}: {
children: React.ReactNode
}) {
const searchParams = useSearchParams()
const orgslug = searchParams.get('orgslug')
if (orgslug) {
return <OrgProvider orgslug={orgslug}>{children}</OrgProvider>
} else {
return <ErrorUI message='Organization not specified' submessage='Please access this page from an Organization' />
}
}

View file

@ -10,11 +10,12 @@ import * as Form from '@radix-ui/react-form'
import { useFormik } from 'formik'
import { getOrgLogoMediaDirectory } from '@services/media/media'
import React from 'react'
import { loginAndGetToken } from '@services/auth/auth'
import { AlertTriangle } from 'lucide-react'
import { AlertTriangle, UserRoundPlus } from 'lucide-react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { getUriWithOrg } from '@services/config/config'
import { signIn } from "next-auth/react"
import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config'
import { useLHSession } from '@components/Contexts/LHSessionContext'
interface LoginClientProps {
org: any
@ -40,7 +41,9 @@ const validate = (values: any) => {
const LoginClient = (props: LoginClientProps) => {
const [isSubmitting, setIsSubmitting] = React.useState(false)
const router = useRouter()
const router = useRouter();
const session = useLHSession() as any;
const [error, setError] = React.useState('')
const formik = useFormik({
initialValues: {
@ -50,25 +53,25 @@ const LoginClient = (props: LoginClientProps) => {
validate,
onSubmit: async (values) => {
setIsSubmitting(true)
let res = await loginAndGetToken(values.email, values.password)
let message = await res.json()
if (res.status == 200) {
router.push(`/`)
setIsSubmitting(false)
} else if (
res.status == 401 ||
res.status == 400 ||
res.status == 404 ||
res.status == 409
) {
setError(message.detail)
setIsSubmitting(false)
const res = await signIn('credentials', {
redirect: false,
email: values.email,
password: values.password,
callbackUrl: '/redirect_from_auth'
});
if (res && res.error) {
setError("Wrong Email or password");
setIsSubmitting(false);
} else {
setError('Something went wrong')
setIsSubmitting(false)
await signIn('credentials', {
email: values.email,
password: values.password,
callbackUrl: '/redirect_from_auth'
});
}
},
})
return (
<div className="grid grid-flow-col justify-stretch h-screen">
<div
@ -165,7 +168,6 @@ const LoginClient = (props: LoginClientProps) => {
Forgot password?
</Link>
</div>
<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">
@ -174,6 +176,18 @@ const LoginClient = (props: LoginClientProps) => {
</Form.Submit>
</div>
</FormLayout>
<div className='flex h-0.5 rounded-2xl bg-slate-100 mt-5 mx-10'></div>
<div className='flex justify-center py-5 mx-auto'>OR </div>
<div className='flex flex-col space-y-4'>
<Link href={{ pathname: getUriWithoutOrg('/signup'), query: props.org.slug ? { orgslug: props.org.slug } : null }} className="flex justify-center items-center py-3 text-md w-full bg-gray-800 text-gray-300 space-x-3 font-semibold text-center p-2 rounded-md shadow hover:cursor-pointer">
<UserRoundPlus size={17} />
<span>Sign up</span>
</Link>
<button onClick={() => signIn('google', { callbackUrl: '/redirect_from_auth' })} className="flex justify-center py-3 text-md w-full bg-white text-slate-600 space-x-3 font-semibold text-center p-2 rounded-md shadow hover:cursor-pointer">
<img src="https://fonts.gstatic.com/s/i/productlogos/googleg/v6/24px.svg" alt="" />
<span>Sign in with Google</span>
</button>
</div>
</div>
</div>
</div>

View file

@ -3,14 +3,14 @@ import LoginClient from './login'
import { Metadata } from 'next'
type MetadataProps = {
params: { orgslug: string; courseid: string }
params: { orgslug: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const orgslug = params.orgslug
export async function generateMetadata(params: MetadataProps): Promise<Metadata> {
const orgslug = params.searchParams.orgslug
//const orgslug = params.orgslug
// Get Org context information
const org = await getOrganizationContextInfo(orgslug, {
revalidate: 0,
@ -22,8 +22,8 @@ export async function generateMetadata({
}
}
const Login = async (params: any) => {
const orgslug = params.params.orgslug
const Login = async (params: MetadataProps) => {
const orgslug = params.searchParams.orgslug
const org = await getOrganizationContextInfo(orgslug, {
revalidate: 0,
tags: ['organizations'],

View file

@ -0,0 +1,114 @@
import {
getNewAccessTokenUsingRefreshTokenServer,
getUserSession,
loginAndGetToken,
loginWithOAuthToken,
} from '@services/auth/auth'
import { LEARNHOUSE_TOP_DOMAIN, getUriWithOrg } from '@services/config/config'
import { getResponseMetadata } from '@services/utils/ts/requests'
import CredentialsProvider from 'next-auth/providers/credentials'
import GoogleProvider from 'next-auth/providers/google'
const isDevEnv = LEARNHOUSE_TOP_DOMAIN == 'localhost' ? true : false
export const nextAuthOptions = {
debug: true,
providers: [
CredentialsProvider({
// The name to display on the sign in form (e.g. 'Sign in with...')
name: 'Credentials',
// The credentials is used to generate a suitable form on the sign in page.
// You can specify whatever fields you are expecting to be submitted.
// e.g. domain, username, password, 2FA token, etc.
// You can pass any HTML attribute to the <input> tag through the object.
credentials: {
email: { label: 'Email', type: 'text', placeholder: 'jsmith' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials, req) {
// logic to verify if user exists
let unsanitized_req = await loginAndGetToken(
credentials?.email,
credentials?.password
)
let res = await getResponseMetadata(unsanitized_req)
if (res.success) {
// If login failed, then this is the place you could do a registration
return res.data
} else {
return null
}
},
}),
GoogleProvider({
clientId: process.env.LEARNHOUSE_GOOGLE_CLIENT_ID || '',
clientSecret: process.env.LEARNHOUSE_GOOGLE_CLIENT_SECRET || '',
}),
],
pages: {
signIn: getUriWithOrg('auth', '/'),
verifyRequest: getUriWithOrg('auth', '/'),
error: getUriWithOrg('auth', '/'), // Error code passed in query string as ?error=
},
cookies: {
sessionToken: {
name: `${!isDevEnv ? '__Secure-' : ''}next-auth.session-token`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
// When working on localhost, the cookie domain must be omitted entirely (https://stackoverflow.com/a/1188145)
domain: `.${LEARNHOUSE_TOP_DOMAIN}`,
secure: !isDevEnv,
},
},
},
callbacks: {
async jwt({ token, user, account }: any) {
// First sign in with Credentials provider
if (account?.provider == 'credentials' && user) {
token.user = user
}
// Sign up with Google
if (account?.provider == 'google' && user) {
let unsanitized_req = await loginWithOAuthToken(
user.email,
'google',
account.access_token
)
let userFromOAuth = await getResponseMetadata(unsanitized_req)
token.user = userFromOAuth.data
}
// Refresh token
// TODO : Improve this implementation
if (token?.user?.tokens) {
const RefreshedToken = await getNewAccessTokenUsingRefreshTokenServer(
token?.user?.tokens?.refresh_token
)
token = {
...token,
user: {
...token.user,
tokens: {
...token.user.tokens,
access_token: RefreshedToken.access_token,
},
},
}
}
return token
},
async session({ session, token }: any) {
// Include user information in the session
if (token.user) {
let api_SESSION = await getUserSession(token.user.tokens.access_token)
session.user = api_SESSION.user
session.roles = api_SESSION.roles
session.tokens = token.user.tokens
}
return session
},
},
}

View file

@ -15,7 +15,7 @@ import { getUriWithOrg } from '@services/config/config'
import { useOrg } from '@components/Contexts/OrgContext'
import { useRouter, useSearchParams } from 'next/navigation'
import { useFormik } from 'formik'
import { resetPassword, sendResetLink } from '@services/auth/auth'
import { resetPassword } from '@services/auth/auth'
const validate = (values: any) => {
const errors: any = {}

View file

@ -13,6 +13,7 @@ import { AlertTriangle, Check, User } from 'lucide-react'
import Link from 'next/link'
import { signUpWithInviteCode } from '@services/auth/auth'
import { useOrg } from '@components/Contexts/OrgContext'
import { signIn } from 'next-auth/react'
const validate = (values: any) => {
const errors: any = {}
@ -92,7 +93,7 @@ function InviteOnlySignUpComponent(props: InviteOnlySignUpProps) {
},
})
useEffect(() => {}, [org])
useEffect(() => { }, [org])
return (
<div className="login-form m-auto w-72">
@ -180,6 +181,13 @@ function InviteOnlySignUpComponent(props: InviteOnlySignUpProps) {
</Form.Submit>
</div>
</FormLayout>
<div>
<div className='flex h-0.5 rounded-2xl bg-slate-100 mt-5 mb-5 mx-10'></div>
<button onClick={() => signIn('google')} className="flex justify-center py-3 text-md w-full bg-white text-slate-600 space-x-3 font-semibold text-center p-2 rounded-md shadow hover:cursor-pointer">
<img src="https://fonts.gstatic.com/s/i/productlogos/googleg/v6/24px.svg" alt="" />
<span>Sign in with Google</span>
</button>
</div>
</div>
)
}

View file

@ -13,6 +13,7 @@ import { AlertTriangle, Check, User } from 'lucide-react'
import Link from 'next/link'
import { signup } from '@services/auth/auth'
import { useOrg } from '@components/Contexts/OrgContext'
import { signIn } from 'next-auth/react'
const validate = (values: any) => {
const errors: any = {}
@ -88,7 +89,7 @@ function OpenSignUpComponent() {
},
})
useEffect(() => {}, [org])
useEffect(() => { }, [org])
return (
<div className="login-form m-auto w-72">
@ -176,6 +177,13 @@ function OpenSignUpComponent() {
</Form.Submit>
</div>
</FormLayout>
<div>
<div className='flex h-0.5 rounded-2xl bg-slate-100 mt-5 mb-5 mx-10'></div>
<button onClick={() => signIn('google')} className="flex justify-center py-3 text-md w-full bg-white text-slate-600 space-x-3 font-semibold text-center p-2 rounded-md shadow hover:cursor-pointer">
<img src="https://fonts.gstatic.com/s/i/productlogos/googleg/v6/24px.svg" alt="" />
<span>Sign in with Google</span>
</button>
</div>
</div>
)
}

View file

@ -9,10 +9,10 @@ type MetadataProps = {
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const orgslug = params.orgslug
export async function generateMetadata(
params
: MetadataProps): Promise<Metadata> {
const orgslug = params.searchParams.orgslug
// Get Org context information
const org = await getOrganizationContextInfo(orgslug, {
revalidate: 0,
@ -25,7 +25,7 @@ export async function generateMetadata({
}
const SignUp = async (params: any) => {
const orgslug = params.params.orgslug
const orgslug = params.searchParams.orgslug
const org = await getOrganizationContextInfo(orgslug, {
revalidate: 0,
tags: ['organizations'],

View file

@ -4,9 +4,9 @@ import Image from 'next/image'
import { getOrgLogoMediaDirectory } from '@services/media/media'
import Link from 'next/link'
import { getUriWithOrg } from '@services/config/config'
import { useSession } from '@components/Contexts/SessionContext'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import React, { useEffect } from 'react'
import { MailWarning, Shield, Ticket, UserPlus } from 'lucide-react'
import { MailWarning, Ticket, UserPlus } from 'lucide-react'
import { useOrg } from '@components/Contexts/OrgContext'
import UserAvatar from '@components/Objects/UserAvatar'
import OpenSignUpComponent from './OpenSignup'
@ -16,13 +16,15 @@ 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'
import { BarLoader } from 'react-spinners'
import { joinOrg } from '@services/organizations/orgs'
interface SignUpClientProps {
org: any
}
function SignUpClient(props: SignUpClientProps) {
const session = useSession() as any
const session = useLHSession() as any
const [joinMethod, setJoinMethod] = React.useState('open')
const [inviteCode, setInviteCode] = React.useState('')
const searchParams = useSearchParams()
@ -33,9 +35,6 @@ function SignUpClient(props: SignUpClientProps) {
setJoinMethod(
props.org?.config?.config?.GeneralConfig.users.signup_mechanism
)
console.log(
props.org?.config?.config?.GeneralConfig.users.signup_mechanism
)
}
if (inviteCodeParam) {
setInviteCode(inviteCodeParam)
@ -72,7 +71,7 @@ function SignUpClient(props: SignUpClientProps) {
props.org.org_uuid,
props.org?.logo_image
)}`}
alt="Learnhouse"
alt="LearnHouse"
style={{ width: 'auto', height: 70 }}
className="rounded-xl shadow-xl inset-0 ring-1 ring-inset ring-black/10 bg-white"
/>
@ -92,15 +91,15 @@ function SignUpClient(props: SignUpClientProps) {
</div>
<div className="left-join-part bg-white flex flex-row">
{joinMethod == 'open' &&
(session.isAuthenticated ? (
(session.status == 'authenticated' ? (
<LoggedInJoinScreen inviteCode={inviteCode} />
) : (
<OpenSignUpComponent />
))}
{joinMethod == 'inviteOnly' &&
(inviteCode ? (
session.isAuthenticated ? (
<LoggedInJoinScreen />
session.status == 'authenticated' ? (
<LoggedInJoinScreen inviteCode={inviteCode} />
) : (
<InviteOnlySignUpComponent inviteCode={inviteCode} />
)
@ -113,9 +112,32 @@ function SignUpClient(props: SignUpClientProps) {
}
const LoggedInJoinScreen = (props: any) => {
const session = useSession() as any
const session = useLHSession() as any
const org = useOrg() as any
const invite_code = props.inviteCode
const [isLoading, setIsLoading] = React.useState(true)
const [isSumbitting, setIsSubmitting] = React.useState(false)
const router = useRouter()
const join = async () => {
setIsSubmitting(true)
const res = await joinOrg({ org_id: org.id, user_id: session?.data?.user?.id, invite_code: props.inviteCode }, null, session.data?.tokens?.access_token)
//wait for 1s
if (res.success) {
toast.success(
res.data
)
setTimeout(() => {
router.push(getUriWithOrg(org.slug,'/'))
}, 2000)
setIsSubmitting(false)
} else {
toast.error(res.data.detail)
setIsLoading(false)
setIsSubmitting(false)
}
}
useEffect(() => {
if (session && org) {
@ -125,18 +147,23 @@ const LoggedInJoinScreen = (props: any) => {
return (
<div className="flex flex-row items-center mx-auto">
<Toast />
<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>{session.data.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 onClick={() => join()} className="flex w-fit h-[35px] space-x-2 bg-black px-6 py-2 text-md rounded-lg font-semibold h-fit text-white items-center shadow-md">
{isSumbitting ? <BarLoader
cssOverride={{ borderRadius: 60 }}
width={60}
color="#ffffff"
/> : <><UserPlus size={18} />
<p>Join </p></>}
</button>
</div>
</div>
@ -144,7 +171,7 @@ const LoggedInJoinScreen = (props: any) => {
}
const NoTokenScreen = (props: any) => {
const session = useSession() as any
const session = useLHSession() as any
const org = useOrg() as any
const router = useRouter()
const [isLoading, setIsLoading] = React.useState(true)
@ -157,14 +184,14 @@ const NoTokenScreen = (props: any) => {
const validateCode = async () => {
setIsLoading(true)
let res = await validateInviteCode(org?.id, inviteCode)
let res = await validateInviteCode(org?.id, inviteCode, session?.user?.tokens.access_token)
//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}`)
router.push(`/signup?inviteCode=${inviteCode}&orgslug=${org.slug}`)
}, 2000)
} else {
toast.error('Invite code is invalid')

View file

@ -3,4 +3,4 @@ import PageLoading from '@components/Objects/Loaders/PageLoading'
export default function Loading() {
// Or a custom loading skeleton component
return <PageLoading></PageLoading>
}
}

View file

@ -1,14 +1,13 @@
import { default as React } from 'react'
import dynamic from 'next/dynamic'
import { getCourseMetadataWithAuthHeader } from '@services/courses/courses'
import { cookies } from 'next/headers'
import { getCourseMetadata } from '@services/courses/courses'
import { Metadata } from 'next'
import { getActivityWithAuthHeader } from '@services/courses/activities'
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
import { getOrganizationContextInfoWithId } from '@services/organizations/orgs'
import SessionProvider from '@components/Contexts/SessionContext'
import EditorOptionsProvider from '@components/Contexts/Editor/EditorContext'
import AIEditorProvider from '@components/Contexts/AI/AIEditorContext'
import { nextAuthOptions } from 'app/auth/options'
import { getServerSession } from 'next-auth'
const EditorWrapper = dynamic(() => import('@components/Objects/Editor/EditorWrapper'), { ssr: false })
@ -20,10 +19,10 @@ type MetadataProps = {
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const session = await getServerSession(nextAuthOptions)
const access_token = session?.tokens?.access_token
// Get Org context information
const course_meta = await getCourseMetadataWithAuthHeader(
const course_meta = await getCourseMetadata(
params.courseid,
{ revalidate: 0, tags: ['courses'] },
access_token ? access_token : null
@ -36,11 +35,11 @@ export async function generateMetadata({
}
const EditActivity = async (params: any) => {
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const session = await getServerSession(nextAuthOptions)
const access_token = session?.tokens?.access_token
const activityuuid = params.params.activityuuid
const courseid = params.params.courseid
const courseInfo = await getCourseMetadataWithAuthHeader(
const courseInfo = await getCourseMetadata(
courseid,
{ revalidate: 0, tags: ['courses'] },
access_token ? access_token : null
@ -53,19 +52,17 @@ const EditActivity = async (params: any) => {
const org = await getOrganizationContextInfoWithId(courseInfo.org_id, {
revalidate: 180,
tags: ['organizations'],
})
}, access_token)
return (
<EditorOptionsProvider options={{ isEditable: true }}>
<AIEditorProvider>
<SessionProvider>
<EditorWrapper
org={org}
course={courseInfo}
activity={activity}
content={activity.content}
></EditorWrapper>
</SessionProvider>
<EditorWrapper
org={org}
course={courseInfo}
activity={activity}
content={activity.content}
></EditorWrapper>
</AIEditorProvider>
</EditorOptionsProvider>
)

View file

@ -0,0 +1,57 @@
'use client'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import UserAvatar from '@components/Objects/UserAvatar';
import { getAPIUrl, getUriWithOrg, getUriWithoutOrg } from '@services/config/config';
import { swrFetcher } from '@services/utils/ts/requests';
import { ArrowRightCircle, Info } from 'lucide-react';
import { signOut } from 'next-auth/react';
import Image from 'next/image';
import Link from 'next/link';
import learnhouseIcon from 'public/learnhouse_bigicon_1.png'
import React, { useEffect } from 'react'
import useSWR from 'swr';
function HomeClient() {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const { data: orgs } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, (url) => swrFetcher(url, access_token))
useEffect(() => {
console.log(orgs)
}, [session, orgs])
return (
<div className='flex flex-col'>
<div className='flex space-x-4 mx-auto font-semibold text-3xl pt-16 items-center bg-black rounded-b-2xl'>
<Image
quality={100}
width={60}
height={60}
src={learnhouseIcon}
alt=""
/>
</div>
<div className='flex space-x-4 mx-auto font-semibold text-2xl pt-16 items-center'><span>Hello,</span> <UserAvatar /> <span className='capitalize'>{session?.data?.user.first_name} {session?.data?.user.last_name}</span></div>
<div className='flex space-x-4 mx-auto font-semibold text-sm mt-12 items-center uppercase bg-slate-200 text-gray-600 px-3 py-2 rounded-md'>Your Organizations</div>
{orgs && orgs.length == 0 && <div className='flex mx-auto my-5 space-x-3 bg-rose-200 rounded-lg px-3 py-2'>
<Info />
<span>It seems you're not part of an organization yet, join one to be able to see it here </span>
</div>}
<div className='flex mx-auto pt-10 rounded-lg'>
{orgs && orgs.map((org: any) => (
<Link href={getUriWithOrg(org.slug, '/')} key={org.id} className='flex space-x-2 mx-auto w-fit justify-between items-center outline outline-1 outline-slate-200 px-3 py-2 rounded-lg'>
<div>{org.name}</div>
<ArrowRightCircle />
</Link>
))}
</div>
<div className='flex cursor-pointer space-x-4 mx-auto font-semibold text-2xl pt-16 items-center'><span onClick={() => signOut({ redirect: true, callbackUrl: getUriWithoutOrg('/') })}>Sign out</span></div>
</div>
)
}
export default HomeClient

View file

@ -0,0 +1,16 @@
import React from 'react'
import HomeClient from './home'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Home',
}
function Home() {
return (
<div>
<HomeClient/>
</div>
)
}
export default Home

View file

@ -10,6 +10,7 @@ import { getAPIUrl } from '@services/config/config'
import { createNewUserInstall, updateInstall } from '@services/install/install'
import { swrFetcher } from '@services/utils/ts/requests'
import { useFormik } from 'formik'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useRouter } from 'next/navigation'
import React from 'react'
import { BarLoader } from 'react-spinners'
@ -47,11 +48,13 @@ const validate = (values: any) => {
function AccountCreation() {
const [isSubmitting, setIsSubmitting] = React.useState(false)
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const {
data: install,
error: error,
isLoading,
} = useSWR(`${getAPIUrl()}install/latest`, swrFetcher)
} = useSWR(`${getAPIUrl()}install/latest`, (url) => swrFetcher(url, access_token))
const router = useRouter()
const formik = useFormik({
initialValues: {

View file

@ -1,16 +1,19 @@
import { getAPIUrl } from '@services/config/config'
import { createDefaultElements, updateInstall } from '@services/install/install'
import { swrFetcher } from '@services/utils/ts/requests'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useRouter } from 'next/navigation'
import React from 'react'
import useSWR from 'swr'
function DefaultElements() {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const {
data: install,
error: error,
isLoading,
} = useSWR(`${getAPIUrl()}install/latest`, swrFetcher)
} = useSWR(`${getAPIUrl()}install/latest`, (url) => swrFetcher(url, access_token))
const [isSubmitting, setIsSubmitting] = React.useState(false)
const [isSubmitted, setIsSubmitted] = React.useState(false)
const router = useRouter()

View file

@ -2,16 +2,19 @@ import { getAPIUrl } from '@services/config/config'
import { updateInstall } from '@services/install/install'
import { swrFetcher } from '@services/utils/ts/requests'
import { Check } from 'lucide-react'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useRouter } from 'next/navigation'
import React from 'react'
import useSWR from 'swr'
const Finish = () => {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const {
data: install,
error: error,
isLoading,
} = useSWR(`${getAPIUrl()}install/latest`, swrFetcher)
} = useSWR(`${getAPIUrl()}install/latest`, (url) => swrFetcher(url, access_token))
const router = useRouter()
async function finishInstall() {

View file

@ -1,16 +1,19 @@
import PageLoading from '@components/Objects/Loaders/PageLoading'
import { getAPIUrl } from '@services/config/config'
import { swrFetcher } from '@services/utils/ts/requests'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useRouter } from 'next/navigation'
import React, { useEffect } from 'react'
import useSWR, { mutate } from 'swr'
function GetStarted() {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const {
data: install,
error: error,
isLoading,
} = useSWR(`${getAPIUrl()}install/latest`, swrFetcher)
} = useSWR(`${getAPIUrl()}install/latest`, (url) => swrFetcher(url, access_token))
const router = useRouter()
async function startInstallation() {

View file

@ -14,6 +14,7 @@ import useSWR from 'swr'
import { createNewOrgInstall, updateInstall } from '@services/install/install'
import { useRouter } from 'next/navigation'
import { Check } from 'lucide-react'
import { useLHSession } from '@components/Contexts/LHSessionContext'
const validate = (values: any) => {
const errors: any = {}
@ -40,11 +41,13 @@ const validate = (values: any) => {
}
function OrgCreation() {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const {
data: install,
error: error,
isLoading,
} = useSWR(`${getAPIUrl()}install/latest`, swrFetcher)
} = useSWR(`${getAPIUrl()}install/latest`, (url) => swrFetcher(url, access_token))
const [isSubmitting, setIsSubmitting] = React.useState(false)
const [isSubmitted, setIsSubmitted] = React.useState(false)
const router = useRouter()

View file

@ -4,16 +4,19 @@ import {
updateInstall,
} from '@services/install/install'
import { swrFetcher } from '@services/utils/ts/requests'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useRouter } from 'next/navigation'
import React from 'react'
import useSWR from 'swr'
function SampleData() {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const {
data: install,
error: error,
isLoading,
} = useSWR(`${getAPIUrl()}install/latest`, swrFetcher)
} = useSWR(`${getAPIUrl()}install/latest`, (url) => swrFetcher(url, access_token))
const router = useRouter()
function createSampleData() {

View file

@ -1,8 +1,9 @@
'use client'
import '../styles/globals.css'
import StyledComponentsRegistry from '../components/Utils/libs/styled-registry'
import { motion } from 'framer-motion'
import { SessionProvider } from 'next-auth/react'
import LHSessionProvider from '@components/Contexts/LHSessionContext'
export default function RootLayout({
children,
@ -18,18 +19,22 @@ export default function RootLayout({
<html className="" lang="en">
<head />
<body>
<StyledComponentsRegistry>
<motion.main
variants={variants} // Pass the variant object into Framer Motion
initial="hidden" // Set the initial state to variants.hidden
animate="enter" // Animated state to variants.enter
exit="exit" // Exit state (used later) to variants.exit
transition={{ type: 'linear' }} // Set the transition to linear
className=""
>
{children}
</motion.main>
</StyledComponentsRegistry>
<SessionProvider>
<LHSessionProvider>
<StyledComponentsRegistry>
<motion.main
variants={variants} // Pass the variant object into Framer Motion
initial="hidden" // Set the initial state to variants.hidden
animate="enter" // Animated state to variants.enter
exit="exit" // Exit state (used later) to variants.exit
transition={{ type: 'linear' }} // Set the transition to linear
className=""
>
{children}
</motion.main>
</StyledComponentsRegistry>
</LHSessionProvider>
</SessionProvider>
</body>
</html>
)

View file

@ -1,58 +0,0 @@
'use client'
import React from 'react'
import { createNewOrganization } from '../../../services/organizations/orgs'
const Organizations = () => {
const [name, setName] = React.useState('')
const [description, setDescription] = React.useState('')
const [email, setEmail] = React.useState('')
const [slug, setSlug] = React.useState('')
const handleNameChange = (e: any) => {
setName(e.target.value)
}
const handleDescriptionChange = (e: any) => {
setDescription(e.target.value)
}
const handleEmailChange = (e: any) => {
setEmail(e.target.value)
}
const handleSlugChange = (e: any) => {
setSlug(e.target.value)
}
const handleSubmit = async (e: any) => {
e.preventDefault()
let logo = ''
const status = await createNewOrganization({
name,
description,
email,
logo,
slug,
default: false,
})
alert(JSON.stringify(status))
}
return (
<div>
<div className="font-bold text-lg">New Organization</div>
Name: <input onChange={handleNameChange} type="text" />
<br />
Description: <input onChange={handleDescriptionChange} type="text" />
<br />
Slug: <input onChange={handleSlugChange} type="text" />
<br />
Email Address: <input onChange={handleEmailChange} type="text" />
<br />
<button onClick={handleSubmit}>Create</button>
</div>
)
}
export default Organizations

View file

@ -1,65 +0,0 @@
'use client' //todo: use server components
import Link from 'next/link'
import React from 'react'
import { deleteOrganizationFromBackend } from '@services/organizations/orgs'
import useSWR, { mutate } from 'swr'
import { swrFetcher } from '@services/utils/ts/requests'
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
const Organizations = () => {
const { data: organizations, error } = useSWR(
`${getAPIUrl()}orgs/user/page/1/limit/10`,
swrFetcher
)
async function deleteOrganization(org_id: any) {
const response = await deleteOrganizationFromBackend(org_id)
response &&
mutate(
`${getAPIUrl()}orgs/user/page/1/limit/10`,
organizations.filter((org: any) => org.org_id !== org_id)
)
}
return (
<>
<div className="font-bold text-lg">
Your Organizations{' '}
<Link href="/organizations/new">
<button className="bg-blue-500 text-white px-2 py-1 rounded-md hover:bg-blue-600 focus:outline-none">
+
</button>
</Link>
</div>
<hr />
{error && <p className="text-red-500">Failed to load</p>}
{!organizations ? (
<p className="text-gray-500">Loading...</p>
) : (
<div>
{organizations.map((org: any) => (
<div
key={org.org_id}
className="flex items-center justify-between mb-4"
>
<Link href={getUriWithOrg(org.slug, '/')}>
<h3 className="text-blue-500 cursor-pointer hover:underline">
{org.name}
</h3>
</Link>
<button
onClick={() => deleteOrganization(org.org_id)}
className="px-3 py-1 text-white bg-red-500 rounded-md hover:bg-red-600 focus:outline-none"
>
Delete
</button>
</div>
))}
</div>
)}
</>
)
}
export default Organizations

View file

@ -1,11 +1,11 @@
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
import { getUriWithOrg } from '@services/config/config'
import { getCollectionByIdWithAuthHeader } from '@services/courses/collections'
import { getCollectionById } from '@services/courses/collections'
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import { nextAuthOptions } from 'app/auth/options'
import { Metadata } from 'next'
import { cookies } from 'next/headers'
import { getServerSession } from 'next-auth'
import Link from 'next/link'
type MetadataProps = {
@ -16,15 +16,15 @@ type MetadataProps = {
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const session = await getServerSession(nextAuthOptions)
const access_token = session?.tokens?.access_token
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, {
revalidate: 1800,
tags: ['organizations'],
})
const col = await getCollectionByIdWithAuthHeader(
const col = await getCollectionById(
params.collectionid,
access_token ? access_token : null,
{ revalidate: 0, tags: ['collections'] }
@ -53,14 +53,14 @@ export async function generateMetadata({
}
const CollectionPage = async (params: any) => {
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const session = await getServerSession(nextAuthOptions)
const access_token = session?.tokens?.access_token
const org = await getOrganizationContextInfo(params.params.orgslug, {
revalidate: 1800,
tags: ['organizations'],
})
const orgslug = params.params.orgslug
const col = await getCollectionByIdWithAuthHeader(
const col = await getCollectionById(
params.params.collectionid,
access_token ? access_token : null,
{ revalidate: 0, tags: ['collections'] }

View file

@ -6,9 +6,12 @@ import useSWR from 'swr'
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
import { revalidateTags, swrFetcher } from '@services/utils/ts/requests'
import { useOrg } from '@components/Contexts/OrgContext'
import { useLHSession } from '@components/Contexts/LHSessionContext'
function NewCollection(params: any) {
const org = useOrg() as any
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const orgslug = params.params.orgslug
const [name, setName] = React.useState('')
const [description, setDescription] = React.useState('')
@ -16,7 +19,7 @@ function NewCollection(params: any) {
const router = useRouter()
const { data: courses, error: error } = useSWR(
`${getAPIUrl()}courses/org_slug/${orgslug}/page/1/limit/10`,
swrFetcher
(url) => swrFetcher(url, access_token)
)
const [isPublic, setIsPublic] = useState('true')
@ -44,7 +47,7 @@ function NewCollection(params: any) {
public: isPublic,
org_id: org.id,
}
await createCollection(collection)
await createCollection(collection, session.data?.tokens?.access_token)
await revalidateTags(['collections'], org.slug)
// reload the page
router.refresh()

View file

@ -2,15 +2,15 @@ import AuthenticatedClientElement from '@components/Security/AuthenticatedClient
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle'
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
import { getUriWithOrg } from '@services/config/config'
import { getOrgCollectionsWithAuthHeader } from '@services/courses/collections'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import { Metadata } from 'next'
import { cookies } from 'next/headers'
import Link from 'next/link'
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
import CollectionThumbnail from '@components/Objects/Thumbnails/CollectionThumbnail'
import NewCollectionButton from '@components/StyledElements/Buttons/NewCollectionButton'
import ContentPlaceHolderIfUserIsNotAdmin from '@components/ContentPlaceHolder'
import { nextAuthOptions } from 'app/auth/options'
import { getServerSession } from 'next-auth'
import { getOrgCollections } from '@services/courses/collections'
type MetadataProps = {
params: { orgslug: string; courseid: string }
@ -49,15 +49,15 @@ export async function generateMetadata({
}
const CollectionsPage = async (params: any) => {
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const session = await getServerSession(nextAuthOptions)
const access_token = session?.tokens?.access_token
const orgslug = params.params.orgslug
const org = await getOrganizationContextInfo(orgslug, {
revalidate: 1800,
tags: ['organizations'],
})
const org_id = org.id
const collections = await getOrgCollectionsWithAuthHeader(
const collections = await getOrgCollections(
org_id,
access_token ? access_token : null,
{ revalidate: 0, tags: ['collections'] }

View file

@ -15,6 +15,7 @@ import { useOrg } from '@components/Contexts/OrgContext'
import { CourseProvider } from '@components/Contexts/CourseContext'
import AIActivityAsk from '@components/Objects/Activities/AI/AIActivityAsk'
import AIChatBotProvider from '@components/Contexts/AI/AIChatBotContext'
import { useLHSession } from '@components/Contexts/LHSessionContext'
interface ActivityClientProps {
activityid: string
@ -106,11 +107,10 @@ function ActivityClient(props: ActivityClientProps) {
{activity ? (
<div
className={`p-7 pt-4 drop-shadow-sm rounded-lg ${
activity.activity_type == 'TYPE_DYNAMIC'
className={`p-7 pt-4 drop-shadow-sm rounded-lg ${activity.activity_type == 'TYPE_DYNAMIC'
? 'bg-white'
: 'bg-zinc-950'
}`}
}`}
>
<div>
{activity.activity_type == 'TYPE_DYNAMIC' && (
@ -147,13 +147,14 @@ export function MarkStatus(props: {
orgslug: string
}) {
const router = useRouter()
console.log(props.course.trail)
const session = useLHSession() as any;
async function markActivityAsCompleteFront() {
const trail = await markActivityAsComplete(
props.orgslug,
props.course.course_uuid,
'activity_' + props.activityid
'activity_' + props.activityid,
session.data?.tokens?.access_token
)
router.refresh()
}
@ -169,8 +170,6 @@ export function MarkStatus(props: {
}
}
console.log('isActivityCompleted', isActivityCompleted())
return (
<>
{isActivityCompleted() ? (

View file

@ -1,10 +1,10 @@
import { getActivityWithAuthHeader } from '@services/courses/activities'
import { getCourseMetadataWithAuthHeader } from '@services/courses/courses'
import { cookies } from 'next/headers'
import { getCourseMetadata } from '@services/courses/courses'
import ActivityClient from './activity'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import { Metadata } from 'next'
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
import { getServerSession } from 'next-auth'
import { nextAuthOptions } from 'app/auth/options'
type MetadataProps = {
params: { orgslug: string; courseuuid: string; activityid: string }
@ -14,15 +14,15 @@ type MetadataProps = {
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const session = await getServerSession(nextAuthOptions)
const access_token = session?.tokens?.access_token
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, {
revalidate: 1800,
tags: ['organizations'],
})
const course_meta = await getCourseMetadataWithAuthHeader(
const course_meta = await getCourseMetadata(
params.courseuuid,
{ revalidate: 0, tags: ['courses'] },
access_token ? access_token : null
@ -58,13 +58,13 @@ export async function generateMetadata({
}
const ActivityPage = async (params: any) => {
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const session = await getServerSession(nextAuthOptions)
const access_token = session?.tokens?.access_token
const activityid = params.params.activityid
const courseuuid = params.params.courseuuid
const orgslug = params.params.orgslug
const course_meta = await getCourseMetadataWithAuthHeader(
const course_meta = await getCourseMetadata(
courseuuid,
{ revalidate: 0, tags: ['courses'] },
access_token ? access_token : null

View file

@ -17,10 +17,12 @@ import { useOrg } from '@components/Contexts/OrgContext'
import UserAvatar from '@components/Objects/UserAvatar'
import CourseUpdates from '@components/Objects/CourseUpdates/CourseUpdates'
import { CourseProvider } from '@components/Contexts/CourseContext'
import { useLHSession } from '@components/Contexts/LHSessionContext'
const CourseClient = (props: any) => {
const [user, setUser] = useState<any>({})
const [learnings, setLearnings] = useState<any>([])
const session = useLHSession() as any;
const courseuuid = props.courseuuid
const orgslug = props.orgslug
const course = props.course
@ -35,7 +37,7 @@ const CourseClient = (props: any) => {
async function startCourseUI() {
// Create activity
await startCourse('course_' + courseuuid, orgslug)
await startCourse('course_' + courseuuid, orgslug, session.data?.tokens?.access_token)
await revalidateTags(['courses'], orgslug)
router.refresh()
@ -54,7 +56,7 @@ const CourseClient = (props: any) => {
async function quitCourse() {
// Close activity
let activity = await removeCourse('course_' + courseuuid, orgslug)
let activity = await removeCourse('course_' + courseuuid, orgslug, session.data?.tokens?.access_token)
// Mutate course
await revalidateTags(['courses'], orgslug)
router.refresh()
@ -277,7 +279,7 @@ const CourseClient = (props: any) => {
<UserAvatar
border="border-8"
avatar_url={course.authors[0].avatar_image ? getUserAvatarMediaDirectory(course.authors[0].user_uuid, course.authors[0].avatar_image) : ''}
predefined_avatar={course.authors[0].avatar_image ? undefined : 'empty'}
predefined_avatar={course.authors[0].avatar_image ? undefined : 'empty'}
width={100}
/>
<div className="-space-y-2 ">

View file

@ -1,6 +1,5 @@
'use client' // Error components must be Client Components
import ErrorUI from '@components/StyledElements/Error/Error'
import { useEffect } from 'react'
export default function Error({
@ -17,7 +16,7 @@ export default function Error({
return (
<div>
<ErrorUI></ErrorUI>
</div>
)
}

View file

@ -1,6 +0,0 @@
import PageLoading from '@components/Objects/Loaders/PageLoading'
export default function Loading() {
// Or a custom loading skeleton component
return <PageLoading></PageLoading>
}

View file

@ -1,11 +1,11 @@
import React from 'react'
import CourseClient from './course'
import { cookies } from 'next/headers'
import { getCourseMetadataWithAuthHeader } from '@services/courses/courses'
import { getCourseMetadata } from '@services/courses/courses'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import { Metadata } from 'next'
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
import { nextAuthOptions } from 'app/auth/options'
import { getServerSession } from 'next-auth'
type MetadataProps = {
params: { orgslug: string; courseuuid: string }
@ -15,15 +15,15 @@ type MetadataProps = {
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const session = await getServerSession(nextAuthOptions)
const access_token = session?.tokens?.access_token
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, {
revalidate: 1800,
tags: ['organizations'],
})
const course_meta = await getCourseMetadataWithAuthHeader(
const course_meta = await getCourseMetadata(
params.courseuuid,
{ revalidate: 0, tags: ['courses'] },
access_token ? access_token : null
@ -67,11 +67,11 @@ export async function generateMetadata({
}
const CoursePage = async (params: any) => {
const cookieStore = cookies()
const courseuuid = params.params.courseuuid
const orgslug = params.params.orgslug
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const course_meta = await getCourseMetadataWithAuthHeader(
const session = await getServerSession(nextAuthOptions)
const access_token = session?.tokens?.access_token
const course_meta = await getCourseMetadata(
courseuuid,
{ revalidate: 0, tags: ['courses'] },
access_token ? access_token : null

View file

@ -1,10 +1,10 @@
import React from 'react'
import Courses from './courses'
import { getOrgCoursesWithAuthHeader } from '@services/courses/courses'
import { Metadata } from 'next'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import { cookies } from 'next/headers'
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
import { nextAuthOptions } from 'app/auth/options'
import { getServerSession } from 'next-auth'
import { getOrgCourses } from '@services/courses/courses'
type MetadataProps = {
params: { orgslug: string }
@ -49,9 +49,10 @@ const CoursesPage = async (params: any) => {
revalidate: 1800,
tags: ['organizations'],
})
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const courses = await getOrgCoursesWithAuthHeader(
const session = await getServerSession(nextAuthOptions)
const access_token = session?.tokens?.access_token
const courses = await getOrgCourses(
orgslug,
{ revalidate: 0, tags: ['courses'] },
access_token ? access_token : null

View file

@ -1,6 +1,7 @@
'use client'
import '@styles/globals.css'
import { Menu } from '@components/Objects/Menu/Menu'
import SessionProvider from '@components/Contexts/SessionContext'
import { SessionProvider } from 'next-auth/react'
export default function RootLayout({
children,

View file

@ -1,20 +1,20 @@
export const dynamic = 'force-dynamic'
import { Metadata } from 'next'
import { getUriWithOrg } from '@services/config/config'
import { getOrgCoursesWithAuthHeader } from '@services/courses/courses'
import { getOrgCourses } from '@services/courses/courses'
import Link from 'next/link'
import { getOrgCollectionsWithAuthHeader } from '@services/courses/collections'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import { cookies } from 'next/headers'
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle'
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail'
import CollectionThumbnail from '@components/Objects/Thumbnails/CollectionThumbnail'
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton'
import NewCollectionButton from '@components/StyledElements/Buttons/NewCollectionButton'
import ContentPlaceHolderIfUserIsNotAdmin from '@components/ContentPlaceHolder'
import { getOrgCollections } from '@services/courses/collections'
import { getServerSession } from 'next-auth'
import { nextAuthOptions } from 'app/auth/options'
type MetadataProps = {
params: { orgslug: string }
@ -54,10 +54,9 @@ export async function generateMetadata({
const OrgHomePage = async (params: any) => {
const orgslug = params.params.orgslug
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const courses = await getOrgCoursesWithAuthHeader(
const session = await getServerSession(nextAuthOptions)
const access_token = session?.tokens?.access_token
const courses = await getOrgCourses(
orgslug,
{ revalidate: 0, tags: ['courses'] },
access_token ? access_token : null
@ -67,7 +66,7 @@ const OrgHomePage = async (params: any) => {
tags: ['organizations'],
})
const org_id = org.id
const collections = await getOrgCollectionsWithAuthHeader(
const collections = await getOrgCollections(
org.id,
access_token ? access_token : null,
{ revalidate: 0, tags: ['courses'] }

View file

@ -2,6 +2,8 @@ import React from 'react'
import { Metadata } from 'next'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import Trail from './trail'
import { getServerSession } from 'next-auth'
import { nextAuthOptions } from 'app/auth/options'
type MetadataProps = {
params: { orgslug: string }
@ -11,11 +13,13 @@ type MetadataProps = {
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const session = await getServerSession(nextAuthOptions)
const access_token = session?.tokens?.access_token
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, {
revalidate: 1800,
tags: ['organizations'],
})
}, access_token)
return {
title: 'Trail — ' + org.name,
description:

View file

@ -1,4 +1,5 @@
'use client'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useOrg } from '@components/Contexts/OrgContext'
import PageLoading from '@components/Objects/Loaders/PageLoading'
import TrailCourseElement from '@components/Pages/Trail/TrailCourseElement'
@ -11,14 +12,16 @@ import useSWR from 'swr'
function Trail(params: any) {
let orgslug = params.orgslug
const session = useLHSession() as any
const access_token = session.data.tokens.access_token;
const org = useOrg() as any
const orgID = org?.id
const { data: trail, error: error } = useSWR(
`${getAPIUrl()}trail/org/${orgID}/trail`,
swrFetcher
(url) => swrFetcher(url, access_token)
)
useEffect(() => {}, [trail, org])
useEffect(() => { }, [trail, org])
return (
<GeneralWrapperStyled>

View file

@ -0,0 +1,26 @@
'use client';
import LeftMenu from '@components/Dashboard/UI/LeftMenu'
import AdminAuthorization from '@components/Security/AdminAuthorization'
import { SessionProvider } from 'next-auth/react'
import React from 'react'
function ClientAdminLayout({
children,
params,
}: {
children: React.ReactNode
params: any
}) {
return (
<SessionProvider>
<AdminAuthorization authorizationMode="page">
<div className="flex">
<LeftMenu />
<div className="flex w-full">{children}</div>
</div>
</AdminAuthorization>
</SessionProvider>
)
}
export default ClientAdminLayout

View file

@ -30,7 +30,6 @@ function CoursesHome(params: CourseProps) {
<div>
<div className="pl-10 mr-10 tracking-tighter">
<BreadCrumbs type="courses" />
<div className="w-100 flex justify-between">
<div className="pt-3 flex font-bold text-4xl">Courses</div>
<AuthenticatedClientElement

View file

@ -7,7 +7,7 @@ import Link from 'next/link'
import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop'
import { motion } from 'framer-motion'
import EditCourseGeneral from '@components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral'
import { GalleryVerticalEnd, Info, Lock, UserRoundCog } from 'lucide-react'
import { GalleryVerticalEnd, Info, UserRoundCog } from 'lucide-react'
import EditCourseAccess from '@components/Dashboard/Course/EditCourseAccess/EditCourseAccess'
export type CourseOverviewParams = {

View file

@ -1,10 +1,10 @@
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
import { getOrgCoursesWithAuthHeader } from '@services/courses/courses'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import { Metadata } from 'next'
import { cookies } from 'next/headers'
import React from 'react'
import CoursesHome from './client'
import { nextAuthOptions } from 'app/auth/options'
import { getServerSession } from 'next-auth'
import { getOrgCourses } from '@services/courses/courses'
type MetadataProps = {
params: { orgslug: string }
@ -49,9 +49,9 @@ async function CoursesPage(params: any) {
revalidate: 1800,
tags: ['organizations'],
})
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const courses = await getOrgCoursesWithAuthHeader(
const session = await getServerSession(nextAuthOptions)
const access_token = session?.tokens?.access_token
const courses = await getOrgCourses(
orgslug,
{ revalidate: 0, tags: ['courses'] },
access_token ? access_token : null

View file

@ -1,8 +1,6 @@
import SessionProvider from '@components/Contexts/SessionContext'
import LeftMenu from '@components/Dashboard/UI/LeftMenu'
import AdminAuthorization from '@components/Security/AdminAuthorization'
import { Metadata } from 'next'
import React from 'react'
import ClientAdminLayout from './ClientAdminLayout'
export const metadata: Metadata = {
title: 'LearnHouse Dashboard',
@ -17,14 +15,10 @@ function DashboardLayout({
}) {
return (
<>
<SessionProvider>
<AdminAuthorization authorizationMode="page">
<div className="flex">
<LeftMenu />
<div className="flex w-full">{children}</div>
</div>
</AdminAuthorization>
</SessionProvider>
<ClientAdminLayout
params={params}>
{children}
</ClientAdminLayout>
</>
)
}

View file

@ -7,7 +7,7 @@ import Link from 'next/link'
import { getUriWithOrg } from '@services/config/config'
import { Info, Lock } from 'lucide-react'
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import { useSession } from '@components/Contexts/SessionContext'
import { useLHSession } from '@components/Contexts/LHSessionContext'
export type SettingsParams = {
subpage: string
@ -15,7 +15,7 @@ export type SettingsParams = {
}
function SettingsPage({ params }: { params: SettingsParams }) {
const session = useSession() as any
const session = useLHSession() as any
useEffect(() => {}, [session])

View file

@ -5,7 +5,7 @@ import Link from 'next/link'
import { getUriWithOrg } from '@services/config/config'
import { ScanEye, SquareUserRound, UserPlus, Users } from 'lucide-react'
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import { useSession } from '@components/Contexts/SessionContext'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useOrg } from '@components/Contexts/OrgContext'
import OrgUsers from '@components/Dashboard/Users/OrgUsers/OrgUsers'
import OrgAccess from '@components/Dashboard/Users/OrgAccess/OrgAccess'
@ -18,7 +18,7 @@ export type SettingsParams = {
}
function UsersSettingsPage({ params }: { params: SettingsParams }) {
const session = useSession() as any
const session = useLHSession() as any
const org = useOrg() as any
const [H1Label, setH1Label] = React.useState('')
const [H2Label, setH2Label] = React.useState('')

View file

@ -1,6 +1,5 @@
'use client'
import { OrgProvider } from '@components/Contexts/OrgContext'
import SessionProvider from '@components/Contexts/SessionContext'
import Toast from '@components/StyledElements/Toast/Toast'
import '@styles/globals.css'
@ -13,9 +12,9 @@ export default function RootLayout({
}) {
return (
<div>
<Toast />
<OrgProvider orgslug={params.orgslug}>
<SessionProvider>{children}</SessionProvider>
<Toast />
{children}
</OrgProvider>
</div>
)

View file

@ -13,17 +13,14 @@ function useGetAIFeatures(props: UseGetAIFeatures) {
const config = org?.config?.config?.AIConfig
if (!config) {
console.log('AI or Organization config is not defined.')
return false
}
if (!config.enabled) {
console.log('AI is not enabled for this Organization.')
return false
}
if (!config.features[feature]) {
console.log(`Feature ${feature} is not enabled for this Organization.`)
return false
}

View file

@ -7,7 +7,7 @@ import useAdminStatus from './Hooks/useAdminStatus'
function ContentPlaceHolderIfUserIsNotAdmin({ text }: { text: string }) {
const isUserAdmin = useAdminStatus() as any
return (
<div>{isUserAdmin ? text : 'No content yet'}</div>
<span>{isUserAdmin ? text : 'No content yet'}</span>
)
}

View file

@ -1,9 +1,9 @@
'use client'
import PageLoading from '@components/Objects/Loaders/PageLoading'
import { getAPIUrl } from '@services/config/config'
import { swrFetcher } from '@services/utils/ts/requests'
import React, { createContext, useContext, useEffect, useReducer } from 'react'
import useSWR from 'swr'
import { useLHSession } from '@components/Contexts/LHSessionContext'
export const CourseContext = createContext(null) as any
export const CourseDispatchContext = createContext(null) as any
@ -15,9 +15,11 @@ export function CourseProvider({
children: React.ReactNode
courseuuid: string
}) {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const { data: courseStructureData } = useSWR(
`${getAPIUrl()}courses/${courseuuid}/meta`,
swrFetcher
(url) => swrFetcher(url, access_token)
)
const [courseStructure, dispatchCourseStructure] = useReducer(courseReducer, {
courseStructure: courseStructureData ? courseStructureData : {},
@ -33,9 +35,9 @@ export function CourseProvider({
payload: courseStructureData,
})
}
}, [courseStructureData])
}, [courseStructureData,session])
if (!courseStructureData) return <PageLoading></PageLoading>
if (!courseStructureData) return
return (
<CourseContext.Provider value={courseStructure}>

View file

@ -0,0 +1,32 @@
'use client'
import PageLoading from '@components/Objects/Loaders/PageLoading';
import { useSession } from 'next-auth/react';
import React, { useContext, createContext, useEffect } from 'react'
export const SessionContext = createContext({}) as any
function LHSessionProvider({ children }: { children: React.ReactNode }) {
const session = useSession();
useEffect(() => {
}, [])
if (session && session.status == 'loading') {
return <PageLoading />
}
else if (session) {
return (
<SessionContext.Provider value={session}>
{children}
</SessionContext.Provider>
)
}
}
export function useLHSession() {
return useContext(SessionContext)
}
export default LHSessionProvider

View file

@ -1,32 +1,46 @@
'use client'
import { getAPIUrl } from '@services/config/config'
import { getAPIUrl, getUriWithoutOrg } from '@services/config/config'
import { swrFetcher } from '@services/utils/ts/requests'
import React, { useContext, useEffect } from 'react'
import React, { createContext, useContext, useMemo } from 'react'
import useSWR from 'swr'
import { createContext } from 'react'
import { useRouter } from 'next/navigation'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import ErrorUI from '@components/StyledElements/Error/Error'
import InfoUI from '@components/StyledElements/Info/Info'
import { usePathname } from 'next/navigation'
export const OrgContext = createContext({}) as any
export const OrgContext = createContext(null)
export function OrgProvider({
children,
orgslug,
}: {
children: React.ReactNode
orgslug: string
}) {
const { data: org } = useSWR(`${getAPIUrl()}orgs/slug/${orgslug}`, swrFetcher)
const router = useRouter()
// Check if Org is Active
const verifyIfOrgIsActive = () => {
if (org && org?.config.config.GeneralConfig.active === false) {
router.push('/404')
}
export function OrgProvider({ children, orgslug }: { children: React.ReactNode, orgslug: string }) {
const session = useLHSession() as any
const pathname = usePathname()
const accessToken = session?.data?.tokens?.access_token
const isAllowedPathname = ['/login', '/signup'].includes(pathname);
const { data: org, error: orgError } = useSWR(
`${getAPIUrl()}orgs/slug/${orgslug}`,
(url) => swrFetcher(url, accessToken)
)
const { data: orgs, error: orgsError } = useSWR(
`${getAPIUrl()}orgs/user/page/1/limit/10`,
(url) => swrFetcher(url, accessToken)
)
const isOrgActive = useMemo(() => org?.config?.config?.GeneralConfig?.active !== false, [org])
const isUserPartOfTheOrg = useMemo(() => orgs?.some((userOrg: any) => userOrg.id === org?.id), [orgs, org?.id])
if (orgError || orgsError) return <ErrorUI message='An error occurred while fetching data' />
if (!org || !orgs || !session) return <div>Loading...</div>
if (!isOrgActive) return <ErrorUI message='This organization is no longer active' />
if (!isUserPartOfTheOrg && session.status == 'authenticated' && !isAllowedPathname) {
return (
<InfoUI
href={getUriWithoutOrg(`/signup?orgslug=${orgslug}`)}
message='You are not part of this Organization yet'
cta={`Join ${org?.name}`}
/>
)
}
useEffect(() => {
verifyIfOrgIsActive()
}, [org])
return <OrgContext.Provider value={org}>{children}</OrgContext.Provider>
}

View file

@ -1,77 +0,0 @@
'use client'
import {
getNewAccessTokenUsingRefreshToken,
getUserSession,
} from '@services/auth/auth'
import React, { useContext, createContext, useEffect } from 'react'
export const SessionContext = createContext({}) as any
type Session = {
access_token: string
user: any
roles: any
isLoading: boolean
isAuthenticated: boolean
}
function SessionProvider({ children }: { children: React.ReactNode }) {
const [session, setSession] = React.useState<Session>({
access_token: '',
user: {},
roles: {},
isLoading: true,
isAuthenticated: false,
})
async function getNewAccessTokenUsingRefreshTokenUI() {
let data = await getNewAccessTokenUsingRefreshToken()
return data.access_token
}
async function checkSession() {
// Get new access token using refresh token
const access_token = await getNewAccessTokenUsingRefreshTokenUI()
if (access_token) {
// Get user session info
const user_session = await getUserSession(access_token)
// Set session
setSession({
access_token: access_token,
user: user_session.user,
roles: user_session.roles,
isLoading: false,
isAuthenticated: true,
})
}
if (!access_token) {
setSession({
access_token: '',
user: {},
roles: {},
isLoading: false,
isAuthenticated: false,
})
}
}
useEffect(() => {
// Check session
checkSession()
}, [])
return (
<SessionContext.Provider value={session}>
{children}
</SessionContext.Provider>
)
}
export function useSession() {
return useContext(SessionContext)
}
export default SessionProvider

View file

@ -5,7 +5,8 @@ import Modal from '@components/StyledElements/Modal/Modal'
import { getAPIUrl } from '@services/config/config'
import { unLinkResourcesToUserGroup } from '@services/usergroups/usergroups'
import { swrFetcher } from '@services/utils/ts/requests'
import { Globe, SquareUserRound, Users, UsersRound, X } from 'lucide-react'
import { Globe, SquareUserRound, Users, X } from 'lucide-react'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import React from 'react'
import toast from 'react-hot-toast'
import useSWR, { mutate } from 'swr'
@ -17,13 +18,15 @@ type EditCourseAccessProps = {
function EditCourseAccess(props: EditCourseAccessProps) {
const [error, setError] = React.useState('')
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const course = useCourse() as any
const dispatchCourse = useCourseDispatch() as any
const courseStructure = course.courseStructure
const { data: usergroups } = useSWR(
courseStructure ? `${getAPIUrl()}usergroups/resource/${courseStructure.course_uuid}` : null,
swrFetcher
(url) => swrFetcher(url, access_token)
)
const [isPublic, setIsPublic] = React.useState(courseStructure.public)
@ -109,7 +112,7 @@ function EditCourseAccess(props: EditCourseAccessProps) {
status="info"
></ConfirmationModal>
</div>
{!isPublic ? ( <UserGroupsSection usergroups={usergroups} />) : null}
{!isPublic ? (<UserGroupsSection usergroups={usergroups} />) : null}
</div>
</div>
)
@ -119,9 +122,11 @@ function EditCourseAccess(props: EditCourseAccessProps) {
function UserGroupsSection({ usergroups }: { usergroups: any[] }) {
const course = useCourse() as any
const [userGroupModal, setUserGroupModal] = React.useState(false)
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const removeUserGroupLink = async (usergroup_id: number) => {
const res = await unLinkResourcesToUserGroup(usergroup_id, course.courseStructure.course_uuid)
const res = await unLinkResourcesToUserGroup(usergroup_id, course.courseStructure.course_uuid,access_token)
if (res.status === 200) {
toast.success('Successfully unliked from usergroup')
mutate(`${getAPIUrl()}usergroups/resource/${course.courseStructure.course_uuid}`)

View file

@ -6,7 +6,6 @@ import FormLayout, {
} from '@components/StyledElements/Form/Form'
import { useFormik } from 'formik'
import { AlertTriangle } from 'lucide-react'
import * as Switch from '@radix-ui/react-switch'
import * as Form from '@radix-ui/react-form'
import React from 'react'
import { useCourse, useCourseDispatch } from '../../../Contexts/CourseContext'

View file

@ -4,11 +4,13 @@ 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 { useLHSession } from '@components/Contexts/LHSessionContext'
import React from 'react'
import { mutate } from 'swr'
function ThumbnailUpdate() {
const course = useCourse() as any
const session = useLHSession() as any;
const org = useOrg() as any
const [localThumbnail, setLocalThumbnail] = React.useState(null) as any
const [isLoading, setIsLoading] = React.useState(false) as any
@ -20,7 +22,8 @@ function ThumbnailUpdate() {
setIsLoading(true)
const res = await updateCourseThumbnail(
course.courseStructure.course_uuid,
file
file,
session.data?.tokens?.access_token
)
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
// wait for 1 second to show loading animation

View file

@ -10,6 +10,7 @@ import {
import { getOrganizationContextInfoWithoutCredentials } from '@services/organizations/orgs'
import { revalidateTags } from '@services/utils/ts/requests'
import { Layers } from 'lucide-react'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useRouter } from 'next/navigation'
import React, { useEffect } from 'react'
import { mutate } from 'swr'
@ -23,6 +24,8 @@ function NewActivityButton(props: NewActivityButtonProps) {
const [newActivityModal, setNewActivityModal] = React.useState(false)
const router = useRouter()
const course = useCourse() as any
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const openNewActivityModal = async (chapterId: any) => {
setNewActivityModal(true)
@ -38,7 +41,7 @@ function NewActivityButton(props: NewActivityButtonProps) {
props.orgslug,
{ revalidate: 1800 }
)
await createActivity(activity, props.chapterId, org.org_id)
await createActivity(activity, props.chapterId, org.org_id, access_token)
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
setNewActivityModal(false)
await revalidateTags(['courses'], props.orgslug)
@ -52,7 +55,7 @@ function NewActivityButton(props: NewActivityButtonProps) {
activity: any,
chapterId: string
) => {
await createFileActivity(file, type, activity, chapterId)
await createFileActivity(file, type, activity, chapterId, access_token)
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
setNewActivityModal(false)
await revalidateTags(['courses'], props.orgslug)
@ -68,7 +71,7 @@ function NewActivityButton(props: NewActivityButtonProps) {
await createExternalVideoActivity(
external_video_data,
activity,
props.chapterId
props.chapterId, access_token
)
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
setNewActivityModal(false)
@ -76,7 +79,7 @@ function NewActivityButton(props: NewActivityButtonProps) {
router.refresh()
}
useEffect(() => {}, [course])
useEffect(() => { }, [course])
return (
<div className="flex justify-center">

View file

@ -12,6 +12,7 @@ import {
Video,
X,
} from 'lucide-react'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import React from 'react'
@ -32,6 +33,8 @@ interface ModifiedActivityInterface {
function ActivityElement(props: ActivitiyElementProps) {
const router = useRouter()
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const [modifiedActivity, setModifiedActivity] = React.useState<
ModifiedActivityInterface | undefined
>(undefined)
@ -41,7 +44,7 @@ function ActivityElement(props: ActivitiyElementProps) {
const activityUUID = props.activity.activity_uuid
async function deleteActivityUI() {
await deleteActivity(props.activity.activity_uuid)
await deleteActivity(props.activity.activity_uuid,access_token)
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug)
router.refresh()
@ -60,7 +63,7 @@ function ActivityElement(props: ActivitiyElementProps) {
content: props.activity.content,
}
await updateActivity(modifiedActivityCopy, activityUUID)
await updateActivity(modifiedActivityCopy, activityUUID,access_token)
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug)
router.refresh()

View file

@ -16,6 +16,7 @@ import { revalidateTags } from '@services/utils/ts/requests'
import { useRouter } from 'next/navigation'
import { getAPIUrl } from '@services/config/config'
import { mutate } from 'swr'
import { useLHSession } from '@components/Contexts/LHSessionContext'
type ChapterElementProps = {
chapter: any
@ -31,6 +32,8 @@ interface ModifiedChapterInterface {
function ChapterElement(props: ChapterElementProps) {
const activities = props.chapter.activities || []
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const [modifiedChapter, setModifiedChapter] = React.useState<
ModifiedChapterInterface | undefined
>(undefined)
@ -41,7 +44,7 @@ function ChapterElement(props: ChapterElementProps) {
const router = useRouter()
const deleteChapterUI = async () => {
await deleteChapter(props.chapter.id)
await deleteChapter(props.chapter.id, access_token)
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug)
router.refresh()
@ -53,7 +56,7 @@ function ChapterElement(props: ChapterElementProps) {
let modifiedChapterCopy = {
name: modifiedChapter.chapterName,
}
await updateChapter(chapterId, modifiedChapterCopy)
await updateChapter(chapterId, modifiedChapterCopy, access_token)
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug)
router.refresh()

View file

@ -15,6 +15,7 @@ import {
import { Hexagon } from 'lucide-react'
import Modal from '@components/StyledElements/Modal/Modal'
import NewChapterModal from '@components/Objects/Modals/Chapters/NewChapter'
import { useLHSession } from '@components/Contexts/LHSessionContext'
type EditCourseStructureProps = {
orgslug: string
@ -38,6 +39,8 @@ export type OrderPayload =
const EditCourseStructure = (props: EditCourseStructureProps) => {
const router = useRouter()
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
// Check window availability
const [winReady, setwinReady] = useState(false)
@ -57,7 +60,7 @@ const EditCourseStructure = (props: EditCourseStructureProps) => {
// Submit new chapter
const submitChapter = async (chapter: any) => {
await createChapter(chapter)
await createChapter(chapter,access_token)
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug)
router.refresh()

View file

@ -9,6 +9,7 @@ import { UploadCloud } from 'lucide-react'
import { revalidateTags } from '@services/utils/ts/requests'
import { useRouter } from 'next/navigation'
import { useOrg } from '@components/Contexts/OrgContext'
import { useLHSession } from '@components/Contexts/LHSessionContext'
interface OrganizationValues {
name: string
@ -20,7 +21,9 @@ interface OrganizationValues {
function OrgEditGeneral(props: any) {
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const router = useRouter()
const router = useRouter();
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const org = useOrg() as any
// ...
@ -34,7 +37,7 @@ function OrgEditGeneral(props: any) {
const uploadLogo = async () => {
if (selectedFile) {
let org_id = org.id
await uploadOrganizationLogo(org_id, selectedFile)
await uploadOrganizationLogo(org_id, selectedFile, access_token)
setSelectedFile(null) // Reset the selected file
await revalidateTags(['organizations'], org.slug)
router.refresh()
@ -51,14 +54,14 @@ function OrgEditGeneral(props: any) {
const updateOrg = async (values: OrganizationValues) => {
let org_id = org.id
await updateOrganization(org_id, values)
await updateOrganization(org_id, values, access_token)
// Mutate the org
await revalidateTags(['organizations'], org.slug)
router.refresh()
}
useEffect(() => {}, [org])
useEffect(() => { }, [org])
return (
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5">

View file

@ -1,22 +1,21 @@
'use client'
import { useOrg } from '@components/Contexts/OrgContext'
import { useSession } from '@components/Contexts/SessionContext'
import { signOut } from 'next-auth/react'
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
import LearnHouseDashboardLogo from '@public/dashLogo.png'
import { logout } from '@services/auth/auth'
import { BookCopy, Home, LogOut, School, Settings, Users } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import React, { useEffect } from 'react'
import UserAvatar from '../../Objects/UserAvatar'
import AdminAuthorization from '@components/Security/AdminAuthorization'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config'
function LeftMenu() {
const org = useOrg() as any
const session = useSession() as any
const session = useLHSession() as any
const [loading, setLoading] = React.useState(true)
const route = useRouter()
function waitForEverythingToLoad() {
if (org && session) {
@ -26,9 +25,9 @@ function LeftMenu() {
}
async function logOutUI() {
const res = await logout()
const res = await signOut({ redirect: true, callbackUrl: getUriWithoutOrg('/login?orgslug=' + org.slug) })
if (res) {
route.push('/login')
getUriWithOrg(org.slug, '/')
}
}
@ -123,7 +122,7 @@ function LeftMenu() {
<div className="flex flex-col mx-auto pb-7 space-y-2">
<div className="flex items-center flex-col space-y-2">
<ToolTip
content={'@' + session.user.username}
content={'@' + session.data.user.username}
slateBlack
sideOffset={8}
side="right"
@ -134,7 +133,7 @@ function LeftMenu() {
</ToolTip>
<div className="flex items-center flex-col space-y-1">
<ToolTip
content={session.user.username + "'s Settings"}
content={session.data.user.username + "'s Settings"}
slateBlack
sideOffset={8}
side="right"

View file

@ -11,9 +11,11 @@ import { useRouter } from 'next/navigation'
import React, { useEffect } from 'react'
import { mutate } from 'swr'
import { updateCourse } from '@services/courses/courses'
import { useLHSession } from '@components/Contexts/LHSessionContext'
function SaveState(props: { orgslug: string }) {
const course = useCourse() as any
const session = useLHSession() as any;
const router = useRouter()
const saved = course ? course.isSaved : true
const dispatchCourse = useCourseDispatch() as any
@ -37,7 +39,8 @@ function SaveState(props: { orgslug: string }) {
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
await updateCourseOrderStructure(
course.courseStructure.course_uuid,
course.courseOrder
course.courseOrder,
session.data?.tokens?.access_token
)
await revalidateTags(['courses'], props.orgslug)
router.refresh()
@ -49,7 +52,8 @@ function SaveState(props: { orgslug: string }) {
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
await updateCourse(
course.courseStructure.course_uuid,
course.courseStructure
course.courseStructure,
session.data?.tokens?.access_token
)
await revalidateTags(['courses'], props.orgslug)
router.refresh()

View file

@ -1,7 +1,8 @@
'use client';
import { updateProfile } from '@services/settings/profile'
import React, { useEffect } from 'react'
import { Formik, Form, Field } from 'formik'
import { useSession } from '@components/Contexts/SessionContext'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import {
ArrowBigUpDash,
Check,
@ -13,7 +14,8 @@ import UserAvatar from '@components/Objects/UserAvatar'
import { updateUserAvatar } from '@services/users/users'
function UserEditGeneral() {
const session = useSession() as any
const session = useLHSession() as any;
const access_token = session.data.tokens.access_token;
const [localAvatar, setLocalAvatar] = React.useState(null) as any
const [isLoading, setIsLoading] = React.useState(false) as any
const [error, setError] = React.useState() as any
@ -23,7 +25,7 @@ function UserEditGeneral() {
const file = event.target.files[0]
setLocalAvatar(file)
setIsLoading(true)
const res = await updateUserAvatar(session.user.user_uuid, file)
const res = await updateUserAvatar(session.data.user_uuid, file, access_token)
// wait for 1 second to show loading animation
await new Promise((r) => setTimeout(r, 1500))
if (res.success === false) {
@ -35,24 +37,24 @@ function UserEditGeneral() {
}
}
useEffect(() => {}, [session, session.user])
useEffect(() => { }, [session, session.data])
return (
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5">
{session.user && (
{session.data.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,
username: session.data.user.username,
first_name: session.data.user.first_name,
last_name: session.data.user.last_name,
email: session.data.user.email,
bio: session.data.user.bio,
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
setSubmitting(false)
updateProfile(values, session.user.id)
updateProfile(values, session.data.user.id, access_token)
}, 400)
}}
>

View file

@ -1,17 +1,18 @@
import { useSession } from '@components/Contexts/SessionContext'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { updatePassword } from '@services/settings/password'
import { Formik, Form, Field } from 'formik'
import React, { useEffect } from 'react'
function UserEditPassword() {
const session = useSession() as any
const session = useLHSession() as any
const access_token = session.data.tokens.access_token;
const updatePasswordUI = async (values: any) => {
let user_id = session.user.id
await updatePassword(user_id, values)
let user_id = session.data.user.id
await updatePassword(user_id, values, access_token)
}
useEffect(() => {}, [session])
useEffect(() => { }, [session])
return (
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5">

View file

@ -3,27 +3,28 @@ 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, Ticket, User, UserSquare, Users, X } from 'lucide-react'
import { Globe, Ticket, UserSquare, Users, 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'
import Modal from '@components/StyledElements/Modal/Modal'
import OrgInviteCodeGenerate from '@components/Objects/Modals/Dash/OrgAccess/OrgInviteCodeGenerate'
import { useLHSession } from '@components/Contexts/LHSessionContext'
function OrgAccess() {
const org = useOrg() as any
const session = useLHSession() as any
const access_token = session.data.tokens.access_token;
const { data: invites } = useSWR(
org ? `${getAPIUrl()}orgs/${org?.id}/invites` : null,
swrFetcher
(url) => swrFetcher(url, access_token)
)
const [isLoading, setIsLoading] = React.useState(false)
const [joinMethod, setJoinMethod] = React.useState('closed')
@ -40,10 +41,8 @@ function OrgAccess() {
}
}
async function deleteInvite(invite: any) {
let res = await deleteInviteCode(org.id, invite.invite_code_uuid)
let res = await deleteInviteCode(org.id, invite.invite_code_uuid, access_token)
if (res.status == 200) {
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
} else {
@ -52,7 +51,7 @@ function OrgAccess() {
}
async function changeJoinMethod(method: 'open' | 'inviteOnly') {
let res = await changeSignupMechanism(org.id, method)
let res = await changeSignupMechanism(org.id, method, access_token)
if (res.status == 200) {
router.refresh()
mutate(`${getAPIUrl()}orgs/slug/${org?.slug}`)

View file

@ -1,4 +1,5 @@
'use client'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useOrg } from '@components/Contexts/OrgContext'
import AddUserGroup from '@components/Objects/Modals/Dash/OrgUserGroups/AddUserGroup'
import ManageUsers from '@components/Objects/Modals/Dash/OrgUserGroups/ManageUsers'
@ -14,17 +15,19 @@ import useSWR, { mutate } from 'swr'
function OrgUserGroups() {
const org = useOrg() as any
const session = useLHSession() as any
const access_token = session.data.tokens.access_token;
const [userGroupManagementModal, setUserGroupManagementModal] = React.useState(false)
const [createUserGroupModal, setCreateUserGroupModal] = React.useState(false)
const [selectedUserGroup, setSelectedUserGroup] = React.useState(null) as any
const { data: usergroups } = useSWR(
org ? `${getAPIUrl()}usergroups/org/${org.id}` : null,
swrFetcher
(url) => swrFetcher(url, access_token)
)
const deleteUserGroupUI = async (usergroup_id: any) => {
const res = await deleteUserGroup(usergroup_id)
const res = await deleteUserGroup(usergroup_id, access_token)
if (res.status == 200) {
mutate(`${getAPIUrl()}usergroups/org/${org.id}`)
}

View file

@ -1,3 +1,4 @@
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useOrg } from '@components/Contexts/OrgContext'
import PageLoading from '@components/Objects/Loaders/PageLoading'
import RolesUpdate from '@components/Objects/Modals/Dash/OrgUsers/RolesUpdate'
@ -14,9 +15,11 @@ import useSWR, { mutate } from 'swr'
function OrgUsers() {
const org = useOrg() as any
const session = useLHSession() as any
const access_token = session.data.tokens.access_token;
const { data: orgUsers } = useSWR(
org ? `${getAPIUrl()}orgs/${org?.id}/users` : null,
swrFetcher
(url) => swrFetcher(url, access_token)
)
const [rolesModal, setRolesModal] = React.useState(false)
const [selectedUser, setSelectedUser] = React.useState(null) as any
@ -28,7 +31,7 @@ function OrgUsers() {
}
const handleRemoveUser = async (user_id: any) => {
const res = await removeUserFromOrg(org.id, user_id)
const res = await removeUserFromOrg(org.id, user_id,access_token)
if (res.status === 200) {
await mutate(`${getAPIUrl()}orgs/${org.id}/users`)
} else {
@ -39,7 +42,6 @@ function OrgUsers() {
useEffect(() => {
if (orgUsers) {
setIsLoading(false)
console.log(orgUsers)
}
}, [org, orgUsers])

View file

@ -1,3 +1,4 @@
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useOrg } from '@components/Contexts/OrgContext'
import PageLoading from '@components/Objects/Loaders/PageLoading'
import Toast from '@components/StyledElements/Toast/Toast'
@ -5,20 +6,22 @@ import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
import { getAPIUrl } from '@services/config/config'
import { inviteBatchUsers } from '@services/organizations/invites'
import { swrFetcher } from '@services/utils/ts/requests'
import { Info, Shield, UserPlus } from 'lucide-react'
import { Info, UserPlus } from 'lucide-react'
import React, { useEffect } from 'react'
import toast from 'react-hot-toast'
import useSWR, { mutate } from 'swr'
function OrgUsersAdd() {
const org = useOrg() as any
const session = useLHSession() as any
const access_token = session.data.tokens.access_token;
const [isLoading, setIsLoading] = React.useState(false)
const [invitedUsers, setInvitedUsers] = React.useState('');
const [selectedInviteCode, setSelectedInviteCode] = React.useState('');
async function sendInvites() {
setIsLoading(true)
let res = await inviteBatchUsers(org.id, invitedUsers, selectedInviteCode)
let res = await inviteBatchUsers(org.id, invitedUsers, selectedInviteCode,access_token)
if (res.status == 200) {
mutate(`${getAPIUrl()}orgs/${org?.id}/invites/users`)
setIsLoading(false)
@ -31,18 +34,17 @@ function OrgUsersAdd() {
const { data: invites } = useSWR(
org ? `${getAPIUrl()}orgs/${org?.id}/invites` : null,
swrFetcher
(url) => swrFetcher(url, access_token)
)
const { data: invited_users } = useSWR(
org ? `${getAPIUrl()}orgs/${org?.id}/invites/users` : null,
swrFetcher
(url) => swrFetcher(url, access_token)
)
useEffect(() => {
if (invites) {
setSelectedInviteCode(invites?.[0]?.invite_code_uuid)
}
console.log('dev,',selectedInviteCode)
}
, [invites, invited_users])

View file

@ -1,40 +1,43 @@
import { useOrg } from '@components/Contexts/OrgContext'
import { useSession } from '@components/Contexts/SessionContext'
import { useEffect } from 'react'
function useAdminStatus() {
const session = useSession() as any
const org = useOrg() as any
// If session is not loaded, redirect to login
useEffect(() => {
if (session.isLoading) {
return
}
}
, [session])
const isUserAdmin = () => {
if (session.isAuthenticated) {
const isAdmin = session.roles.some((role: any) => {
return (
role.org.id === org.id &&
(role.role.id === 1 ||
role.role.id === 2 ||
role.role.role_uuid === 'role_global_admin' ||
role.role.role_uuid === 'role_global_maintainer')
)
})
return isAdmin
}
return false
}
// Return the user admin status
return isUserAdmin()
import { useOrg } from '@components/Contexts/OrgContext';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import { useEffect, useState, useMemo } from 'react';
interface Role {
org: { id: number };
role: { id: number; role_uuid: string };
}
export default useAdminStatus
function useAdminStatus() {
const session = useLHSession() as any;
const org = useOrg() as any;
const [isAdmin, setIsAdmin] = useState<boolean | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const userRoles = useMemo(() => session?.data?.roles || [], [session?.data?.roles]);
useEffect(() => {
if (session.status === 'authenticated' && org?.id) {
const isAdminVar = userRoles.some((role: Role) => {
return (
role.org.id === org.id &&
(
role.role.id === 1 ||
role.role.id === 2 ||
role.role.role_uuid === 'role_global_admin' ||
role.role.role_uuid === 'role_global_maintainer'
)
);
});
setIsAdmin(isAdminVar);
setLoading(false); // Set loading to false once the status is determined
} else {
setIsAdmin(false);
setLoading(false); // Set loading to false if not authenticated or org not found
}
}, [session.status, userRoles, org.id]);
return { isAdmin, loading };
}
export default useAdminStatus;

View file

@ -1,4 +1,4 @@
import { useSession } from '@components/Contexts/SessionContext'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import {
sendActivityAIChatMessage,
startActivityAIChatSession,
@ -74,7 +74,8 @@ type ActivityChatMessageBoxProps = {
}
function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
const session = useSession() as any
const session = useLHSession() as any
const access_token = session.data.tokens.access_token;
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes
const dispatchAIChatBot = useAIChatBotDispatch() as any
@ -115,7 +116,8 @@ function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
const response = await sendActivityAIChatMessage(
message,
aiChatBotState.aichat_uuid,
props.activity.activity_uuid
props.activity.activity_uuid,
access_token
)
if (response.success == false) {
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
@ -143,8 +145,9 @@ function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
})
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' })
const response = await startActivityAIChatSession(
message,
message,access_token,
props.activity.activity_uuid
)
if (response.success == false) {
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
@ -219,14 +222,12 @@ function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
/>
</div>
<div
className={`flex space-x-2 items-center -ml-[100px] ${
aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''
}`}
className={`flex space-x-2 items-center -ml-[100px] ${aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''
}`}
>
<Image
className={`outline outline-1 outline-neutral-200/20 rounded-lg ${
aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''
}`}
className={`outline outline-1 outline-neutral-200/20 rounded-lg ${aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''
}`}
width={24}
src={learnhouseAI_icon}
alt=""
@ -244,12 +245,11 @@ function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
</div>
</div>
<div
className={`w-100 h-0.5 bg-white/5 rounded-full mx-auto mb-3 ${
aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''
}`}
className={`w-100 h-0.5 bg-white/5 rounded-full mx-auto mb-3 ${aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''
}`}
></div>
{aiChatBotState.messages.length > 0 &&
!aiChatBotState.error.isError ? (
!aiChatBotState.error.isError ? (
<div className="flex-col h-[237px] w-full space-y-4 overflow-scroll scrollbar-w-2 scrollbar scrollbar-thumb-white/20 scrollbar-thumb-rounded-full scrollbar-track-rounded-full">
{aiChatBotState.messages.map(
(message: AIMessage, index: number) => {
@ -328,7 +328,7 @@ type AIMessageProps = {
}
function AIMessage(props: AIMessageProps) {
const session = useSession() as any
const session = useLHSession() as any
const words = props.message.message.split(' ')
@ -378,7 +378,7 @@ const AIMessagePlaceHolder = (props: {
activity_uuid: string
sendMessage: any
}) => {
const session = useSession() as any
const session = useLHSession() as any
const [feedbackModal, setFeedbackModal] = React.useState(false)
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes
@ -409,7 +409,7 @@ const AIMessagePlaceHolder = (props: {
<span className="items-center">Hello</span>
<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.data.user.username},</span>
</span>
<span>how can we help today ?</span>
</p>

View file

@ -12,7 +12,6 @@ function DocumentPdfActivity({
const org = useOrg() as any
React.useEffect(() => {
console.log(activity)
}, [activity, org])
return (

View file

@ -15,6 +15,7 @@ import {
startActivityAIChatSession,
} from '@services/ai/ai'
import useGetAIFeatures from '../../../../AI/Hooks/useGetAIFeatures'
import { useLHSession } from '@components/Contexts/LHSessionContext'
type AICanvaToolkitProps = {
editor: Editor
@ -92,6 +93,8 @@ function AIActionButton(props: {
label: string
activity: any
}) {
const session = useLHSession() as any
const access_token = session.data.tokens.access_token;
const dispatchAIChatBot = useAIChatBotDispatch() as any
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes
@ -132,7 +135,7 @@ function AIActionButton(props: {
const response = await sendActivityAIChatMessage(
message,
aiChatBotState.aichat_uuid,
props.activity.activity_uuid
props.activity.activity_uuid, access_token
)
if (response.success == false) {
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
@ -160,8 +163,7 @@ function AIActionButton(props: {
})
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' })
const response = await startActivityAIChatSession(
message,
props.activity.activity_uuid
message, access_token
)
if (response.success == false) {
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
@ -193,10 +195,10 @@ function AIActionButton(props: {
props.label === 'Explain'
? 'Explain a word or a sentence with AI'
: props.label === 'Summarize'
? 'Summarize a long paragraph or text with AI'
: props.label === 'Translate'
? 'Translate to different languages with AI'
: 'Give examples to understand better with AI'
? 'Summarize a long paragraph or text with AI'
: props.label === 'Translate'
? 'Translate to different languages with AI'
: 'Give examples to understand better with AI'
return (
<div className="flex space-x-2">
<ToolTip sideOffset={10} slateBlack content={tooltipLabel}>

View file

@ -7,22 +7,6 @@ function VideoActivity({ activity, course }: { activity: any; course: any }) {
const org = useOrg() as any
const [videoId, setVideoId] = React.useState('')
function getYouTubeEmbed(url: any) {
// Extract video ID from the YouTube URL
var videoId = url.match(
/(?:\?v=|\/embed\/|\/\d\/|\/vi\/|\/v\/|https?:\/\/(?:www\.)?youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([^#\&\?\/]+)/
)[1]
// Create the embed object
var embedObject = {
videoId: videoId,
width: 560,
height: 315,
}
return embedObject
}
React.useEffect(() => {
if (activity && activity.content && activity.content.uri) {
var getYouTubeID = require('get-youtube-id');

View file

@ -20,12 +20,15 @@ import toast from 'react-hot-toast'
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { useLHSession } from '@components/Contexts/LHSessionContext'
dayjs.extend(relativeTime);
function CourseUpdates() {
const course = useCourse() as any;
const { data: updates } = useSWR(`${getAPIUrl()}courses/${course?.courseStructure.course_uuid}/updates`, swrFetcher)
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const { data: updates } = useSWR(`${getAPIUrl()}courses/${course?.courseStructure.course_uuid}/updates`, (url) => swrFetcher(url, access_token))
const [isModelOpen, setIsModelOpen] = React.useState(false)
function handleModelOpen() {
@ -35,7 +38,6 @@ function CourseUpdates() {
// if user clicks outside the model, close the model
React.useLayoutEffect(() => {
function handleClickOutside(event: any) {
console.log(event.target.id)
if (event.target.closest('.bg-white') || event.target.id === 'delete-update-button') return;
setIsModelOpen(false);
}
@ -71,7 +73,7 @@ function CourseUpdates() {
const UpdatesSection = () => {
const [selectedView, setSelectedView] = React.useState('list')
const isAdmin = useAdminStatus() as boolean;
const adminStatus = useAdminStatus() ;
return (
<div className='bg-white/95 backdrop-blur-md nice-shadow rounded-lg w-[700px] overflow-hidden'>
<div className='bg-gray-50/70 flex justify-between outline outline-1 rounded-lg outline-neutral-200/40'>
@ -80,7 +82,7 @@ const UpdatesSection = () => {
<span>Updates</span>
</div>
{isAdmin && <div
{adminStatus.isAdmin && <div
onClick={() => setSelectedView('new')}
className='py-2 px-4 space-x-2 items-center flex cursor-pointer text-xs font-medium hover:bg-gray-200 bg-gray-100 outline outline-1 outline-neutral-200/40'>
<PencilLine size={14} />
@ -98,6 +100,7 @@ const UpdatesSection = () => {
const NewUpdateForm = ({ setSelectedView }: any) => {
const org = useOrg() as any;
const course = useCourse() as any;
const session = useLHSession() as any;
const validate = (values: any) => {
const errors: any = {}
@ -124,7 +127,7 @@ const NewUpdateForm = ({ setSelectedView }: any) => {
course_uuid: course.courseStructure.course_uuid,
org_id: org.id
}
const res = await createCourseUpdate(body)
const res = await createCourseUpdate(body, session.data?.tokens?.access_token)
if (res.status === 200) {
toast.success('Update added successfully')
setSelectedView('list')
@ -192,23 +195,25 @@ const NewUpdateForm = ({ setSelectedView }: any) => {
const UpdatesListView = () => {
const course = useCourse() as any;
const isAdmin = useAdminStatus() as boolean;
const { data: updates } = useSWR(`${getAPIUrl()}courses/${course?.courseStructure.course_uuid}/updates`, swrFetcher)
const adminStatus = useAdminStatus() ;
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const { data: updates } = useSWR(`${getAPIUrl()}courses/${course?.courseStructure?.course_uuid}/updates`, (url) => swrFetcher(url, access_token))
return (
<div className='px-5 bg-white overflow-y-auto' style={{ maxHeight: '400px' }}>
{updates && updates.map((update: any) => (
{updates && !adminStatus.loading && updates.map((update: any) => (
<div key={update.id} className='py-2 border-b border-neutral-200 antialiased'>
<div className='font-bold text-gray-500 flex space-x-2 items-center justify-between '>
<div className='flex space-x-2 items-center'>
<span> {update.title}</span>
<span
title={"Created at " + dayjs(update.creation_date).format('MMMM D, YYYY')}
className='text-xs font-semibold text-gray-300'>
{dayjs(update.creation_date).fromNow()}
<span
title={"Created at " + dayjs(update.creation_date).format('MMMM D, YYYY')}
className='text-xs font-semibold text-gray-300'>
{dayjs(update.creation_date).fromNow()}
</span>
</div>
{isAdmin && <DeleteUpdateButton update={update} />}</div>
{adminStatus.isAdmin && !adminStatus.loading && <DeleteUpdateButton update={update} />}</div>
<div className='text-gray-600'>{update.content}</div>
</div>
))}
@ -223,11 +228,12 @@ const UpdatesListView = () => {
}
const DeleteUpdateButton = ({ update }: any) => {
const session = useLHSession() as any;
const course = useCourse() as any;
const org = useOrg() as any;
const handleDelete = async () => {
const res = await deleteCourseUpdate(course.courseStructure.course_uuid, update.courseupdate_uuid)
const res = await deleteCourseUpdate(course.courseStructure.course_uuid, update.courseupdate_uuid, session.data?.tokens?.access_token)
if (res.status === 200) {
toast.success('Update deleted successfully')
mutate(`${getAPIUrl()}courses/${course?.courseStructure.course_uuid}/updates`)

View file

@ -24,6 +24,7 @@ import {
startActivityAIChatSession,
} from '@services/ai/ai'
import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures'
import { useLHSession } from '@components/Contexts/LHSessionContext'
type AIEditorToolkitProps = {
editor: Editor
@ -32,11 +33,11 @@ type AIEditorToolkitProps = {
type AIPromptsLabels = {
label:
| 'Writer'
| 'ContinueWriting'
| 'MakeLonger'
| 'GenerateQuiz'
| 'Translate'
| 'Writer'
| 'ContinueWriting'
| 'MakeLonger'
| 'GenerateQuiz'
| 'Translate'
selection: string
}
@ -141,6 +142,8 @@ function AIEditorToolkit(props: AIEditorToolkitProps) {
const UserFeedbackModal = (props: AIEditorToolkitProps) => {
const dispatchAIEditor = useAIEditorDispatch() as any
const aiEditorState = useAIEditor() as AIEditorStateTypes
const session = useLHSession() as any
const access_token = session.data.tokens.access_token;
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
await dispatchAIEditor({
@ -159,7 +162,7 @@ const UserFeedbackModal = (props: AIEditorToolkitProps) => {
const response = await sendActivityAIChatMessage(
message,
aiEditorState.aichat_uuid,
props.activity.activity_uuid
props.activity.activity_uuid, access_token
)
if (response.success === false) {
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' })
@ -191,7 +194,7 @@ const UserFeedbackModal = (props: AIEditorToolkitProps) => {
})
await dispatchAIEditor({ type: 'setIsWaitingForResponse' })
const response = await startActivityAIChatSession(
message,
message, access_token,
props.activity.activity_uuid
)
if (response.success === false) {

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'
import UserAvatar from '../UserAvatar'
import { useSession } from '@components/Contexts/SessionContext'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { getUserAvatarMediaDirectory } from '@services/media/media';
import { getCollaborationServerUrl } from '@services/config/config';
import { useOrg } from '@components/Contexts/OrgContext';
@ -11,7 +11,7 @@ type ActiveAvatarsProps = {
}
function ActiveAvatars(props: ActiveAvatarsProps) {
const session = useSession() as any;
const session = useLHSession() as any;
const org = useOrg() as any;
const [activeUsers, setActiveUsers] = useState({} as any);
@ -27,11 +27,11 @@ function ActiveAvatars(props: ActiveAvatarsProps) {
});
// Remove the current user from the list
delete users[session.user.user_uuid];
delete users[session.data.user.user_uuid];
setActiveUsers(users);
}
, [props.mouseMovements, session.user, org]);
, [props.mouseMovements, session.data.user, org]);
return (
@ -50,7 +50,7 @@ function ActiveAvatars(props: ActiveAvatarsProps) {
<div className="h-2 w-2 rounded-full" style={{ position: 'absolute', bottom: -5, right: 16, backgroundColor: props.mouseMovements[key].color }} />
</div>
))}
{session.isAuthenticated && (
{session.status && (
<div className='z-50'>
<UserAvatar
width={40}

View file

@ -41,11 +41,9 @@ import html from 'highlight.js/lib/languages/xml'
import python from 'highlight.js/lib/languages/python'
import java from 'highlight.js/lib/languages/java'
import { CourseProvider } from '@components/Contexts/CourseContext'
import { useSession } from '@components/Contexts/SessionContext'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import AIEditorToolkit from './AI/AIEditorToolkit'
import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures'
import UserAvatar from '../UserAvatar'
import randomColor from 'randomcolor'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import ActiveAvatars from './ActiveAvatars'
@ -65,7 +63,7 @@ interface Editor {
}
function Editor(props: Editor) {
const session = useSession() as any
const session = useLHSession() as any
const dispatchAIEditor = useAIEditorDispatch() as any
const aiEditorState = useAIEditor() as AIEditorStateTypes
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'editor' })
@ -145,7 +143,7 @@ function Editor(props: Editor) {
CollaborationCursor.configure({
provider: props.hocuspocusProvider,
user: {
name: props.session.user.first_name + ' ' + props.session.user.last_name,
name: props.session.data.user.first_name + ' ' + props.session.data.user.last_name,
color: props.userRandomColor,
},
}),

View file

@ -5,7 +5,7 @@ import { updateActivity } from '@services/courses/activities'
import { toast } from 'react-hot-toast'
import Toast from '@components/StyledElements/Toast/Toast'
import { OrgProvider } from '@components/Contexts/OrgContext'
import { useSession } from '@components/Contexts/SessionContext'
import { useLHSession } from '@components/Contexts/LHSessionContext'
// Collaboration
import { HocuspocusProvider } from '@hocuspocus/provider'
@ -24,7 +24,8 @@ interface EditorWrapperProps {
}
function EditorWrapper(props: EditorWrapperProps): JSX.Element {
const session = useSession() as any
const session = useLHSession() as any
const access_token = session.data.tokens.access_token;
// Define provider in the state
const [provider, setProvider] = React.useState<HocuspocusProvider | null>(null);
const [thisPageColor, setThisPageColor] = useState(randomColor({ luminosity: 'light' }) as string)
@ -57,7 +58,7 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element {
document.addEventListener("mousemove", (event) => {
// Share any information you like
provider?.setAwarenessField("userMouseMovement", {
user: session.user,
user: session.data.user,
mouseX: event.clientX,
mouseY: event.clientY,
color: thisPageColor,
@ -72,14 +73,14 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element {
provider?.setAwarenessField("savings_states", {
[session.user.user_uuid]: {
[session.data.user.user_uuid]: {
status: 'action_save',
timestamp: new Date().toISOString(),
user: session.user
user: session.data.user
}
});
toast.promise(updateActivity(activity, activity.activity_uuid), {
toast.promise(updateActivity(activity, activity.activity_uuid,access_token), {
loading: 'Saving...',
success: <b>Activity saved!</b>,
error: <b>Could not save.</b>,

View file

@ -9,11 +9,14 @@ import { getActivityBlockMediaDirectory } from '@services/media/media'
import { useOrg } from '@components/Contexts/OrgContext'
import { useCourse } from '@components/Contexts/CourseContext'
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
import { useLHSession } from '@components/Contexts/LHSessionContext'
function ImageBlockComponent(props: any) {
const org = useOrg() as any
const course = useCourse() as any
const editorState = useEditorProvider() as any
const session = useLHSession() as any
const access_token = session.data.tokens.access_token;
const isEditable = editorState.isEditable
const [image, setImage] = React.useState(null)
@ -36,7 +39,7 @@ function ImageBlockComponent(props: any) {
setIsLoading(true)
let object = await uploadNewImageFile(
image,
props.extension.options.activity.activity_uuid
props.extension.options.activity.activity_uuid,access_token
)
setIsLoading(false)
setblockObject(object)

View file

@ -8,10 +8,13 @@ import { getActivityBlockMediaDirectory } from '@services/media/media'
import { useOrg } from '@components/Contexts/OrgContext'
import { useCourse } from '@components/Contexts/CourseContext'
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
import { useLHSession } from '@components/Contexts/LHSessionContext'
function PDFBlockComponent(props: any) {
const org = useOrg() as any
const course = useCourse() as any
const session = useLHSession() as any
const access_token = session.data.tokens.access_token;
const [pdf, setPDF] = React.useState(null)
const [isLoading, setIsLoading] = React.useState(false)
const [blockObject, setblockObject] = React.useState(
@ -32,7 +35,7 @@ function PDFBlockComponent(props: any) {
setIsLoading(true)
let object = await uploadNewPDFFile(
pdf,
props.extension.options.activity.activity_uuid
props.extension.options.activity.activity_uuid, access_token
)
setIsLoading(false)
setblockObject(object)
@ -41,7 +44,7 @@ function PDFBlockComponent(props: any) {
})
}
useEffect(() => {}, [course, org])
useEffect(() => { }, [course, org])
return (
<NodeViewWrapper className="block-pdf">

View file

@ -86,10 +86,8 @@ function QuizBlockComponent(props: any) {
if (allCorrect) {
setSubmissionMessage('All answers are correct!')
console.log('All answers are correct!')
} else {
setSubmissionMessage('Some answers are incorrect!')
console.log('Some answers are incorrect!')
}
}

View file

@ -8,6 +8,7 @@ import { UploadIcon } from '@radix-ui/react-icons'
import { useOrg } from '@components/Contexts/OrgContext'
import { useCourse } from '@components/Contexts/CourseContext'
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
import { useLHSession } from '@components/Contexts/LHSessionContext'
function VideoBlockComponents(props: any) {
const org = useOrg() as any
@ -15,6 +16,8 @@ function VideoBlockComponents(props: any) {
const editorState = useEditorProvider() as any
const isEditable = editorState.isEditable
const [video, setVideo] = React.useState(null)
const session = useLHSession() as any
const access_token = session.data.tokens.access_token;
const [isLoading, setIsLoading] = React.useState(false)
const [blockObject, setblockObject] = React.useState(
props.node.attrs.blockObject
@ -32,7 +35,7 @@ function VideoBlockComponents(props: any) {
setIsLoading(true)
let object = await uploadNewVideoFile(
video,
props.extension.options.activity.activity_uuid
props.extension.options.activity.activity_uuid, access_token
)
setIsLoading(false)
setblockObject(object)
@ -41,7 +44,7 @@ function VideoBlockComponents(props: any) {
})
}
useEffect(() => {}, [course, org])
useEffect(() => { }, [course, org])
return (
<NodeViewWrapper className="block-video">
@ -98,7 +101,7 @@ function VideoBlockComponents(props: any) {
)
}
const BlockVideoWrapper = styled.div`
//border: ${(props) =>
border: ${(props) =>
props.contentEditable ? '2px dashed #713f1117' : 'none'};
// center

View file

@ -1,21 +1,19 @@
'use client'
import React from 'react'
import Link from 'next/link'
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
import { getUriWithOrg } from '@services/config/config'
import { HeaderProfileBox } from '@components/Security/HeaderProfileBox'
import MenuLinks from './MenuLinks'
import { getOrgLogoMediaDirectory } from '@services/media/media'
import useSWR from 'swr'
import { swrFetcher } from '@services/utils/ts/requests'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useOrg } from '@components/Contexts/OrgContext'
export const Menu = (props: any) => {
const orgslug = props.orgslug
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const [feedbackModal, setFeedbackModal] = React.useState(false)
const {
data: org,
error: error,
isLoading,
} = useSWR(`${getAPIUrl()}orgs/slug/${orgslug}`, swrFetcher)
const org = useOrg() as any;
function closeFeedbackModal() {
setFeedbackModal(false)

View file

@ -15,9 +15,11 @@ import React, { useState } from 'react'
import { BarLoader } from 'react-spinners'
import { revalidateTags } from '@services/utils/ts/requests'
import { useRouter } from 'next/navigation'
import { useLHSession } from '@components/Contexts/LHSessionContext'
function CreateCourseModal({ closeModal, orgslug }: any) {
const [isSubmitting, setIsSubmitting] = useState(false)
const session = useLHSession() as any;
const [name, setName] = React.useState('')
const [description, setDescription] = React.useState('')
const [learnings, setLearnings] = React.useState('')
@ -53,7 +55,6 @@ function CreateCourseModal({ closeModal, orgslug }: any) {
const handleVisibilityChange = (event: React.ChangeEvent<any>) => {
setVisibility(event.target.value)
console.log(visibility)
}
const handleTagsChange = (event: React.ChangeEvent<any>) => {
@ -71,7 +72,8 @@ function CreateCourseModal({ closeModal, orgslug }: any) {
let status = await createNewCourse(
orgId,
{ name, description, tags, visibility },
thumbnail
thumbnail,
session.data?.tokens?.access_token
)
await revalidateTags(['courses'], orgslug)
setIsSubmitting(false)
@ -80,9 +82,6 @@ function CreateCourseModal({ closeModal, orgslug }: any) {
closeModal()
router.refresh()
await revalidateTags(['courses'], orgslug)
// refresh page (FIX for Next.js BUG)
// window.location.reload();
} else {
alert('Error creating course, please see console logs')
}

View file

@ -1,10 +1,11 @@
'use client';
import { useCourse } from '@components/Contexts/CourseContext';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import { useOrg } from '@components/Contexts/OrgContext';
import { getAPIUrl } from '@services/config/config';
import { linkResourcesToUserGroup } from '@services/usergroups/usergroups';
import { swrFetcher } from '@services/utils/ts/requests';
import { AlertTriangle, Info } from 'lucide-react';
import { Info } from 'lucide-react';
import React, { useEffect } from 'react'
import toast from 'react-hot-toast';
import useSWR, { mutate } from 'swr'
@ -17,6 +18,8 @@ type LinkToUserGroupProps = {
function LinkToUserGroup(props: LinkToUserGroupProps) {
const course = useCourse() as any
const org = useOrg() as any
const session = useLHSession() as any
const access_token = session.data.tokens.access_token;
const courseStructure = course.courseStructure
const { data: usergroups } = useSWR(
@ -27,8 +30,7 @@ function LinkToUserGroup(props: LinkToUserGroupProps) {
const handleLink = async () => {
console.log('selectedUserGroup', selectedUserGroup)
const res = await linkResourcesToUserGroup(selectedUserGroup, courseStructure.course_uuid)
const res = await linkResourcesToUserGroup(selectedUserGroup, courseStructure.course_uuid, access_token)
if (res.status === 200) {
props.setUserGroupModal(false)
toast.success('Successfully linked to usergroup')
@ -37,7 +39,6 @@ function LinkToUserGroup(props: LinkToUserGroupProps) {
else {
toast.error('Error ' + res.status + ': ' + res.data.detail)
}
}
useEffect(() => {
@ -54,25 +55,25 @@ function LinkToUserGroup(props: LinkToUserGroupProps) {
<h1 className=' font-medium'>Users that are not part of the UserGroup will no longer have access to this course</h1>
</div>
<div className='p-4 flex-row flex justify-between items-center'>
<div className='py-1'>
<span className='px-3 text-gray-400 font-bold rounded-full py-1 bg-gray-100 mx-3'>UserGroup Name </span>
<select
onChange={(e) => setSelectedUserGroup(e.target.value)}
defaultValue={selectedUserGroup}
>
{usergroups && usergroups.map((group: any) => (
<option key={group.id} value={group.id}>{group.name}</option>
))}
</select>
</div>
<div className='py-3'>
<button onClick={() => { handleLink() }} className='bg-green-700 text-white font-bold px-4 py-2 rounded-md shadow'>Link</button>
<div className='py-1'>
<span className='px-3 text-gray-400 font-bold rounded-full py-1 bg-gray-100 mx-3'>UserGroup Name </span>
<select
onChange={(e) => setSelectedUserGroup(e.target.value)}
defaultValue={selectedUserGroup}
>
{usergroups && usergroups.map((group: any) => (
<option key={group.id} value={group.id}>{group.name}</option>
))}
</select>
</div>
<div className='py-3'>
<button onClick={() => { handleLink() }} className='bg-green-700 text-white font-bold px-4 py-2 rounded-md shadow'>Link</button>
</div>
</div>
</div>
</div>
)
}

View file

@ -2,7 +2,8 @@ import { useOrg } from '@components/Contexts/OrgContext'
import { getAPIUrl } from '@services/config/config'
import { createInviteCode, createInviteCodeWithUserGroup } from '@services/organizations/invites'
import { swrFetcher } from '@services/utils/ts/requests'
import { Shield, Ticket } from 'lucide-react'
import { Ticket } from 'lucide-react'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import React, { useEffect } from 'react'
import toast from 'react-hot-toast'
import useSWR, { mutate } from 'swr'
@ -13,6 +14,8 @@ type OrgInviteCodeGenerateProps = {
function OrgInviteCodeGenerate(props: OrgInviteCodeGenerateProps) {
const org = useOrg() as any
const session = useLHSession() as any
const access_token = session.data.tokens.access_token;
const [usergroup_id, setUsergroup_id] = React.useState(0);
const { data: usergroups } = useSWR(
org ? `${getAPIUrl()}usergroups/org/${org.id}` : null,
@ -20,7 +23,7 @@ function OrgInviteCodeGenerate(props: OrgInviteCodeGenerateProps) {
)
async function createInviteWithUserGroup() {
let res = await createInviteCodeWithUserGroup(org.id, usergroup_id)
let res = await createInviteCodeWithUserGroup(org.id, usergroup_id, session.data?.tokens?.access_token)
if (res.status == 200) {
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
props.setInvitesModal(false)
@ -30,7 +33,7 @@ function OrgInviteCodeGenerate(props: OrgInviteCodeGenerateProps) {
}
async function createInvite() {
let res = await createInviteCode(org.id)
let res = await createInviteCode(org.id, session.data?.tokens?.access_token)
if (res.status == 200) {
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
props.setInvitesModal(false)

View file

@ -14,13 +14,16 @@ import { BarLoader } from 'react-spinners'
import { createUserGroup } from '@services/usergroups/usergroups'
import { mutate } from 'swr'
import { getAPIUrl } from '@services/config/config'
import { useLHSession } from '@components/Contexts/LHSessionContext'
type AddUserGroupProps = {
setCreateUserGroupModal: any
}
function AddUserGroup(props: AddUserGroupProps) {
const org = useOrg() as any
const org = useOrg() as any;
const session = useLHSession() as any
const access_token = session.data.tokens.access_token;
const [userGroupName, setUserGroupName] = React.useState('')
const [userGroupDescription, setUserGroupDescription] = React.useState('')
const [isSubmitting, setIsSubmitting] = React.useState(false)
@ -42,7 +45,7 @@ function AddUserGroup(props: AddUserGroupProps) {
description: userGroupDescription,
org_id: org.id
}
const res = await createUserGroup(obj)
const res = await createUserGroup(obj, access_token)
if (res.status == 200) {
setIsSubmitting(false)
mutate(`${getAPIUrl()}usergroups/org/${org.id}`)

Some files were not shown because too many files have changed in this diff Show more