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 pydantic import BaseModel, EmailStr
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 config.config import get_learnhouse_config
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()

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

@ -16,6 +16,8 @@ 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
@ -97,7 +99,7 @@ function SignUpClient(props: SignUpClientProps) {
{joinMethod == 'inviteOnly' &&
(inviteCode ? (
session.status == 'authenticated' ? (
<LoggedInJoinScreen />
<LoggedInJoinScreen inviteCode={inviteCode} />
) : (
<InviteOnlySignUpComponent inviteCode={inviteCode} />
)
@ -112,7 +114,30 @@ function SignUpClient(props: SignUpClientProps) {
const LoggedInJoinScreen = (props: 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(`/`)
}, 2000)
setIsSubmitting(false)
} else {
toast.error(res.data.detail)
setIsLoading(false)
setIsSubmitting(false)
}
}
useEffect(() => {
if (session && org) {
@ -122,6 +147,7 @@ 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>
@ -131,9 +157,13 @@ const LoggedInJoinScreen = (props: any) => {
</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>
@ -154,7 +184,7 @@ const NoTokenScreen = (props: any) => {
const validateCode = async () => {
setIsLoading(true)
let res = await validateInviteCode(org?.id, inviteCode,session?.user?.tokens.access_token)
let res = await validateInviteCode(org?.id, inviteCode, session?.user?.tokens.access_token)
//wait for 1s
if (res.success) {
toast.success(

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({

View file

@ -1,5 +1,4 @@
'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'

View file

@ -1,7 +1,7 @@
'use client'
import { getAPIUrl, getUriWithoutOrg } from '@services/config/config'
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 { useLHSession } from '@components/Contexts/LHSessionContext'
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 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'

View file

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

View file

@ -7,11 +7,11 @@ import UserAvatar from '@components/Objects/UserAvatar'
import useAdminStatus from '@components/Hooks/useAdminStatus'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useOrg } from '@components/Contexts/OrgContext'
import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config'
import { getUriWithoutOrg } from '@services/config/config'
export const HeaderProfileBox = () => {
const session = useLHSession() as any
const isUserAdmin = useAdminStatus() as any
const isUserAdmin = useAdminStatus()
const org = useOrg() as any
useEffect(() => { }
@ -37,7 +37,7 @@ export const HeaderProfileBox = () => {
<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>
{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 className="py-4">
<UserAvatar border="border-4" rounded="rounded-lg" width={30} />

View file

@ -1,6 +1,6 @@
'use client'
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 React from 'react'

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
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) {
const result: any = await fetch(

View file

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

View file

@ -13,7 +13,7 @@ import {
export async function createNewOrganization(body: any, access_token: string) {
const result = await fetch(
`${getAPIUrl()}orgs/`,
RequestBodyWithAuthHeader('POST', body, null,access_token)
RequestBodyWithAuthHeader('POST', body, null, access_token)
)
const res = await errorHandling(result)
return res
@ -113,3 +113,20 @@ export async function removeUserFromOrg(
const res = await getResponseMetadata(result)
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
}