diff --git a/src/backend/app/auth/auth_routes.py b/src/backend/app/auth/auth_routes.py index 1f0438d310..915b199412 100644 --- a/src/backend/app/auth/auth_routes.py +++ b/src/backend/app/auth/auth_routes.py @@ -17,17 +17,17 @@ # """Auth routes, to login, logout, and get user details.""" +from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, Request, Response from fastapi.responses import JSONResponse from loguru import logger as log +from sqlalchemy import text from sqlalchemy.orm import Session from app.auth.osm import AuthUser, init_osm_auth, login_required from app.config import settings from app.db import database -from app.db.db_models import DbUser -from app.users import user_crud router = APIRouter( prefix="/auth", @@ -127,45 +127,104 @@ async def logout(): async def get_or_create_user( db: Session, user_data: AuthUser, -) -> DbUser: +): """Get user from User table if exists, else create.""" - existing_user = await user_crud.get_user(db, user_data.id) - - if existing_user: - # Update an existing user - if user_data.img_url: - existing_user.profile_img = user_data.img_url - db.commit() - return existing_user - - user_by_username = await user_crud.get_user_by_username(db, user_data.username) - if user_by_username: - raise HTTPException( - status_code=400, - detail=( - f"User with this username {user_data.username} already exists. " - "Please contact the administrator." - ), + try: + update_sql = text( + """ + DO + $$ + BEGIN + IF EXISTS (SELECT 1 FROM users WHERE id = :user_id) THEN + UPDATE users + SET profile_img = :profile_img + WHERE id = :user_id; + ELSIF EXISTS (SELECT 1 FROM users WHERE username = :username) THEN + -- Username already exists, raise an error + RAISE EXCEPTION + ' + User with this username % already exists + ', :username; + ELSE + INSERT INTO users ( + id, username, profile_img, role, mapping_level, + is_email_verified, is_expert, tasks_mapped, tasks_validated, + tasks_invalidated, date_registered, last_validation_date + ) + VALUES ( + :user_id, :username, :profile_img, :role, + :mapping_level, FALSE, FALSE, 0, 0, 0, + :current_date, :current_date + ); + END IF; + END + $$; + + """ ) - # Add user to database - db_user = DbUser( - id=user_data.id, - username=user_data.username, - profile_img=user_data.img_url, - role=user_data.role, - ) - db.add(db_user) - db.commit() - - return db_user - + db.execute( + update_sql, + { + "user_id": user_data.id, + "username": user_data.username, + "profile_img": user_data.img_url or None, + "role": "MAPPER", + "mapping_level": "BEGINNER", + "current_date": datetime.now(timezone.utc), + }, + ) + db.commit() -@router.get("/me/", response_model=AuthUser) + get_sql = text( + """ + SELECT users.*, + COALESCE(user_roles.project_id, Null) as project_id, + COALESCE(user_roles.role, 'MAPPER') as project_role, + COALESCE(organisation_managers.organisation_id, Null) as created_org + FROM users + LEFT JOIN user_roles ON users.id = user_roles.user_id + LEFT JOIN organisation_managers on users.id = organisation_managers.user_id + WHERE users.id = :user_id; + """ + ) + result = db.execute( + get_sql, + {"user_id": user_data.id}, + ) + db_user = result.fetchall() + user = [ + { + "id": row.id, + "username": row.username, + "img_url": row.profile_img, + "role": row.role, + "project_id": row.project_id, + "project_role": row.project_role, + "created_org": row.created_org, + } + for row in db_user + ] + return user[0] + + except Exception as e: + # Check if the exception is due to username already existing + if "already exists" in str(e): + raise HTTPException( + status_code=400, + detail=( + f"User with this username {user_data.username} already exists." + ), + ) from e + else: + raise HTTPException(status_code=400, detail=str(e)) from e + + +@router.get("/me/") async def my_data( db: Session = Depends(database.get_db), user_data: AuthUser = Depends(login_required), -) -> AuthUser: +): """Read access token and get user details from OSM. Args: diff --git a/src/frontend/src/components/ProjectDetailsV2/ProjectOptions.tsx b/src/frontend/src/components/ProjectDetailsV2/ProjectOptions.tsx index c57cc5fd24..77b4368cad 100644 --- a/src/frontend/src/components/ProjectDetailsV2/ProjectOptions.tsx +++ b/src/frontend/src/components/ProjectDetailsV2/ProjectOptions.tsx @@ -19,6 +19,7 @@ const ProjectOptions = () => { const downloadDataExtractLoading: boolean = CoreModules.useAppSelector( (state) => state.project.downloadDataExtractLoading, ); + const token = CoreModules.useAppSelector((state) => state.login.loginToken); const encodedId: string = params.id; const decodedId: number = environment.decode(encodedId); @@ -107,16 +108,18 @@ const ProjectOptions = () => { > Generate MbTiles - navigate(`/manage-project/${encodedId}`)} - variant="contained" - color="error" - sx={{ width: '200px', mr: '15px' }} - endIcon={} - className="fmtm-truncate" - > - Manage Project - + {token && ( + navigate(`/manage-project/${encodedId}`)} + variant="contained" + color="error" + sx={{ width: '200px', mr: '15px' }} + endIcon={} + className="fmtm-truncate" + > + Manage Project + + )} navigate(`/project-submissions/${encodedId}`)} variant="contained" diff --git a/src/frontend/src/components/home/HomePageFilters.tsx b/src/frontend/src/components/home/HomePageFilters.tsx index e263690681..ba1b4c0e05 100755 --- a/src/frontend/src/components/home/HomePageFilters.tsx +++ b/src/frontend/src/components/home/HomePageFilters.tsx @@ -6,6 +6,7 @@ import Switch from '@/components/common/Switch'; import { HomeActions } from '@/store/slices/HomeSlice'; import { homeProjectPaginationTypes } from '@/models/home/homeModel'; import { useAppSelector } from '@/types/reduxTypes'; +import { user_roles } from '@/types/enums'; type homePageFiltersPropType = { onSearch: (data: string) => void; @@ -21,6 +22,7 @@ const HomePageFilters = ({ onSearch, filteredProjectCount, totalProjectCount }: const defaultTheme: any = useAppSelector((state) => state.theme.hotTheme); const showMapStatus = useAppSelector((state) => state.home.showMapStatus); const homeProjectPagination = useAppSelector((state) => state.home.homeProjectPagination); + const token = CoreModules.useAppSelector((state) => state.login.loginToken); const { windowSize } = windowDimention(); const searchableInnerStyle: any = { @@ -117,16 +119,18 @@ const HomePageFilters = ({ onSearch, filteredProjectCount, totalProjectCount }:
PROJECTS
- - - + {token && [user_roles.ADMIN].includes(token['role']) && ( + + + + )}
diff --git a/src/frontend/src/components/organisation/OrganisationGridCard.tsx b/src/frontend/src/components/organisation/OrganisationGridCard.tsx index cc92129a1a..6d8d3fa4c8 100644 --- a/src/frontend/src/components/organisation/OrganisationGridCard.tsx +++ b/src/frontend/src/components/organisation/OrganisationGridCard.tsx @@ -3,8 +3,9 @@ import CoreModules from '@/shared/CoreModules'; import CustomizedImage from '@/utilities/CustomizedImage'; import { useNavigate } from 'react-router-dom'; import { user_roles } from '@/types/enums'; +import AssetModules from '@/shared/AssetModules'; -const OrganisationGridCard = ({ filteredData, allDataLength }) => { +const OrganisationGridCard = ({ filteredData, allDataLength, isEditable = false }) => { const navigate = useNavigate(); const token = CoreModules.useAppSelector((state) => state.login.loginToken); const cardStyle = { @@ -47,12 +48,20 @@ const OrganisationGridCard = ({ filteredData, allDataLength }) => { className="fmtm-overflow-hidden fmtm-grow fmtm-h-full fmtm-justify-between" >
-

- {data.name} -

+
+

+ {data.name} +

+ {isEditable && token && [user_roles.ADMIN].includes(token['role']) && ( + navigate(`/edit-organization/${data.id}`)} + /> + )} +

import('./views/Submissions')); const Tasks = React.lazy(() => import('./views/Tasks')); @@ -35,33 +36,41 @@ const routes = createBrowserRouter([ { path: '/organisation', element: ( - - - + + + + + ), }, { path: '/create-organization', element: ( - - - + + + + + ), }, { path: '/edit-organization/:id', element: ( - - - + + + + + ), }, { path: '/approve-organization/:id', element: ( - - - + + + + + ), }, // { @@ -143,7 +152,7 @@ const routes = createBrowserRouter([ { path: '/create-project', element: ( - + Loading...

}> @@ -155,7 +164,7 @@ const routes = createBrowserRouter([ { path: '/upload-area', element: ( - + Loading...
}> @@ -167,7 +176,7 @@ const routes = createBrowserRouter([ { path: '/data-extract', element: ( - + Loading...
}> @@ -179,7 +188,7 @@ const routes = createBrowserRouter([ { path: '/split-tasks', element: ( - + Loading...
}> @@ -191,7 +200,7 @@ const routes = createBrowserRouter([ { path: '/select-category', element: ( - + Loading...}> @@ -233,7 +242,7 @@ const routes = createBrowserRouter([ { path: '/manage-project/:id', element: ( - + Loading...}> diff --git a/src/frontend/src/types/enums.ts b/src/frontend/src/types/enums.ts index 07636cc712..b3885a5fd5 100644 --- a/src/frontend/src/types/enums.ts +++ b/src/frontend/src/types/enums.ts @@ -21,3 +21,11 @@ export enum user_roles { MAPPER = '0', ADMIN = '1', } + +export enum user_project_roles { + MAPPER = '0', + VALIDATOR = '1', + FIELD_MANAGER = '2', + ASSOCIATE_PROJECT_MANAGER = '3', + PROJECT_MANAGER = '4', +} diff --git a/src/frontend/src/utilfunctions/login.ts b/src/frontend/src/utilfunctions/login.ts index 7ed72970e7..4749272424 100644 --- a/src/frontend/src/utilfunctions/login.ts +++ b/src/frontend/src/utilfunctions/login.ts @@ -45,6 +45,9 @@ export const createLoginWindow = (redirectTo) => { picture: userRes.img_url, redirect_to: redirectTo, role: userRes.role, + project_role: userRes?.project_role, + created_org: userRes?.created_org, + project_id: userRes?.project_id, }).toString(); const redirectUrl = `/osmauth?${params}`; window.location.href = redirectUrl; diff --git a/src/frontend/src/utilities/ProtectedRoute.jsx b/src/frontend/src/utilities/ProtectedRoute.jsx index c2d15445f6..8f6ab8c490 100644 --- a/src/frontend/src/utilities/ProtectedRoute.jsx +++ b/src/frontend/src/utilities/ProtectedRoute.jsx @@ -4,7 +4,7 @@ import CoreModules from '@/shared/CoreModules'; import { createLoginWindow } from '@/utilfunctions/login'; import environment from '@/environment'; -const ProtectedRoute = ({ children }) => { +const ProtectedRoute = ({ children, permittedRoles }) => { // Bypass check if NODE_ENV=development (local dev) if (import.meta.env.MODE === 'development') { return children; @@ -16,6 +16,10 @@ const ProtectedRoute = ({ children }) => { return ; } + if (permittedRoles && token && !permittedRoles.includes(token['role'])) { + return ; + } + return children; }; export default ProtectedRoute; diff --git a/src/frontend/src/views/Authorized.tsx b/src/frontend/src/views/Authorized.tsx index 59440dcb40..d3dafe341e 100644 --- a/src/frontend/src/views/Authorized.tsx +++ b/src/frontend/src/views/Authorized.tsx @@ -30,8 +30,24 @@ function Authorized() { const osm_oauth_token = params.get('osm_oauth_token'); const picture = params.get('picture'); const role = params.get('role'); + const project_role = params.get('project_role'); + const project_id = params.get('project_id'); + const created_org = params.get('created_org'); + dispatch(LoginActions.setAuthDetails(username, sessionToken, osm_oauth_token)); - dispatch(LoginActions.SetLoginToken({ username, id, sessionToken, osm_oauth_token, picture, role })); + dispatch( + LoginActions.SetLoginToken({ + username, + id, + sessionToken, + osm_oauth_token, + picture, + role, + project_id, + project_role, + created_org, + }), + ); const redirectUrl = params.get('redirect_to') || '/'; setIsReadyToRedirect(true); diff --git a/src/frontend/src/views/ManageProject.tsx b/src/frontend/src/views/ManageProject.tsx index a46faa3449..80138bf036 100644 --- a/src/frontend/src/views/ManageProject.tsx +++ b/src/frontend/src/views/ManageProject.tsx @@ -8,12 +8,8 @@ import environment from '@/environment'; import { GetIndividualProjectDetails } from '@/api/CreateProjectService'; import { useNavigate } from 'react-router-dom'; import { useAppSelector } from '@/types/reduxTypes'; +import { user_roles } from '@/types/enums'; -const tabList = [ - { id: 'users', name: 'USERS', icon: }, - { id: 'edit', name: 'EDIT', icon: }, - { id: 'delete', name: 'DELETE', icon: }, -]; const ManageProject = () => { const dispatch = CoreModules.useAppDispatch(); const params = CoreModules.useParams(); @@ -22,6 +18,23 @@ const ManageProject = () => { const decodedProjectId = environment.decode(encodedProjectId); const [tabView, setTabView] = useState<'users' | 'edit' | string>('users'); const editProjectDetails = useAppSelector((state) => state.createproject.editProjectDetails); + const token = CoreModules.useAppSelector((state) => state.login.loginToken); + + const tabList = [ + { id: 'users', name: 'USERS', icon: , permission: !!token }, + { + id: 'edit', + name: 'EDIT', + icon: , + permission: token && [user_roles.ADMIN].includes(token['role']), + }, + { + id: 'delete', + name: 'DELETE', + icon: , + permission: token && [user_roles.ADMIN].includes(token['role']), + }, + ]; useEffect(() => { dispatch(GetIndividualProjectDetails(`${import.meta.env.VITE_API_URL}/projects/${decodedProjectId}`)); @@ -38,18 +51,21 @@ const ManageProject = () => {

BACK

- {tabList.map((tab) => ( -
setTabView(tab.id)} - > -
{tab.icon}
-

{tab.name}

-
- ))} + {tabList.map( + (tab) => + tab.permission && ( +
setTabView(tab.id)} + > +
{tab.icon}
+

{tab.name}

+
+ ), + )}
diff --git a/src/frontend/src/views/Organisation.tsx b/src/frontend/src/views/Organisation.tsx index 8f183dcf1f..632810cdef 100644 --- a/src/frontend/src/views/Organisation.tsx +++ b/src/frontend/src/views/Organisation.tsx @@ -241,6 +241,7 @@ const Organisation = () => { ) ) : null} diff --git a/src/frontend/src/views/ProjectDetailsV2.tsx b/src/frontend/src/views/ProjectDetailsV2.tsx index db67a2ff80..18ddecbc79 100644 --- a/src/frontend/src/views/ProjectDetailsV2.tsx +++ b/src/frontend/src/views/ProjectDetailsV2.tsx @@ -76,6 +76,7 @@ const Home = () => { const mapTheme = useAppSelector((state) => state.theme.hotTheme); const geolocationStatus = useAppSelector((state) => state.project.geolocationStatus); const projectDetailsLoading = useAppSelector((state) => state?.project?.projectDetailsLoading); + const token = CoreModules.useAppSelector((state) => state.login.loginToken); //snackbar handle close funtion const handleClose = (event, reason) => { @@ -303,13 +304,15 @@ const Home = () => { ) : (

{`#${state.projectInfo.id}`}

)} -
{projectDetailsLoading ? ( @@ -361,30 +364,32 @@ const Home = () => { /> )}
-
-
-
- + {token && ( +
+
+
+ +
- + )} {params?.id && (
@@ -459,17 +464,19 @@ const Home = () => { collapsed={true} />
-
-
+ {token && ( +
+
+ )}
{
)} - {featuresLayer != undefined && ( + {featuresLayer != undefined && token && (