feat: implement logged in organization joining + improvements

This commit is contained in:
swve 2024-06-06 16:45:22 +01:00
parent 25ac82f4ad
commit 693a28721d
19 changed files with 200 additions and 29 deletions

View file

@ -4,11 +4,11 @@ from fastapi import Depends, APIRouter, HTTPException, Response, status, Request
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel, EmailStr from pydantic import BaseModel, EmailStr
from sqlmodel import Session from sqlmodel import Session
from src.db.users import AnonymousUser, PublicUser, UserRead from src.db.users import AnonymousUser, UserRead
from src.core.events.database import get_db_session from src.core.events.database import get_db_session
from config.config import get_learnhouse_config from config.config import get_learnhouse_config
from src.security.auth import AuthJWT, authenticate_user, get_current_user from src.security.auth import AuthJWT, authenticate_user, get_current_user
from src.services.auth.utils import get_google_user_info, signWithGoogle from src.services.auth.utils import signWithGoogle
router = APIRouter() router = APIRouter()

View file

@ -8,6 +8,7 @@ from src.services.orgs.invites import (
get_invite_code, get_invite_code,
get_invite_codes, get_invite_codes,
) )
from src.services.orgs.join import JoinOrg, join_org
from src.services.orgs.users import ( from src.services.orgs.users import (
get_list_of_invited_users, get_list_of_invited_users,
get_organization_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) 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}") @router.put("/{org_id}/users/{user_id}/role/{role_uuid}")
async def api_update_user_role( async def api_update_user_role(
request: Request, request: Request,

View file

@ -85,7 +85,6 @@ async def api_create_user_with_orgid(
""" """
Create User with Org ID 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 # TODO(fix) : This is temporary, logic should be moved to service
if ( if (

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() orgs = result.all()
return orgs return orgs #type:ignore
# Config related # Config related
async def update_org_signup_mechanism( async def update_org_signup_mechanism(

View file

@ -16,6 +16,8 @@ import { validateInviteCode } from '@services/organizations/invites'
import PageLoading from '@components/Objects/Loaders/PageLoading' import PageLoading from '@components/Objects/Loaders/PageLoading'
import Toast from '@components/StyledElements/Toast/Toast' import Toast from '@components/StyledElements/Toast/Toast'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { BarLoader } from 'react-spinners'
import { joinOrg } from '@services/organizations/orgs'
interface SignUpClientProps { interface SignUpClientProps {
org: any org: any
@ -97,7 +99,7 @@ function SignUpClient(props: SignUpClientProps) {
{joinMethod == 'inviteOnly' && {joinMethod == 'inviteOnly' &&
(inviteCode ? ( (inviteCode ? (
session.status == 'authenticated' ? ( session.status == 'authenticated' ? (
<LoggedInJoinScreen /> <LoggedInJoinScreen inviteCode={inviteCode} />
) : ( ) : (
<InviteOnlySignUpComponent inviteCode={inviteCode} /> <InviteOnlySignUpComponent inviteCode={inviteCode} />
) )
@ -112,7 +114,30 @@ function SignUpClient(props: SignUpClientProps) {
const LoggedInJoinScreen = (props: any) => { const LoggedInJoinScreen = (props: any) => {
const session = useLHSession() as any const session = useLHSession() as any
const org = useOrg() as any const org = useOrg() as any
const invite_code = props.inviteCode
const [isLoading, setIsLoading] = React.useState(true) 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(`/`)
}, 2000)
setIsSubmitting(false)
} else {
toast.error(res.data.detail)
setIsLoading(false)
setIsSubmitting(false)
}
}
useEffect(() => { useEffect(() => {
if (session && org) { if (session && org) {
@ -122,6 +147,7 @@ const LoggedInJoinScreen = (props: any) => {
return ( return (
<div className="flex flex-row items-center mx-auto"> <div className="flex flex-row items-center mx-auto">
<Toast />
<div className="flex space-y-7 flex-col justify-center items-center"> <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"> <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="items-center">Hi</span>
@ -131,9 +157,13 @@ const LoggedInJoinScreen = (props: any) => {
</span> </span>
<span>join {org?.name} ?</span> <span>join {org?.name} ?</span>
</p> </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"> <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">
<UserPlus size={18} /> {isSumbitting ? <BarLoader
<p>Join </p> cssOverride={{ borderRadius: 60 }}
width={60}
color="#ffffff"
/> : <><UserPlus size={18} />
<p>Join </p></>}
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,6 +1,5 @@
'use client' // Error components must be Client Components 'use client' // Error components must be Client Components
import ErrorUI from '@components/StyledElements/Error/Error'
import { useEffect } from 'react' import { useEffect } from 'react'
export default function Error({ export default function Error({

View file

@ -1,5 +1,4 @@
'use client' 'use client'
import PageLoading from '@components/Objects/Loaders/PageLoading'
import { getAPIUrl } from '@services/config/config' import { getAPIUrl } from '@services/config/config'
import { swrFetcher } from '@services/utils/ts/requests' import { swrFetcher } from '@services/utils/ts/requests'
import React, { createContext, useContext, useEffect, useReducer } from 'react' import React, { createContext, useContext, useEffect, useReducer } from 'react'

View file

@ -1,7 +1,7 @@
'use client' 'use client'
import { getAPIUrl, getUriWithoutOrg } from '@services/config/config' import { getAPIUrl, getUriWithoutOrg } from '@services/config/config'
import { swrFetcher } from '@services/utils/ts/requests' import { swrFetcher } from '@services/utils/ts/requests'
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react' import React, { createContext, useContext, useMemo } from 'react'
import useSWR from 'swr' import useSWR from 'swr'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import ErrorUI from '@components/StyledElements/Error/Error' import ErrorUI from '@components/StyledElements/Error/Error'

View file

@ -6,7 +6,6 @@ import LearnHouseDashboardLogo from '@public/dashLogo.png'
import { BookCopy, Home, LogOut, School, Settings, Users } from 'lucide-react' import { BookCopy, Home, LogOut, School, Settings, Users } from 'lucide-react'
import Image from 'next/image' import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/navigation'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import UserAvatar from '../../Objects/UserAvatar' import UserAvatar from '../../Objects/UserAvatar'
import AdminAuthorization from '@components/Security/AdminAuthorization' import AdminAuthorization from '@components/Security/AdminAuthorization'

View file

@ -20,10 +20,12 @@ function useAdminStatus() {
const isAdminVar = userRoles.some((role: Role) => { const isAdminVar = userRoles.some((role: Role) => {
return ( return (
role.org.id === org.id && role.org.id === org.id &&
(role.role.id === 1 || (
role.role.id === 1 ||
role.role.id === 2 || role.role.id === 2 ||
role.role.role_uuid === 'role_global_admin' || role.role.role_uuid === 'role_global_admin' ||
role.role.role_uuid === 'role_global_maintainer') role.role.role_uuid === 'role_global_maintainer'
)
); );
}); });
setIsAdmin(isAdminVar); setIsAdmin(isAdminVar);
@ -38,3 +40,4 @@ function useAdminStatus() {
} }
export default useAdminStatus; export default useAdminStatus;

View file

@ -7,11 +7,11 @@ import UserAvatar from '@components/Objects/UserAvatar'
import useAdminStatus from '@components/Hooks/useAdminStatus' import useAdminStatus from '@components/Hooks/useAdminStatus'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useOrg } from '@components/Contexts/OrgContext' import { useOrg } from '@components/Contexts/OrgContext'
import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config' import { getUriWithoutOrg } from '@services/config/config'
export const HeaderProfileBox = () => { export const HeaderProfileBox = () => {
const session = useLHSession() as any const session = useLHSession() as any
const isUserAdmin = useAdminStatus() as any const isUserAdmin = useAdminStatus()
const org = useOrg() as any const org = useOrg() as any
useEffect(() => { } useEffect(() => { }
@ -37,7 +37,7 @@ export const HeaderProfileBox = () => {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className='flex items-center space-x-2' > <div className='flex items-center space-x-2' >
<p className='text-sm'>{session.data.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>} {isUserAdmin.isAdmin && <div className="text-[10px] bg-rose-300 px-2 font-bold rounded-md shadow-inner py-1">ADMIN</div>}
</div> </div>
<div className="py-4"> <div className="py-4">
<UserAvatar border="border-4" rounded="rounded-lg" width={30} /> <UserAvatar border="border-4" rounded="rounded-lg" width={30} />

View file

@ -1,6 +1,6 @@
'use client' 'use client'
import { getUriWithoutOrg } from '@services/config/config' import { getUriWithoutOrg } from '@services/config/config'
import { AlertTriangle, Diamond, Home, PersonStanding, RefreshCcw } from 'lucide-react' import { Diamond, Home, PersonStanding } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import React from 'react' import React from 'react'

View file

@ -8,7 +8,6 @@ import {
} from './services/config/config' } from './services/config/config'
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server' import type { NextRequest } from 'next/server'
import path from 'path'
export const config = { export const config = {
matcher: [ matcher: [

View file

@ -1,7 +1,5 @@
import { getAPIUrl } from '@services/config/config' import { getAPIUrl } from '@services/config/config'
import { import {
RequestBody,
RequestBodyForm,
RequestBodyFormWithAuthHeader, RequestBodyFormWithAuthHeader,
RequestBodyWithAuthHeader, RequestBodyWithAuthHeader,
} from '@services/utils/ts/requests' } from '@services/utils/ts/requests'

View file

@ -1,7 +1,5 @@
import { getAPIUrl } from '@services/config/config' import { getAPIUrl } from '@services/config/config'
import { import {
RequestBody,
RequestBodyForm,
RequestBodyFormWithAuthHeader, RequestBodyFormWithAuthHeader,
RequestBodyWithAuthHeader, RequestBodyWithAuthHeader,
} from '@services/utils/ts/requests' } from '@services/utils/ts/requests'

View file

@ -1,5 +1,5 @@
import { getAPIUrl } from '@services/config/config' import { getAPIUrl } from '@services/config/config'
import { RequestBody, RequestBodyWithAuthHeader } from '@services/utils/ts/requests' import { RequestBodyWithAuthHeader } from '@services/utils/ts/requests'
export async function submitQuizBlock(activity_id: string, data: any,access_token:string) { export async function submitQuizBlock(activity_id: string, data: any,access_token:string) {
const result: any = await fetch( const result: any = await fetch(

View file

@ -1,7 +1,5 @@
import { getAPIUrl } from '@services/config/config' import { getAPIUrl } from '@services/config/config'
import { import {
RequestBody,
RequestBodyForm,
RequestBodyFormWithAuthHeader, RequestBodyFormWithAuthHeader,
RequestBodyWithAuthHeader, RequestBodyWithAuthHeader,
} from '@services/utils/ts/requests' } from '@services/utils/ts/requests'

View file

@ -113,3 +113,20 @@ export async function removeUserFromOrg(
const res = await getResponseMetadata(result) const res = await getResponseMetadata(result)
return res return res
} }
export async function joinOrg(
args: {
org_id: number
user_id: string
invite_code?: string
},
next: any,
access_token?: string
) {
const result = await fetch(
`${getAPIUrl()}orgs/join`,
RequestBodyWithAuthHeader('POST', args, next, access_token)
)
const res = await getResponseMetadata(result)
return res
}