mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: implement logged in organization joining + improvements
This commit is contained in:
parent
25ac82f4ad
commit
693a28721d
19 changed files with 200 additions and 29 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
119
apps/api/src/services/orgs/join.py
Normal file
119
apps/api/src/services/orgs/join.py
Normal 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.",
|
||||
)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { getAPIUrl } from '@services/config/config'
|
||||
import {
|
||||
RequestBody,
|
||||
RequestBodyForm,
|
||||
RequestBodyFormWithAuthHeader,
|
||||
RequestBodyWithAuthHeader,
|
||||
} from '@services/utils/ts/requests'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { getAPIUrl } from '@services/config/config'
|
||||
import {
|
||||
RequestBody,
|
||||
RequestBodyForm,
|
||||
RequestBodyFormWithAuthHeader,
|
||||
RequestBodyWithAuthHeader,
|
||||
} from '@services/utils/ts/requests'
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { getAPIUrl } from '@services/config/config'
|
||||
import {
|
||||
RequestBody,
|
||||
RequestBodyForm,
|
||||
RequestBodyFormWithAuthHeader,
|
||||
RequestBodyWithAuthHeader,
|
||||
} from '@services/utils/ts/requests'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue