Compare commits

...

3 commits

Author SHA1 Message Date
6a90e03d40 docs: org router tag metadata
Issue: #13
2026-05-28 16:46:44 +01:00
d3bdfe8469 docs: org router decorators
Issue: #13
2026-05-28 16:43:39 +01:00
cba04e4249 docs: user router summaries
Issue: #13
2026-05-28 16:11:54 +01:00
3 changed files with 147 additions and 29 deletions

View file

@ -36,6 +36,10 @@ tags_metadata = [
"name": "Service", "name": "Service",
"description": "Services related operations, includes registering services and reissuing API keys", "description": "Services related operations, includes registering services and reissuing API keys",
}, },
{
"name": "Organisation",
"description": "Organisation related operations, includes getting lists of users etc associated with orgs",
},
] ]

View file

@ -43,11 +43,20 @@ from src.organisation.schemas import OrgPostOrgRequest, OrgPatchQuestionnaireReq
router = APIRouter( router = APIRouter(
prefix="/org", prefix="/org",
tags=["org"], tags=["Organisation"],
) )
@router.get("/id", response_model=OrgGetOrgResponse) @router.get("/id",
summary="Get org details by ID.",
response_model=OrgGetOrgResponse,
status_code=status.HTTP_200_OK,
responses={
status.HTTP_200_OK: {"description": "Successful retrieval from database"},
status.HTTP_404_NOT_FOUND: {"description": "Organisation not found"},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Missing or invalid org_id query parameter"},
status.HTTP_401_UNAUTHORIZED: {"description": "Not authorised. Must be org root user."},
})
async def get_org_by_id(org_model: org_model_root_claim_query_dependency): async def get_org_by_id(org_model: org_model_root_claim_query_dependency):
response = { response = {
"name": org_model.name, "name": org_model.name,
@ -61,8 +70,17 @@ async def get_org_by_id(org_model: org_model_root_claim_query_dependency):
return response return response
@router.post("/") @router.post("/",
summary="Create new organisation.",
status_code=status.HTTP_201_CREATED,
responses={
status.HTTP_201_CREATED: {"description": "Successfully created organisation."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Invalid data in request."},
status.HTTP_401_UNAUTHORIZED: {"description": "User must be logged in with OIDC to create organisation."},
status.HTTP_409_CONFLICT: {"description": "Organisation with this name already exists."},
})
async def create_org(db: db_dependency, user_model: user_model_claims_dependency, request_model: OrgPostOrgRequest): async def create_org(db: db_dependency, user_model: user_model_claims_dependency, request_model: OrgPostOrgRequest):
# TODO: Response model
if request_model.intake_questionnaire: if request_model.intake_questionnaire:
intake_questionnaire = request_model.intake_questionnaire.model_dump() intake_questionnaire = request_model.intake_questionnaire.model_dump()
else: else:
@ -88,13 +106,21 @@ async def create_org(db: db_dependency, user_model: user_model_claims_dependency
db.commit() db.commit()
@router.patch("/questionnaire") @router.patch("/questionnaire",
summary="Update questionnaire.",
status_code=status.HTTP_200_OK,
responses={
status.HTTP_200_OK: {"description": "Successfully updated questionnaire."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Invalid data in request."},
status.HTTP_401_UNAUTHORIZED: {"description": "Not authorised. Must be org root user."},
})
async def update_questionnaire(db: db_dependency, org_model: org_model_root_claim_query_dependency, request_model: OrgPatchQuestionnaireRequest): async def update_questionnaire(db: db_dependency, org_model: org_model_root_claim_query_dependency, request_model: OrgPatchQuestionnaireRequest):
""" """
Route for updating questionnaire. Route for updating questionnaire.
The partial bool allows for submission of partially completed questionnaire and/or The partial bool allows for submission of partially completed questionnaire and/or
final "are you sure" check before setting the org to be in "submitted" status, awaiting admin approval. final "are you sure" check before setting the org to be in "submitted" status, awaiting admin approval.
""" """
# TODO: Response model
org_model.intake_questionnaire = request_model.intake_questionnaire.model_dump() org_model.intake_questionnaire = request_model.intake_questionnaire.model_dump()
# Allows for partially completed questionnaires to be saved without being submitted for review # Allows for partially completed questionnaires to be saved without being submitted for review
@ -104,45 +130,101 @@ async def update_questionnaire(db: db_dependency, org_model: org_model_root_clai
db.commit() db.commit()
@router.patch("/status") @router.patch("/status",
summary="Update status of organisation.",
status_code=status.HTTP_200_OK,
responses={
status.HTTP_200_OK: {"description": "Successfully updated organisation status."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Invalid data in request."},
status.HTTP_401_UNAUTHORIZED: {"description": "Not authorised. Must be super admin."},
})
async def update_status(db: db_dependency, org_model: org_model_body_dependency, su: super_admin_dependency, request_model: OrgPatchStatusRequest): async def update_status(db: db_dependency, org_model: org_model_body_dependency, su: super_admin_dependency, request_model: OrgPatchStatusRequest):
# TODO: Response model
org_model.status = request_model.status org_model.status = request_model.status
db.commit() db.commit()
@router.get("/users", response_model=OrgGetUserResponse) @router.get("/users",
summary="Get email addresses of users of the organisation.",
status_code=status.HTTP_200_OK,
response_model=OrgGetUserResponse,
responses={
status.HTTP_200_OK: {"description": "Successful retrieval of users."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Org ID missing or invalid."},
status.HTTP_401_UNAUTHORIZED: {"description": "Not authorised. Must be org root user."},
})
async def get_users(org_model: org_model_root_claim_query_dependency): async def get_users(org_model: org_model_root_claim_query_dependency):
return {"users": [user.email for user in org_model.user_rel]} return {"users": [user.email for user in org_model.user_rel]}
@router.post("/users") @router.post("/users",
summary="All user to the organisation.",
status_code=status.HTTP_200_OK,
responses={
status.HTTP_200_OK: {"description": "Successfully added user to the organisation."},
status.HTTP_401_UNAUTHORIZED: {"description": "Not authorised. Must be org root user."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Invalid data in request."},
status.HTTP_409_CONFLICT: {"description": "User is already a member of the organisation."},
})
async def add_user_to_org(db: db_dependency, org_model: org_model_root_claim_body_dependency, user_model: user_model_body_dependency, request_model: OrgPostUserRequest): async def add_user_to_org(db: db_dependency, org_model: org_model_root_claim_body_dependency, user_model: user_model_body_dependency, request_model: OrgPostUserRequest):
# TODO: response model
if user_model in org_model.user_rel: if user_model in org_model.user_rel:
raise Conflict(message="User already a part of this organisation") raise Conflict(message="User already a part of this organisation")
org_model.user_rel.append(user_model) org_model.user_rel.append(user_model)
db.commit() db.commit()
@router.delete("/", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/",
summary="Delete organisation from the hub.",
status_code=status.HTTP_204_NO_CONTENT,
responses={
status.HTTP_204_NO_CONTENT: {"description": "Successfully deleted organisation."},
status.HTTP_401_UNAUTHORIZED: {"description": "Not authorised. Must be super admin."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Org ID missing or invalid."},
})
async def delete_organisation_by_id(db: db_dependency, org_model: org_model_body_dependency, su: super_admin_dependency, request_model: OrgDeleteOrgRequest): async def delete_organisation_by_id(db: db_dependency, org_model: org_model_body_dependency, su: super_admin_dependency, request_model: OrgDeleteOrgRequest):
db.delete(org_model) db.delete(org_model)
db.commit() db.commit()
@router.patch("/root_user", status_code=status.HTTP_204_NO_CONTENT) @router.patch("/root_user",
summary="Update the root user of the organisation.",
status_code=status.HTTP_200_OK,
responses={
status.HTTP_200_OK: {"description": "Successfully updated root user."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Invalid data in request."},
status.HTTP_401_UNAUTHORIZED: {"description": "Not authorised. Must be super admin."},
})
async def update_root_user(db: db_dependency, org_model: org_model_body_dependency, user_model: user_model_body_dependency, su: super_admin_dependency, request_model: OrgPatchRootRequest): async def update_root_user(db: db_dependency, org_model: org_model_body_dependency, user_model: user_model_body_dependency, su: super_admin_dependency, request_model: OrgPatchRootRequest):
# TODO: response model
org_model.root_user_rel = user_model org_model.root_user_rel = user_model
db.commit() db.commit()
@router.get("/groups", response_model=OrgGetGroupResponse) @router.get("/groups",
summary="Get all organisation IAM groups.",
status_code=status.HTTP_200_OK,
response_model=OrgGetGroupResponse,
responses={
status.HTTP_200_OK: {"description": "Successful retrieval of IAM groups."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Org ID missing or invalid."},
status.HTTP_401_UNAUTHORIZED: {"description": "Not authorised. Must be org root user."},
})
async def get_org_groups(org_model: org_model_root_claim_query_dependency): async def get_org_groups(org_model: org_model_root_claim_query_dependency):
return {"groups": [group.name for group in org_model.group_rel]} return {"groups": [group.name for group in org_model.group_rel]}
@router.delete("/user", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/user",
summary="Remove user from organisation.",
status_code=status.HTTP_204_NO_CONTENT,
responses={
status.HTTP_204_NO_CONTENT: {"description": "Successfully removed user."},
status.HTTP_401_UNAUTHORIZED: {"description": "Not authorised. Must be org root user."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Invalid data in request."},
})
async def remove_user_from_org(db: db_dependency, org_model: org_model_root_claim_body_dependency, user_model: user_model_body_dependency, request_model: OrgDeleteUserRequest): async def remove_user_from_org(db: db_dependency, org_model: org_model_root_claim_body_dependency, user_model: user_model_body_dependency, request_model: OrgDeleteUserRequest):
# TODO: response model
if user_model not in org_model.user_rel: if user_model not in org_model.user_rel:
return return
@ -150,7 +232,15 @@ async def remove_user_from_org(db: db_dependency, org_model: org_model_root_clai
db.commit() db.commit()
@router.get("/contact", response_model=OrgGetContactResponse) @router.get("/contact",
summary="Get contact for organisation.",
status_code=status.HTTP_200_OK,
response_model=OrgGetContactResponse,
responses={
status.HTTP_200_OK: {"description": "Successful retrieval of contact."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Invalid data in request."},
status.HTTP_401_UNAUTHORIZED: {"description": "Not authorised. Must be org root user."},
})
async def get_contact(org_model: org_model_root_claim_query_dependency, contact_type: Annotated[ContactType, Query()]): async def get_contact(org_model: org_model_root_claim_query_dependency, contact_type: Annotated[ContactType, Query()]):
match contact_type: match contact_type:
case "billing": case "billing":
@ -171,7 +261,15 @@ async def get_contact(org_model: org_model_root_claim_query_dependency, contact_
return {"contact": contact_response} return {"contact": contact_response}
@router.patch("/contact", response_model=OrgGetContactResponse) @router.patch("/contact",
summary="Update contact for organisation.",
status_code=status.HTTP_200_OK,
response_model=OrgGetContactResponse,
responses={
status.HTTP_200_OK: {"description": "Successfully updated contact."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Invalid data in request."},
status.HTTP_401_UNAUTHORIZED: {"description": "Not authorised. Must be org root user."},
})
async def update_contact(db: db_dependency, org_model: org_model_root_claim_body_dependency, request_model: OrgPatchContactRequest): async def update_contact(db: db_dependency, org_model: org_model_root_claim_body_dependency, request_model: OrgPatchContactRequest):
match request_model.contact_type: match request_model.contact_type:
case "billing": case "billing":

View file

@ -23,9 +23,13 @@ router = APIRouter(
) )
@router.get("/self/claims", response_model=OIDCClaims, status_code=status.HTTP_200_OK, responses={ @router.get("/self/claims",
status.HTTP_200_OK: {"description": "Successful retrieval from database"}, summary="Get current user OIDC claims.",
}) response_model=OIDCClaims,
status_code=status.HTTP_200_OK,
responses={
status.HTTP_200_OK: {"description": "Successful retrieval from database"},
})
async def current_user_claims(user: claims_dependency): async def current_user_claims(user: claims_dependency):
""" """
Returns the full OIDC claims associated with the currently logged-in user. Returns the full OIDC claims associated with the currently logged-in user.
@ -34,10 +38,14 @@ async def current_user_claims(user: claims_dependency):
return user return user
@router.get("/self/db", response_model=UserResponse, status_code=status.HTTP_200_OK, responses={ @router.get("/self/db",
status.HTTP_404_NOT_FOUND: {"description": "User not found"}, summary="Get current user hub details.",
status.HTTP_200_OK: {"description": "Successful retrieval from database"}, response_model=UserResponse,
}) status_code=status.HTTP_200_OK,
responses={
status.HTTP_404_NOT_FOUND: {"description": "User not found"},
status.HTTP_200_OK: {"description": "Successful retrieval from database"},
})
async def current_user(user_model: user_model_claims_dependency): async def current_user(user_model: user_model_claims_dependency):
""" """
Returns the database details associated with the currently logged-in user. Returns the database details associated with the currently logged-in user.
@ -45,10 +53,14 @@ async def current_user(user_model: user_model_claims_dependency):
return user_model return user_model
@router.get("/", response_model=UserResponse, status_code=status.HTTP_200_OK, responses={ @router.get("/",
status.HTTP_404_NOT_FOUND: {"description": "User not found"}, summary="Get user hub details by ID.",
status.HTTP_200_OK: {"description": "Successful retrieval from database"}, response_model=UserResponse,
}) status_code=status.HTTP_200_OK,
responses={
status.HTTP_404_NOT_FOUND: {"description": "User not found"},
status.HTTP_200_OK: {"description": "Successful retrieval from database"},
})
async def get_user_by_id(user_model: user_model_query_dependency, su: super_admin_dependency): async def get_user_by_id(user_model: user_model_query_dependency, su: super_admin_dependency):
""" """
Returns the database details associated with the provided user ID. Returns the database details associated with the provided user ID.
@ -56,11 +68,15 @@ async def get_user_by_id(user_model: user_model_query_dependency, su: super_admi
return user_model return user_model
@router.delete("/", status_code=status.HTTP_204_NO_CONTENT, responses={ @router.delete("/",
status.HTTP_204_NO_CONTENT: {"description": "User deleted"}, summary="Delete user from hub by ID.",
status.HTTP_404_NOT_FOUND: {"description": "User not found"}, status_code=status.HTTP_204_NO_CONTENT,
}) responses={
async def delete_user_by_id(db: db_dependency, user_model: user_model_body_dependency, su: super_admin_dependency, request_model: UserDeleteUserRequest): status.HTTP_204_NO_CONTENT: {"description": "User deleted"},
status.HTTP_404_NOT_FOUND: {"description": "User not found"},
})
async def delete_user_by_id(db: db_dependency, user_model: user_model_body_dependency, su: super_admin_dependency,
request_model: UserDeleteUserRequest):
""" """
Deletes the user with the provided ID from the database. This will not remove them from OIDC, and they will be automatically readded on next login. Deletes the user with the provided ID from the database. This will not remove them from OIDC, and they will be automatically readded on next login.
""" """