feat: init new auth

This commit is contained in:
swve 2024-05-25 14:56:28 +02:00
parent f838a8512c
commit 1708b36818
34 changed files with 1853 additions and 3613 deletions

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, PublicUser, 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 get_google_user_info, 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

@ -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

@ -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

@ -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,92 @@
import {
getNewAccessTokenUsingRefreshTokenServer,
getUserSession,
loginAndGetToken,
loginWithOAuthToken,
} from '@services/auth/auth'
import { getResponseMetadata } from '@services/utils/ts/requests'
import CredentialsProvider from 'next-auth/providers/credentials'
import GoogleProvider from 'next-auth/providers/google'
export const nextAuthOptions = {
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 || '',
}),
],
callbacks: {
async jwt({ token, user, account }) {
// 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 }) {
// 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

@ -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

@ -6,7 +6,6 @@ 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'
const EditorWrapper = dynamic(() => import('@components/Objects/Editor/EditorWrapper'), { ssr: false })
@ -58,14 +57,12 @@ const EditActivity = async (params: any) => {
return (
<EditorOptionsProvider options={{ isEditable: true }}>
<AIEditorProvider>
<SessionProvider>
<EditorWrapper
org={org}
course={courseInfo}
activity={activity}
content={activity.content}
></EditorWrapper>
</SessionProvider>
</AIEditorProvider>
</EditorOptionsProvider>
)

View file

@ -1,8 +1,8 @@
'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'
export default function RootLayout({
children,
@ -18,6 +18,7 @@ export default function RootLayout({
<html className="" lang="en">
<head />
<body>
<SessionProvider>
<StyledComponentsRegistry>
<motion.main
variants={variants} // Pass the variant object into Framer Motion
@ -30,6 +31,7 @@ export default function RootLayout({
{children}
</motion.main>
</StyledComponentsRegistry>
</SessionProvider>
</body>
</html>
)

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,7 +1,8 @@
import SessionProvider from '@components/Contexts/SessionContext'
import LeftMenu from '@components/Dashboard/UI/LeftMenu'
import AdminAuthorization from '@components/Security/AdminAuthorization'
import ClientComponentSkeleton from '@components/Utils/ClientComp'
import { Metadata } from 'next'
import { SessionProvider } from 'next-auth/react'
import React from 'react'
export const metadata: Metadata = {
@ -17,14 +18,12 @@ function DashboardLayout({
}) {
return (
<>
<SessionProvider>
<AdminAuthorization authorizationMode="page">
<div className="flex">
<LeftMenu />
<div className="flex w-full">{children}</div>
</div>
</AdminAuthorization>
</SessionProvider>
</>
)
}

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 { useSession } from 'next-auth/react'
export type SettingsParams = {
subpage: string

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 { useSession } from 'next-auth/react'
import { useOrg } from '@components/Contexts/OrgContext'
import OrgUsers from '@components/Dashboard/Users/OrgUsers/OrgUsers'
import OrgAccess from '@components/Dashboard/Users/OrgAccess/OrgAccess'

View file

@ -1,8 +1,8 @@
'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'
import { SessionProvider } from 'next-auth/react'
export default function RootLayout({
children,

View file

@ -14,6 +14,7 @@ import { loginAndGetToken } from '@services/auth/auth'
import { AlertTriangle } from 'lucide-react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { signIn, useSession } from "next-auth/react"
import { getUriWithOrg } from '@services/config/config'
interface LoginClientProps {
@ -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 = useSession();
const [error, setError] = React.useState('')
const formik = useFormik({
initialValues: {
@ -50,25 +53,23 @@ 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) {
//let res = await loginAndGetToken(values.email, values.password)
const res = await signIn('credentials', {
redirect: false,
email: values.email,
password: values.password,
});
if (res && res.error) {
setError("Wrong Email or password");
setIsSubmitting(false);
}
else {
router.push(`/`)
setIsSubmitting(false)
} else if (
res.status == 401 ||
res.status == 400 ||
res.status == 404 ||
res.status == 409
) {
setError(message.detail)
setIsSubmitting(false)
} else {
setError('Something went wrong')
setIsSubmitting(false)
}
},
})
return (
<div className="grid grid-flow-col justify-stretch h-screen">
<div
@ -165,7 +166,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 +174,11 @@ const LoginClient = (props: LoginClientProps) => {
</Form.Submit>
</div>
</FormLayout>
<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>
</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 = {}
@ -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

@ -4,7 +4,7 @@ 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 { useSession } from 'next-auth/react'
import React, { useEffect } from 'react'
import { MailWarning, Shield, Ticket, UserPlus } from 'lucide-react'
import { useOrg } from '@components/Contexts/OrgContext'
@ -92,14 +92,14 @@ 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 ? (
session.status == 'authenticated' ? (
<LoggedInJoinScreen />
) : (
<InviteOnlySignUpComponent inviteCode={inviteCode} />
@ -130,7 +130,7 @@ const LoggedInJoinScreen = (props: any) => {
<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>
@ -157,7 +157,7 @@ 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(

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,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

@ -1,6 +1,6 @@
'use client'
import { useOrg } from '@components/Contexts/OrgContext'
import { useSession } from '@components/Contexts/SessionContext'
import { useSession } from 'next-auth/react'
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
import LearnHouseDashboardLogo from '@public/dashLogo.png'
import { logout } from '@services/auth/auth'
@ -123,7 +123,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 +134,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

@ -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 { useSession } from 'next-auth/react'
import {
ArrowBigUpDash,
Check,
@ -23,7 +24,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)
// wait for 1 second to show loading animation
await new Promise((r) => setTimeout(r, 1500))
if (res.success === false) {
@ -35,24 +36,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)
}, 400)
}}
>

View file

@ -1,4 +1,4 @@
import { useSession } from '@components/Contexts/SessionContext'
import { useSession } from 'next-auth/react'
import { updatePassword } from '@services/settings/password'
import { Formik, Form, Field } from 'formik'
import React, { useEffect } from 'react'
@ -7,7 +7,7 @@ function UserEditPassword() {
const session = useSession() as any
const updatePasswordUI = async (values: any) => {
let user_id = session.user.id
let user_id = session.data.user.id
await updatePassword(user_id, values)
}

View file

@ -1,15 +1,18 @@
import { useOrg } from '@components/Contexts/OrgContext'
import { useSession } from '@components/Contexts/SessionContext'
import { useSession } from 'next-auth/react'
import { useEffect } from 'react'
function useAdminStatus() {
const session = useSession() as any
const org = useOrg() as any
console.log('useAdminStatus', {
session,
})
// If session is not loaded, redirect to login
useEffect(() => {
if (session.isLoading) {
if (session.status == 'loading') {
return
}
@ -17,8 +20,8 @@ function useAdminStatus() {
, [session])
const isUserAdmin = () => {
if (session.isAuthenticated) {
const isAdmin = session.roles.some((role: any) => {
if (session.status == 'authenticated') {
const isAdmin = session?.data?.roles.some((role: any) => {
return (
role.org.id === org.id &&
(role.role.id === 1 ||

View file

@ -1,4 +1,4 @@
import { useSession } from '@components/Contexts/SessionContext'
import { useSession } from 'next-auth/react'
import {
sendActivityAIChatMessage,
startActivityAIChatSession,
@ -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

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'
import UserAvatar from '../UserAvatar'
import { useSession } from '@components/Contexts/SessionContext'
import { useSession } from 'next-auth/react'
import { getUserAvatarMediaDirectory } from '@services/media/media';
import { getCollaborationServerUrl } from '@services/config/config';
import { useOrg } from '@components/Contexts/OrgContext';
@ -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,7 +41,7 @@ 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 { useSession } from 'next-auth/react'
import AIEditorToolkit from './AI/AIEditorToolkit'
import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures'
import UserAvatar from '../UserAvatar'
@ -145,7 +145,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 { useSession } from 'next-auth/react'
// Collaboration
import { HocuspocusProvider } from '@hocuspocus/provider'
@ -57,7 +57,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,10 +72,10 @@ 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
}
});

View file

@ -1,8 +1,8 @@
import { useSession } from '@components/Contexts/SessionContext'
import React, { useEffect } from 'react'
import { getUriWithOrg } from '@services/config/config'
import { useParams } from 'next/navigation'
import { getUserAvatarMediaDirectory } from '@services/media/media'
import { useSession } from 'next-auth/react'
type UserAvatarProps = {
width?: number
@ -23,6 +23,10 @@ function UserAvatar(props: UserAvatarProps) {
const session = useSession() as any
const params = useParams() as any
function checkUrlProtocol(url: string): boolean {
return url.startsWith('https://') || url.startsWith('http://');
}
const predefinedAvatarFunc = () => {
if (props.predefined_avatar === 'ai') {
return getUriWithOrg(params.orgslug, '/ai_avatar.png')
@ -35,10 +39,10 @@ function UserAvatar(props: UserAvatarProps) {
const predefinedAvatar = predefinedAvatarFunc()
const emptyAvatar = getUriWithOrg(params.orgslug, '/empty_avatar.png') as any
const uploadedAvatar = getUserAvatarMediaDirectory(
session.user.user_uuid,
session.user.avatar_image
) as any
const uploadedAvatar = (session.status == 'authenticated') && (checkUrlProtocol(session?.data?.user?.avatar_image)) ? session?.data?.user?.avatar_image : getUserAvatarMediaDirectory(
session?.data?.user?.user_uuid,
session?.data?.user?.avatar_image
)
const useAvatar = () => {
if (props.predefined_avatar) {
@ -47,7 +51,7 @@ function UserAvatar(props: UserAvatarProps) {
if (props.avatar_url) {
return props.avatar_url
} else {
if (session.user.avatar_image) {
if (session?.data?.user?.avatar_image) {
return uploadedAvatar
} else {
return emptyAvatar
@ -58,7 +62,9 @@ function UserAvatar(props: UserAvatarProps) {
useEffect(() => {
}, [session])
}, [session.status])
return (
<img
@ -66,12 +72,9 @@ function UserAvatar(props: UserAvatarProps) {
width={props.width ? props.width : 50}
height={props.width ? props.width : 50}
src={useAvatar()}
className={`${
props.avatar_url && session.user.avatar_image ? '' : 'bg-gray-700'
} ${props.border ? 'border ' + props.border : ''} ${
props.borderColor ? props.borderColor : 'border-white'
} shadow-xl aspect-square w-[${props.width ? props.width : 50}px] h-[${
props.width ? props.width : 50
className={`${props.avatar_url && session?.data?.user?.avatar_image ? '' : 'bg-gray-700'
} ${props.border ? 'border ' + props.border : ''} ${props.borderColor ? props.borderColor : 'border-white'
} shadow-xl aspect-square w-[${props.width ? props.width : 50}px] h-[${props.width ? props.width : 50
}px] ${props.rounded ? props.rounded : 'rounded-xl'}`}
/>
)

View file

@ -1,6 +1,6 @@
'use client'
import { useOrg } from '@components/Contexts/OrgContext'
import { useSession } from '@components/Contexts/SessionContext'
import { useSession } from 'next-auth/react'
import useAdminStatus from '@components/Hooks/useAdminStatus'
import { usePathname, useRouter } from 'next/navigation'
import React from 'react'
@ -33,7 +33,7 @@ function AdminAuthorization(props: AuthorizationProps) {
// Verify if the user is authenticated
const isUserAuthenticated = () => {
if (session.isAuthenticated === true) {
if (session.status === 'authenticated') {
return true
} else {
return false
@ -59,7 +59,6 @@ function AdminAuthorization(props: AuthorizationProps) {
if (props.authorizationMode === 'page') {
// Check if user is in an admin path
if (ADMIN_PATHS.some((path) => checkPathname(path, pathname))) {
console.log('Admin path')
if (isUserAuthenticated()) {
// Check if the user is an Admin
if (isUserAdmin) {
@ -92,7 +91,7 @@ function AdminAuthorization(props: AuthorizationProps) {
}
React.useEffect(() => {
if (session.isLoading) {
if (session.status == 'loading') {
return
}

View file

@ -1,6 +1,6 @@
'use client'
import React from 'react'
import { useSession } from '@components/Contexts/SessionContext'
import { useSession } from 'next-auth/react'
import { useOrg } from '@components/Contexts/OrgContext'
interface AuthenticatedClientElementProps {
@ -49,19 +49,19 @@ export const AuthenticatedClientElement = (
}
function check() {
if (session.isAuthenticated === false) {
if (session.status == 'authenticated') {
setIsAllowed(false)
return
} else {
if (props.checkMethod === 'authentication') {
setIsAllowed(session.isAuthenticated)
} else if (props.checkMethod === 'roles') {
setIsAllowed(session.status == 'authenticated')
} else if (props.checkMethod === 'roles' && session.status == 'authenticated') {
return setIsAllowed(
isUserAllowed(
session.roles,
session?.data?.roles,
props.action!,
props.ressourceType!,
org.org_uuid
org?.org_uuid
)
)
}
@ -69,7 +69,7 @@ export const AuthenticatedClientElement = (
}
React.useEffect(() => {
if (session.isLoading) {
if (session.status == 'loading') {
return
}

View file

@ -3,21 +3,22 @@ import React, { useEffect } from 'react'
import styled from 'styled-components'
import Link from 'next/link'
import { Settings } from 'lucide-react'
import { useSession } from '@components/Contexts/SessionContext'
import UserAvatar from '@components/Objects/UserAvatar'
import useAdminStatus from '@components/Hooks/useAdminStatus'
import { useSession } from 'next-auth/react'
export const HeaderProfileBox = () => {
const session = useSession() as any
const isUserAdmin = useAdminStatus() as any
useEffect(() => {
console.log(session)
}
, [session])
return (
<ProfileArea>
{!session.isAuthenticated && (
{session.status == 'unauthenticated' && (
<UnidentifiedArea className="flex text-sm text-gray-700 font-bold p-1.5 px-2 rounded-lg">
<ul className="flex space-x-3 items-center">
<li>
@ -29,11 +30,11 @@ export const HeaderProfileBox = () => {
</ul>
</UnidentifiedArea>
)}
{session.isAuthenticated && (
{session.status == 'authenticated' && (
<AccountArea className="space-x-0">
<div className="flex items-center space-x-2">
<div className='flex items-center space-x-2' >
<p className='text-sm'>{session.user.username}</p>
<p className='text-sm'>{session.data.user.username}</p>
{isUserAdmin && <div className="text-[10px] bg-rose-300 px-2 font-bold rounded-md shadow-inner py-1">ADMIN</div>}
</div>
<div className="py-4">

View file

@ -39,6 +39,7 @@
"lowlight": "^3.1.0",
"lucide-react": "^0.363.0",
"next": "14.2.3",
"next-auth": "^4.24.7",
"prosemirror-state": "^1.4.3",
"randomcolor": "^0.6.2",
"re-resizable": "^6.9.11",

4458
apps/web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -10,8 +10,8 @@ interface LoginAndGetTokenResponse {
// TODO : everything in this file need to be refactored including security issues fix
export async function loginAndGetToken(
username: string,
password: string
username: any,
password: any
): Promise<any> {
// Request Config
@ -37,6 +37,37 @@ export async function loginAndGetToken(
return response
}
export async function loginWithOAuthToken(
email: any,
provider: any,
accessToken: string
): Promise<any> {
// Request Config
// get origin
const HeadersConfig = new Headers({
'Content-Type': 'application/json',
})
const body = {
email: email,
provider: provider,
access_token: accessToken,
}
const jsonBody = JSON.stringify(body);
const requestOptions: any = {
method: 'POST',
headers: HeadersConfig,
body: jsonBody,
redirect: 'follow',
credentials: 'include',
}
// fetch using await and async
const response = await fetch(`${getAPIUrl()}auth/oauth`, requestOptions)
return response
}
export async function sendResetLink(email: string, org_id: number) {
const result = await fetch(
`${getAPIUrl()}users/reset_password/send_reset_code/${email}?org_id=${org_id}`,
@ -102,10 +133,8 @@ export async function getUserInfo(token: string): Promise<any> {
}
export async function getUserSession(token: string): Promise<any> {
const origin = window.location.origin
const HeadersConfig = new Headers({
Authorization: `Bearer ${token}`,
Origin: origin,
})
const requestOptions: any = {

View file

@ -1,19 +1,27 @@
import { getAPIUrl } from '@services/config/config'
import { RequestBody, getResponseMetadata } from '@services/utils/ts/requests'
import {
RequestBody,
RequestBodyWithAuthHeader,
getResponseMetadata,
} from '@services/utils/ts/requests'
export async function createInviteCode(org_id: any) {
export async function createInviteCode(org_id: any, access_token: any) {
const result = await fetch(
`${getAPIUrl()}orgs/${org_id}/invites`,
RequestBody('POST', null, null)
RequestBodyWithAuthHeader('POST', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function createInviteCodeWithUserGroup(org_id: any, usergroup_id: number) {
export async function createInviteCodeWithUserGroup(
org_id: any,
usergroup_id: number,
access_token: any
) {
const result = await fetch(
`${getAPIUrl()}orgs/${org_id}/invites_with_usergroups?usergroup_id=${usergroup_id}`,
RequestBody('POST', null, null)
RequestBodyWithAuthHeader('POST', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
@ -21,11 +29,12 @@ export async function createInviteCodeWithUserGroup(org_id: any, usergroup_id: n
export async function deleteInviteCode(
org_id: any,
org_invite_code_uuid: string
org_invite_code_uuid: string,
access_token: any
) {
const result = await fetch(
`${getAPIUrl()}orgs/${org_id}/invites/${org_invite_code_uuid}`,
RequestBody('DELETE', null, null)
RequestBodyWithAuthHeader('DELETE', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
@ -33,20 +42,25 @@ export async function deleteInviteCode(
export async function changeSignupMechanism(
org_id: any,
signup_mechanism: string
signup_mechanism: string,
access_token: any
) {
const result = await fetch(
`${getAPIUrl()}orgs/${org_id}/signup_mechanism?signup_mechanism=${signup_mechanism}`,
RequestBody('PUT', null, null)
RequestBodyWithAuthHeader('PUT', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function validateInviteCode(org_id: any, invite_code: string) {
export async function validateInviteCode(
org_id: any,
invite_code: string,
access_token: any
) {
const result = await fetch(
`${getAPIUrl()}orgs/${org_id}/invites/code/${invite_code}`,
RequestBody('GET', null, null)
RequestBodyWithAuthHeader('GET', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
@ -55,11 +69,12 @@ export async function validateInviteCode(org_id: any, invite_code: string) {
export async function inviteBatchUsers(
org_id: any,
emails: string,
invite_code_uuid: string
invite_code_uuid: string,
access_token: any
) {
const result = await fetch(
`${getAPIUrl()}orgs/${org_id}/invites/users/batch?emails=${emails}&invite_code_uuid=${invite_code_uuid}`,
RequestBody('POST', null, null)
RequestBodyWithAuthHeader('POST', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res