Skip to content

Commit

Permalink
feat: add new endpoint to refresh the app user token (#1583)
Browse files Browse the repository at this point in the history
* feat: added new endpoint to refresh the app user token

* refactor: added exception, reduce db transaction
  • Loading branch information
Sujanadh authored Jun 28, 2024
1 parent af254a0 commit 3d37971
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 55 deletions.
75 changes: 74 additions & 1 deletion src/backend/app/central/central_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from sqlalchemy.orm import Session

from app.central import central_deps
from app.config import settings
from app.config import encrypt_value, settings
from app.db.postgis_utils import (
geojson_to_javarosa_geom,
javarosa_to_geojson_geom,
Expand Down Expand Up @@ -956,3 +956,76 @@ def convert_csv(
csvin.finishGeoJson()

return True


async def get_appuser_token(
xform_id: str,
project_odk_id: int,
odk_credentials: project_schemas.ODKCentralDecrypted,
db: Session,
):
"""Get the app user token for a specific project.
Args:
db: The database session to use.
odk_credentials: ODK credentials for the project.
project_odk_id: The ODK ID of the project.
xform_id: The ID of the XForm.
Returns:
The app user token.
"""
try:
appuser = get_odk_app_user(odk_credentials)
odk_project = get_odk_project(odk_credentials)
odk_app_user = odk_project.listAppUsers(project_odk_id)

# delete if app_user already exists
if odk_app_user:
app_user_id = odk_app_user[0].get("id")
appuser.delete(project_odk_id, app_user_id)

# create new app_user
appuser_name = "fmtm_user"
log.info(
f"Creating ODK appuser ({appuser_name}) for ODK project ({project_odk_id})"
)
appuser_json = appuser.create(project_odk_id, appuser_name)
appuser_token = appuser_json.get("token")
appuser_id = appuser_json.get("id")

odk_url = odk_credentials.odk_central_url

# Update the user role for the created xform
log.info("Updating XForm role for appuser in ODK Central")
response = appuser.updateRole(
projectId=project_odk_id,
xform=xform_id,
actorId=appuser_id,
)
if not response.ok:
try:
json_data = response.json()
log.error(json_data)
except json.decoder.JSONDecodeError:
log.error(
"Could not parse response json during appuser update. "
f"status_code={response.status_code}"
)
finally:
msg = f"Failed to update appuser for formId: ({xform_id})"
log.error(msg)
raise HTTPException(
status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=msg
) from None
odk_token = encrypt_value(
f"{odk_url}/v1/key/{appuser_token}/projects/{project_odk_id}"
)
return odk_token

except Exception as e:
log.error(f"An error occurred: {str(e)}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="An error occurred while creating the app user token.",
) from e
44 changes: 42 additions & 2 deletions src/backend/app/central/central_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
#
"""Routes to relay requests to ODK Central server."""

from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException
from fastapi.concurrency import run_in_threadpool
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session

from app.auth.roles import project_admin
from app.central import central_crud
from app.db import database
from app.db import database, db_models
from app.models.enums import HTTPStatus
from app.projects import project_deps

router = APIRouter(
prefix="/central",
Expand Down Expand Up @@ -54,3 +57,40 @@ async def get_form_lists(
"""
forms = await central_crud.get_form_list(db)
return forms


@router.post("/refresh_appuser_token")
async def refresh_appuser_token(
current_user: db_models.DbUser = Depends(project_admin),
db: Session = Depends(database.get_db),
):
"""Refreshes the token for the app user associated with a specific project.
Args:
project_id (int): The ID of the project to refresh the app user token for.
current_user: The current authenticated user with project admin privileges.
db: The database session to use.
Returns:
The refreshed app user token.
"""
project = current_user.get("project")
project_id = project.id
try:
odk_credentials = await project_deps.get_odk_credentials(db, project_id)
project_odk_id = project.odkid
db_xform = await project_deps.get_project_xform(db, project_id)
odk_token = await central_crud.get_appuser_token(
db_xform.odk_form_id, project_odk_id, odk_credentials, db
)
project.odk_token = odk_token
db.commit()
return {
"status_code": HTTPStatus.OK,
"message": "App User token has been successfully refreshed.",
}
except Exception as e:
raise HTTPException(
status_code=400,
detail={f"failed to refresh the appuser token for project{project_id}"},
) from e
56 changes: 4 additions & 52 deletions src/backend/app/projects/project_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,14 @@
from geojson.feature import Feature, FeatureCollection
from loguru import logger as log
from osm_fieldwork.basemapper import create_basemap_file
from osm_fieldwork.OdkCentral import OdkAppUser
from osm_fieldwork.xlsforms import entities_registration, xlsforms_path
from osm_rawdata.postgres import PostgresClient
from shapely.geometry import shape
from sqlalchemy import and_, column, func, select, table, text
from sqlalchemy.orm import Session

from app.central import central_crud, central_deps
from app.config import encrypt_value, settings
from app.config import settings
from app.db import db_models
from app.db.postgis_utils import (
check_crs,
Expand Down Expand Up @@ -832,32 +831,6 @@ async def generate_odk_central_project_content(
) -> str:
"""Populate the project in ODK Central with XForm, Appuser, Permissions."""
project_odk_id = project.odkid
# Create an app user (i.e. QR Code) for the project
appuser_name = "fmtm_user"
log.info(
f"Creating ODK appuser ({appuser_name}) for ODK project ({project_odk_id})"
)
appuser = OdkAppUser(
odk_credentials.odk_central_url,
odk_credentials.odk_central_user,
odk_credentials.odk_central_password,
)
appuser_json = appuser.create(project_odk_id, appuser_name)

# If app user could not be created, raise an exception.
if not appuser_json:
msg = f"Couldn't create appuser {appuser_name} for project"
log.error(msg)
raise HTTPException(
status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=msg
) from None
if not (appuser_token := appuser_json.get("token")):
msg = f"Couldn't get token for appuser {appuser_name}"
log.error(msg)
raise HTTPException(
status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=msg
) from None
appuser_id = appuser_json.get("id")

# NOTE Entity Registration form: this may be removed with future Central
# API changes to allow Entity creation
Expand Down Expand Up @@ -892,28 +865,6 @@ async def generate_odk_central_project_content(
odk_credentials,
)

log.info("Updating XForm role for appuser in ODK Central")
# Update the user role for the created xform
response = appuser.updateRole(
projectId=project_odk_id,
xform=xform_id,
actorId=appuser_id,
)
if not response.ok:
try:
json_data = response.json()
log.error(json_data)
except json.decoder.JSONDecodeError:
log.error(
"Could not parse response json during appuser update. "
f"status_code={response.status_code}"
)
finally:
msg = f"Failed to update appuser for formId: ({xform_id})"
log.error(msg)
raise HTTPException(
status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=msg
) from None
sql = text(
"""
INSERT INTO xforms (
Expand All @@ -929,8 +880,9 @@ async def generate_odk_central_project_content(
{"project_id": project.id, "xform_id": xform_id, "category": form_category},
)
db.commit()
odk_url = odk_credentials.odk_central_url
return encrypt_value(f"{odk_url}/v1/key/{appuser_token}/projects/{project_odk_id}")
return await central_crud.get_appuser_token(
xform_id, project_odk_id, odk_credentials, db
)


async def generate_project_files(
Expand Down

0 comments on commit 3d37971

Please sign in to comment.