diff --git a/.env.example b/.env.example index fa029164..19e6f0e1 100644 --- a/.env.example +++ b/.env.example @@ -41,6 +41,7 @@ FRONTEND_TARGET_OVERRIDE=${FRONTEND_TARGET_OVERRIDE:-development} SITE_NAME=${SITE_NAME:-"DTM-Drone Tasking Manager"} BASE_URL=${BASE_URL:-http://localhost:8000/api} API_URL_V1=${API_URL_V1:-http://localhost:8000/api} +NODE_ODM_URL=${NODE_ODM_URL:-http://odm-api:3000} ### ODM ### diff --git a/.github/workflows/build_and_deploy.yml b/.github/workflows/build_and_deploy.yml index 26fd44f7..712165c0 100644 --- a/.github/workflows/build_and_deploy.yml +++ b/.github/workflows/build_and_deploy.yml @@ -21,6 +21,7 @@ jobs: build_target: service image_name: ghcr.io/${{ github.repository }}/backend dockerfile: Dockerfile + scan_image: false secrets: inherit encode-envs: diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index 63b29b80..f6f4e0cc 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -151,7 +151,14 @@ async def delete_project_by_id( Raises: HTTPException: If the project is not found. """ - project_id = await project_schemas.DbProject.delete(db, project.id) + user_id = user_data.id + project_id = await project_schemas.DbProject.delete(db, project.id, user_id) + if not project_id: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Project not found or user not authorized to delete it.", + ) + return {"message": f"Project successfully deleted {project_id}"} @@ -311,13 +318,11 @@ async def read_projects( db, user_id=user_id, skip=skip, limit=limit ) if not projects: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="No projects found." - ) + return [] return projects except KeyError as e: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND) from e + raise HTTPException(status_code=HTTPStatus.UNPROCESSABLE_ENTITY) from e @router.get( diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index be52d3c7..b06b4dc6 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -395,27 +395,27 @@ async def create(db: Connection, project: ProjectIn, user_id: str) -> uuid.UUID: return new_project_id[0] @staticmethod - async def delete(db: Connection, project_id: uuid.UUID) -> uuid.UUID: + async def delete(db: Connection, project_id: uuid.UUID, user_id: str) -> uuid.UUID: """Delete a single project.""" sql = """ - WITH deleted_project AS ( - DELETE FROM projects - WHERE id = %(project_id)s - RETURNING id - ), deleted_tasks AS ( - DELETE FROM tasks - WHERE project_id = %(project_id)s - RETURNING project_id - ), deleted_task_events AS ( - DELETE FROM task_events - WHERE project_id = %(project_id)s - RETURNING project_id - ) - SELECT id FROM deleted_project + WITH deleted_project AS ( + DELETE FROM projects + WHERE id = %(project_id)s AND author_id = %(user_id)s + RETURNING id + ), deleted_tasks AS ( + DELETE FROM tasks + WHERE project_id = %(project_id)s + RETURNING project_id + ), deleted_task_events AS ( + DELETE FROM task_events + WHERE project_id = %(project_id)s + RETURNING project_id + ) + SELECT id FROM deleted_project """ async with db.cursor() as cur: - await cur.execute(sql, {"project_id": project_id}) + await cur.execute(sql, {"project_id": project_id, "user_id": user_id}) deleted_project_id = await cur.fetchone() if not deleted_project_id: diff --git a/src/backend/app/users/user_deps.py b/src/backend/app/users/user_deps.py index 82984d2c..29fd0c2c 100644 --- a/src/backend/app/users/user_deps.py +++ b/src/backend/app/users/user_deps.py @@ -1,11 +1,12 @@ +import jwt from app.users.user_logic import verify_token -from fastapi import HTTPException, Request, Header +from fastapi import HTTPException, Request, Security +from fastapi.security.api_key import APIKeyHeader from app.config import settings from app.users.auth import Auth from app.users.user_schemas import AuthUser from loguru import logger as log from datetime import datetime, timedelta -import jwt async def init_google_auth(): @@ -27,7 +28,7 @@ async def init_google_auth(): async def login_required( - request: Request, access_token: str = Header(None) + request: Request, access_token: str = Security(APIKeyHeader(name="access-token")) ) -> AuthUser: """Dependency to inject into endpoints requiring login.""" if settings.DEBUG: @@ -41,9 +42,6 @@ async def login_required( if not access_token: raise HTTPException(status_code=401, detail="No access token provided") - if not access_token: - raise HTTPException(status_code=401, detail="No access token provided") - try: user = verify_token(access_token) except HTTPException as e: diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py index fdd8384f..a3fcb185 100644 --- a/src/backend/app/users/user_routes.py +++ b/src/backend/app/users/user_routes.py @@ -61,6 +61,14 @@ async def login_access_token( return Token(access_token=access_token, refresh_token=refresh_token) +@router.get("/", tags=["users"], response_model=list[user_schemas.DbUser]) +async def get_user( + db: Annotated[Connection, Depends(database.get_db)], + user_data: Annotated[AuthUser, Depends(login_required)], +): + return await user_schemas.DbUser.all(db) + + @router.patch("/{user_id}/profile") @router.post("/{user_id}/profile") async def update_user_profile( diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index fb61a6fd..63c26df9 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -188,6 +188,17 @@ class DbUser(BaseModel): name: str profile_img: Optional[str] = None + @staticmethod + async def all(db: Connection): + "Fetch all users." + async with db.cursor(row_factory=class_row(DbUser)) as cur: + await cur.execute( + """ + SELECT * FROM users; + """ + ) + return await cur.fetchall() + @staticmethod async def one(db: Connection, user_id: str): """Fetch user from the database by user_id.""" diff --git a/src/frontend/src/components/CreateProject/FormContents/GenerateTask/index.tsx b/src/frontend/src/components/CreateProject/FormContents/GenerateTask/index.tsx index 36cf3f57..0357e2db 100644 --- a/src/frontend/src/components/CreateProject/FormContents/GenerateTask/index.tsx +++ b/src/frontend/src/components/CreateProject/FormContents/GenerateTask/index.tsx @@ -65,6 +65,7 @@ export default function GenerateTask({ formProps }: { formProps: any }) { onFocus={() => setError('')} /> {error && } +

Recommended : 50-700

diff --git a/src/frontend/src/components/LandingPage/ClientsAndPartners/index.tsx b/src/frontend/src/components/LandingPage/ClientsAndPartners/index.tsx index 23b1a33d..546a97b7 100644 --- a/src/frontend/src/components/LandingPage/ClientsAndPartners/index.tsx +++ b/src/frontend/src/components/LandingPage/ClientsAndPartners/index.tsx @@ -3,7 +3,6 @@ import { motion } from 'framer-motion'; import worldBankLogo from '@Assets/images/LandingPage/WorldbankLogo.png'; import { fadeUpVariant } from '@Constants/animations'; import gfdrrLogo from '@Assets/images/GFDRR-logo.png'; -import JamaicaFlyingLabsLogo from '@Assets/images/LandingPage/JamaicaFlyingLabs_Logo.png'; import { FlexRow } from '@Components/common/Layouts'; export default function ClientAndPartners() { @@ -34,7 +33,6 @@ export default function ClientAndPartners() { > world bank logo gfdrrLogo - gfdrrLogo diff --git a/src/frontend/src/components/LandingPage/Footer/index.tsx b/src/frontend/src/components/LandingPage/Footer/index.tsx index 741b6bd4..fb7de811 100644 --- a/src/frontend/src/components/LandingPage/Footer/index.tsx +++ b/src/frontend/src/components/LandingPage/Footer/index.tsx @@ -1,7 +1,8 @@ /* eslint-disable no-unused-vars */ -import { Flex, FlexRow } from '@Components/common/Layouts'; -import Icon from '@Components/common/Icon'; +import { FlexRow } from '@Components/common/Layouts'; +// import Icon from '@Components/common/Icon'; import Image from '@Components/RadixComponents/Image'; +import JamaicaFlyingLabsLogo from '@Assets/images/LandingPage/JamaicaFlyingLabs_Logo.png'; import naxaLogo from '@Assets/images/LandingPage/Naxa-logo.png'; import hotLogo from '@Assets/images/LandingPage/HOT-logo.png'; import { Button } from '@Components/RadixComponents/Button'; @@ -57,6 +58,16 @@ export default function Footer() { FAQs Cookies */} +
+

+ Official Training Partner +

+ Jamaica-Flying-Labs-Logo +

© Drone Arial Tasking Manager. All Rights Reserved 2024 diff --git a/src/frontend/src/services/tasks.ts b/src/frontend/src/services/tasks.ts index 47a7f4d1..d24703ad 100644 --- a/src/frontend/src/services/tasks.ts +++ b/src/frontend/src/services/tasks.ts @@ -20,3 +20,6 @@ export const postTaskWaypoint = (payload: Record) => { }; export const getTaskAssetsInfo = (projectId: string, taskId: string) => authenticated(api).get(`/projects/assets/${projectId}/${taskId}/`); + +export const postProcessImagery = (projectId: string, taskId: string) => + authenticated(api).post(`/projects/process_imagery/${projectId}/${taskId}/`); diff --git a/src/frontend/src/utils/callApiSimultaneously.ts b/src/frontend/src/utils/callApiSimultaneously.ts index e6a58fbb..d649bac3 100644 --- a/src/frontend/src/utils/callApiSimultaneously.ts +++ b/src/frontend/src/utils/callApiSimultaneously.ts @@ -1,10 +1,35 @@ import axios from 'axios'; +import { toast } from 'react-toastify'; // function that calls the api simultaneously export default async function callApiSimultaneously(urls: any, data: any) { - const promises = urls.map((url: any, index: any) => - axios.put(url, data[index]), + // eslint-disable-next-line no-promise-executor-return + const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + + const retryFc = async ( + url: string, + singleData: any, + n: number, + ): Promise => { + try { + return await axios.put(url, singleData); + } catch (err) { + if (n === 1) throw err; + delay(1000); // 1 sec delay + // eslint-disable-next-line no-return-await + return await retryFc(url, singleData, n - 1); + } + }; + + const promises = urls.map( + (url: any, index: any) => retryFc(url, data[index], 3), // 3 entries for each api call ); - const responses = await Promise.all(promises); - return responses; + + try { + const responses = await Promise.all(promises); + return responses; + } catch (err) { + toast.error('Error occurred on image upload'); + throw err; + } } diff --git a/src/frontend/src/views/Projects/index.tsx b/src/frontend/src/views/Projects/index.tsx index db6c1e5f..0f5b8b98 100644 --- a/src/frontend/src/views/Projects/index.tsx +++ b/src/frontend/src/views/Projects/index.tsx @@ -20,9 +20,8 @@ const Projects = () => { ); // fetch api for projectsList - const { data: projectsList, isLoading } = useGetProjectsListQuery( - projectsFilterByOwner, - ); + const { data: projectsList, isLoading }: Record = + useGetProjectsListQuery(projectsFilterByOwner); const { data: userDetails } = useGetUserDetailsQuery(); const localStorageUserDetails = getLocalStorageValue('userprofile'); @@ -54,17 +53,20 @@ const Projects = () => { ))} ) : ( - (projectsList as Record[])?.map( - (project: Record) => ( - - ), - ) + <> + {!projectsList?.length &&

No projects available
} + {(projectsList as Record[])?.map( + (project: Record) => ( + + ), + )} + )} {showMap && (