Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat user roles frontend integration #1331

Draft
wants to merge 9 commits into
base: development
Choose a base branch
from
127 changes: 93 additions & 34 deletions src/backend/app/auth/auth_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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:
Expand Down
23 changes: 13 additions & 10 deletions src/frontend/src/components/ProjectDetailsV2/ProjectOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -107,16 +108,18 @@ const ProjectOptions = () => {
>
Generate MbTiles
</CoreModules.Button>
<CoreModules.Button
onClick={() => navigate(`/manage-project/${encodedId}`)}
variant="contained"
color="error"
sx={{ width: '200px', mr: '15px' }}
endIcon={<AssetModules.SettingsIcon />}
className="fmtm-truncate"
>
Manage Project
</CoreModules.Button>
{token && (
<CoreModules.Button
onClick={() => navigate(`/manage-project/${encodedId}`)}
variant="contained"
color="error"
sx={{ width: '200px', mr: '15px' }}
endIcon={<AssetModules.SettingsIcon />}
className="fmtm-truncate"
>
Manage Project
</CoreModules.Button>
)}
<CoreModules.Button
onClick={() => navigate(`/project-submissions/${encodedId}`)}
variant="contained"
Expand Down
24 changes: 14 additions & 10 deletions src/frontend/src/components/home/HomePageFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = {
Expand Down Expand Up @@ -117,16 +119,18 @@ const HomePageFilters = ({ onSearch, filteredProjectCount, totalProjectCount }:
<div className="fmtm-px-4 fmtm-py-3 ">
<div className="fmtm-flex fmtm-flex-col sm:fmtm-flex-row sm:fmtm-items-center fmtm-gap-4">
<h5 className="fmtm-text-2xl">PROJECTS</h5>
<CoreModules.Link
to={'/create-project'}
style={{
textDecoration: 'none',
}}
>
<button className="fmtm-bg-primaryRed fmtm-text-sm sm:fmtm-text-[1rem] fmtm-px-4 fmtm-py-2 fmtm-rounded fmtm-w-auto fmtm-text-white fmtm-uppercase">
+ Create New Project{' '}
</button>
</CoreModules.Link>
{token && [user_roles.ADMIN].includes(token['role']) && (
<CoreModules.Link
to={'/create-project'}
style={{
textDecoration: 'none',
}}
>
<button className="fmtm-bg-primaryRed fmtm-text-sm sm:fmtm-text-[1rem] fmtm-px-4 fmtm-py-2 fmtm-rounded fmtm-w-auto fmtm-text-white fmtm-uppercase">
+ Create New Project{' '}
</button>
</CoreModules.Link>
)}
</div>
<div className="fmtm-flex fmtm-flex-col fmtm-gap-3 sm:fmtm-flex-row sm:fmtm-justify-between">
<div className="fmtm-mt-3 fmtm-flex fmtm-items-center fmtm-gap-1">
Expand Down
23 changes: 16 additions & 7 deletions src/frontend/src/components/organisation/OrganisationGridCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -47,12 +48,20 @@ const OrganisationGridCard = ({ filteredData, allDataLength }) => {
className="fmtm-overflow-hidden fmtm-grow fmtm-h-full fmtm-justify-between"
>
<div className="fmtm-flex fmtm-flex-col fmtm-gap-1">
<h2
className="fmtm-line-clamp-1 fmtm-text-base sm:fmtm-text-lg fmtm-font-bold fmtm-capitalize"
title={data.name}
>
{data.name}
</h2>
<div className="fmtm-flex fmtm-justify-between fmtm-items-center">
<h2
className="fmtm-line-clamp-1 fmtm-text-base sm:fmtm-text-lg fmtm-font-bold fmtm-capitalize"
title={data.name}
>
{data.name}
</h2>
{isEditable && token && [user_roles.ADMIN].includes(token['role']) && (
<AssetModules.EditIcon
className="fmtm-text-[#7A7676] hover:fmtm-text-[#5a5757]"
onClick={() => navigate(`/edit-organization/${data.id}`)}
/>
)}
</div>
<p
className="fmtm-line-clamp-3 fmtm-text-[#7A7676] fmtm-font-archivo fmtm-text-sm sm:fmtm-text-base"
title={data.description}
Expand Down
45 changes: 27 additions & 18 deletions src/frontend/src/routes.jsx β†’ src/frontend/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import ErrorBoundary from '@/views/ErrorBoundary';
import ProjectDetailsV2 from '@/views/ProjectDetailsV2';
import ProjectSubmissions from '@/views/ProjectSubmissions';
import ManageProject from '@/views/ManageProject';
import { user_roles } from '@/types/enums';

const Submissions = React.lazy(() => import('./views/Submissions'));
const Tasks = React.lazy(() => import('./views/Tasks'));
Expand All @@ -35,33 +36,41 @@ const routes = createBrowserRouter([
{
path: '/organisation',
element: (
<ErrorBoundary>
<Organisation />
</ErrorBoundary>
<ProtectedRoute>
<ErrorBoundary>
<Organisation />
</ErrorBoundary>
</ProtectedRoute>
),
},
{
path: '/create-organization',
element: (
<ErrorBoundary>
<CreateEditOrganization />
</ErrorBoundary>
<ProtectedRoute>
<ErrorBoundary>
<CreateEditOrganization />
</ErrorBoundary>
</ProtectedRoute>
),
},
{
path: '/edit-organization/:id',
element: (
<ErrorBoundary>
<CreateEditOrganization />
</ErrorBoundary>
<ProtectedRoute permittedRoles={[user_roles.ADMIN]}>
<ErrorBoundary>
<CreateEditOrganization />
</ErrorBoundary>
</ProtectedRoute>
),
},
{
path: '/approve-organization/:id',
element: (
<ErrorBoundary>
<ApproveOrganization />
</ErrorBoundary>
<ProtectedRoute permittedRoles={[user_roles.ADMIN]}>
<ErrorBoundary>
<ApproveOrganization />
</ErrorBoundary>
</ProtectedRoute>
),
},
// {
Expand Down Expand Up @@ -143,7 +152,7 @@ const routes = createBrowserRouter([
{
path: '/create-project',
element: (
<ProtectedRoute>
<ProtectedRoute permittedRoles={[user_roles.ADMIN]}>
<Suspense fallback={<div>Loading...</div>}>
<ErrorBoundary>
<CreateNewProject />
Expand All @@ -155,7 +164,7 @@ const routes = createBrowserRouter([
{
path: '/upload-area',
element: (
<ProtectedRoute>
<ProtectedRoute permittedRoles={[user_roles.ADMIN]}>
<Suspense fallback={<div>Loading...</div>}>
<ErrorBoundary>
<CreateNewProject />
Expand All @@ -167,7 +176,7 @@ const routes = createBrowserRouter([
{
path: '/data-extract',
element: (
<ProtectedRoute>
<ProtectedRoute permittedRoles={[user_roles.ADMIN]}>
<Suspense fallback={<div>Loading...</div>}>
<ErrorBoundary>
<CreateNewProject />
Expand All @@ -179,7 +188,7 @@ const routes = createBrowserRouter([
{
path: '/split-tasks',
element: (
<ProtectedRoute>
<ProtectedRoute permittedRoles={[user_roles.ADMIN]}>
<Suspense fallback={<div>Loading...</div>}>
<ErrorBoundary>
<CreateNewProject />
Expand All @@ -191,7 +200,7 @@ const routes = createBrowserRouter([
{
path: '/select-category',
element: (
<ProtectedRoute>
<ProtectedRoute permittedRoles={[user_roles.ADMIN]}>
<Suspense fallback={<div>Loading...</div>}>
<ErrorBoundary>
<CreateNewProject />
Expand Down Expand Up @@ -233,7 +242,7 @@ const routes = createBrowserRouter([
{
path: '/manage-project/:id',
element: (
<ProtectedRoute>
<ProtectedRoute permittedRoles={[user_roles.ADMIN]}>
<Suspense fallback={<div>Loading...</div>}>
<ErrorBoundary>
<ManageProject />
Expand Down