diff --git a/backend/auth/jwt.py b/backend/auth/jwt.py
index f1a05af8a..9e49c0878 100644
--- a/backend/auth/jwt.py
+++ b/backend/auth/jwt.py
@@ -1,13 +1,16 @@
from flask_jwt_extended import JWTManager, verify_jwt_in_request, get_jwt
from functools import wraps
from ..database.models.user import User
-from flask import abort
+from flask import abort, request
jwt = JWTManager()
def verify_roles_or_abort(min_role):
+ # Skip auth checks for CORS preflight requests
+ if request.method == "OPTIONS":
+ return True
verify_jwt_in_request()
jwt_decoded = get_jwt()
current_user = User.get(jwt_decoded["sub"])
@@ -21,6 +24,9 @@ def verify_roles_or_abort(min_role):
def verify_contributor_has_source_or_abort():
+ # Skip auth checks for CORS preflight requests
+ if request.method == "OPTIONS":
+ return True
verify_jwt_in_request()
jwt_decoded = get_jwt()
current_user = User.get(jwt_decoded["sub"])
@@ -52,6 +58,9 @@ def min_role_required(*roles):
def wrapper(fn):
@wraps(fn)
def decorator(*args, **kwargs):
+ # Skip auth checks for CORS preflight requests
+ if request.method == "OPTIONS":
+ return fn(*args, **kwargs)
if verify_roles_or_abort(roles):
return fn(*args, **kwargs)
@@ -64,6 +73,9 @@ def contributor_has_source():
def wrapper(fn):
@wraps(fn)
def decorator(*args, **kwargs):
+ # Skip auth checks for CORS preflight requests
+ if request.method == "OPTIONS":
+ return fn(*args, **kwargs)
if verify_contributor_has_source_or_abort():
return fn(*args, **kwargs)
diff --git a/backend/routes/agencies.py b/backend/routes/agencies.py
index 6bfcde03b..9a710b6cd 100644
--- a/backend/routes/agencies.py
+++ b/backend/routes/agencies.py
@@ -148,7 +148,7 @@ def delete_agency(agency_id: str):
# Get all agencies
-@bp.route("/", methods=["GET"])
+@bp.route("/", methods=["GET", "OPTIONS"], strict_slashes=False)
@jwt_required()
@min_role_required(UserRole.PUBLIC)
def get_all_agencies():
diff --git a/backend/routes/officers.py b/backend/routes/officers.py
index d4ba35ef6..48710e0b3 100644
--- a/backend/routes/officers.py
+++ b/backend/routes/officers.py
@@ -73,7 +73,7 @@ def get_officer(officer_uid: str):
# Get all officers
-@bp.route("", methods=["GET"])
+@bp.route("", methods=["GET", "OPTIONS"], strict_slashes=False)
@jwt_required()
@min_role_required(UserRole.PUBLIC)
def get_all_officers():
diff --git a/backend/routes/search.py b/backend/routes/search.py
index c6002ea58..ef4317524 100644
--- a/backend/routes/search.py
+++ b/backend/routes/search.py
@@ -174,105 +174,150 @@ def fetch_details(uids: List[str], type: str) -> Dict[str, dict]:
return details
-def build_agency_result(node, details_row: dict) -> Searchresult:
- if not isinstance(node, Agency):
- a = Agency.inflate(node)
- else:
- a = node
- uid = a.uid
- details = []
-
- subtitle = "{} Agency in {}, {}".format(
- a.jurisdiction_enum.describe() if a.jurisdiction_enum else "",
- a.hq_city if a.hq_city else "Unknown City",
- a.hq_state if a.hq_state else "Unknown State"
- )
-
- details.append("{} Unit(s), {} Officer(s), {} Complaint(s)".format(
- details_row.get("units", 0),
- details_row.get("officers", 0),
- details_row.get("complaints", 0)
- ))
- return Searchresult(
- uid=uid,
- title=a.name,
- subtitle=subtitle,
- details=details,
- content_type="Agency",
- source=details_row.get("source", "Unknown Source"),
- last_updated=details_row.get("last_updated", None),
- href=f"/api/v1/agencies/{uid}"
- )
-
-
-def build_unit_result(node, details_row: dict) -> Searchresult:
- if not isinstance(node, Unit):
- u = Unit.inflate(node)
- else:
- u = node
- uid = u.uid
- details = []
-
- subtitle = "Established by {}".format(
- details_row.get("agency_name", "Unknown Agency")
- )
- details.append("{} Officer(s), {} Complaint(s)".format(
- details_row.get("officers", 0),
- details_row.get("complaints", 0)
- ))
-
- return Searchresult(
- uid=uid,
- title=u.name,
- subtitle=subtitle,
- details=details,
- content_type="Unit",
- source=details_row.get("source", "Unknown Source"),
- last_updated=details_row.get("last_updated", None),
- href=f"/api/v1/units/{uid}"
- )
-
-
-def build_officer_result(node, details_row: dict) -> Searchresult:
- if not isinstance(node, Officer):
- o = Officer.inflate(node)
- else:
- o = node
- uid = o.uid
- details = []
-
- subtitle = "{ethnicity} {gender}, {rank} at the {agency}".format(
- ethnicity=(
- o.ethnicity_enum.describe()
- if o.ethnicity_enum else "Unknown Ethnicity"
- ),
- gender=(
- o.gender_enum.describe()
- if o.gender_enum else "Unknown Gender"
- ),
- rank=(
- details_row.get("rank", "Officer")
- ),
- agency=(
+def build_agency_result(node, details_row: dict) -> Searchresult | None:
+ try:
+ if not isinstance(node, Agency):
+ a = Agency.inflate(node)
+ else:
+ a = node
+ uid = a.uid
+ details = []
+
+ subtitle = "{} Agency in {}, {}".format(
+ a.jurisdiction_enum.describe() if a.jurisdiction_enum else "",
+ a.hq_city if a.hq_city else "Unknown City",
+ a.hq_state if a.hq_state else "Unknown State"
+ )
+
+ details.append("{} Unit(s), {} Officer(s), {} Complaint(s)".format(
+ details_row.get("units", 0),
+ details_row.get("officers", 0),
+ details_row.get("complaints", 0)
+ ))
+ return Searchresult(
+ uid=uid,
+ title=a.name,
+ subtitle=subtitle,
+ details=details,
+ content_type="Agency",
+ source=details_row.get("source", "Unknown Source"),
+ last_updated=details_row.get("last_updated", None),
+ href=f"/api/v1/agencies/{uid}"
+ )
+ except Exception as e:
+ failed_uid = None
+ try:
+ failed_uid = getattr(node, "get", lambda *_: None)("uid") or getattr(node, "uid", None)
+ except Exception:
+ pass
+
+ logging.warning(
+ "Failed to build agency search result for node uid=%s: %s",
+ failed_uid,
+ e,
+ exc_info=True,
+ )
+ return None
+
+
+def build_unit_result(node, details_row: dict) -> Searchresult | None:
+ try:
+ if not isinstance(node, Unit):
+ u = Unit.inflate(node)
+ else:
+ u = node
+ uid = u.uid
+ details = []
+
+ subtitle = "Established by {}".format(
details_row.get("agency_name", "Unknown Agency")
)
- )
-
- details.append("{} Complaints, {} Allegations, {} Substantiated".format(
- details_row.get("complaints", 0),
- details_row.get("allegations", 0),
- details_row.get("substantiated", 0)
- ))
- return Searchresult(
- uid=uid,
- title=o.full_name,
- subtitle=subtitle,
- details=details,
- content_type="Officer",
- source=details_row.get("source", "Unknown Source"),
- last_updated=details_row.get("last_updated", None),
- href=f"/api/v1/officers/{uid}"
- )
+ details.append("{} Officer(s), {} Complaint(s)".format(
+ details_row.get("officers", 0),
+ details_row.get("complaints", 0)
+ ))
+
+ return Searchresult(
+ uid=uid,
+ title=u.name,
+ subtitle=subtitle,
+ details=details,
+ content_type="Unit",
+ source=details_row.get("source", "Unknown Source"),
+ last_updated=details_row.get("last_updated", None),
+ href=f"/api/v1/units/{uid}"
+ )
+ except Exception as e:
+ failed_uid = None
+ try:
+ failed_uid = getattr(node, "get", lambda *_: None)("uid") or getattr(node, "uid", None)
+ except Exception:
+ pass
+
+ logging.warning(
+ "Failed to build unit search result for node uid=%s: %s",
+ failed_uid,
+ e,
+ exc_info=True,
+ )
+ return None
+
+
+def build_officer_result(node, details_row: dict) -> Searchresult | None:
+ try:
+ if not isinstance(node, Officer):
+ o = Officer.inflate(node)
+ else:
+ o = node
+ uid = o.uid
+ details = []
+
+ subtitle = "{ethnicity} {gender}, {rank} at the {agency}".format(
+ ethnicity=(
+ o.ethnicity_enum.describe()
+ if o.ethnicity_enum else "Unknown Ethnicity"
+ ),
+ gender=(
+ o.gender_enum.describe()
+ if o.gender_enum else "Unknown Gender"
+ ),
+ rank=(
+ details_row.get("rank", "Officer")
+ ),
+ agency=(
+ details_row.get("agency_name", "Unknown Agency")
+ )
+ )
+
+ details.append("{} Complaints, {} Allegations, {} Substantiated".format(
+ details_row.get("complaints", 0),
+ details_row.get("allegations", 0),
+ details_row.get("substantiated", 0)
+ ))
+ return Searchresult(
+ uid=uid,
+ title=o.full_name,
+ subtitle=subtitle,
+ details=details,
+ content_type="Officer",
+ source=details_row.get("source", "Unknown Source"),
+ last_updated=details_row.get("last_updated", None),
+ href=f"/api/v1/officers/{uid}"
+ )
+ except Exception as e:
+ failed_uid = None
+ try:
+ failed_uid = getattr(node, "get", lambda *_: None)("uid") or getattr(node, "uid", None)
+ except Exception:
+ pass
+
+ logging.warning(
+ "Failed to build officer search result for node uid=%s: %s",
+ failed_uid,
+ e,
+ exc_info=True,
+ )
+ return None
def group_nodes_by_type(results) -> Dict[str, List]:
@@ -307,7 +352,7 @@ def group_nodes_by_type(results) -> Dict[str, List]:
# Text Search Endpoint
-@bp.route("/", methods=["GET"])
+@bp.route("/", methods=["GET", "OPTIONS"])
@jwt_required()
@min_role_required(UserRole.PUBLIC)
def text_search():
@@ -411,7 +456,8 @@ def text_search():
else:
continue
- page.append(item.model_dump())
+ if item:
+ page.append(item.model_dump())
response = add_pagination_wrapper(
page_data=page, total=total_results,
diff --git a/backend/routes/units.py b/backend/routes/units.py
index ca95e38f2..d3b76b8f0 100644
--- a/backend/routes/units.py
+++ b/backend/routes/units.py
@@ -13,7 +13,7 @@
unit_service = UnitService()
-@bp.route("", methods=["GET"])
+@bp.route("", methods=["GET", "OPTIONS"], strict_slashes=False)
@jwt_required()
@min_role_required(UserRole.PUBLIC)
def get_all_units():
diff --git a/frontend/components/search/SearchResultsTabs.tsx b/frontend/components/search/SearchResultsTabs.tsx
index 90dd7ade1..1eb83e9a6 100644
--- a/frontend/components/search/SearchResultsTabs.tsx
+++ b/frontend/components/search/SearchResultsTabs.tsx
@@ -18,14 +18,13 @@ export default function SearchResultsTabs({ tab, updateTab }: SearchResultsTabsP
slotProps={{ indicator: { style: { backgroundColor: "black" } } }}
sx={{
"& .MuiTab-root": { color: "black" }
- }}
- >
+ }}>
-
-
+
+
)
diff --git a/frontend/providers/SearchProvider.tsx b/frontend/providers/SearchProvider.tsx
index 4d99c3d10..c24405217 100644
--- a/frontend/providers/SearchProvider.tsx
+++ b/frontend/providers/SearchProvider.tsx
@@ -7,6 +7,7 @@ import API_ROUTES, { apiBaseUrl } from "@/utils/apiRoutes"
import { ApiError } from "@/utils/apiError"
import { useRouter, useSearchParams } from "next/navigation"
import { getParamKeys } from "./config"
+import { transformSearchResults } from "@/utils/searchTransform"
interface SearchContext {
searchAll: (
@@ -86,6 +87,10 @@ function useHook(): SearchContext {
switch (tab) {
case 1:
return API_ROUTES.search.officers
+ case 2:
+ return API_ROUTES.search.agencies
+ case 3:
+ return API_ROUTES.search.units
default:
return API_ROUTES.search.all
}
@@ -139,9 +144,25 @@ function useHook(): SearchContext {
}
const data: PaginatedSearchResponses = await response.json()
- setResults(data)
+
+ // Determine content type from current tab
+ let contentType: string | undefined
+ switch (updatedTab !== undefined ? updatedTab : tab) {
+ case 1:
+ contentType = "Officer"
+ break
+ case 2:
+ contentType = "Agency"
+ break
+ case 3:
+ contentType = "Unit"
+ break
+ }
+
+ const transformedData = transformSearchResults(data, contentType)
+ setResults(transformedData)
setError(null)
- return data
+ return transformedData
} catch (err) {
if (err instanceof Error && err.name === "AbortError") {
return { results: [] }
diff --git a/frontend/utils/apiRoutes.ts b/frontend/utils/apiRoutes.ts
index ce23f3d50..7d7ab13eb 100644
--- a/frontend/utils/apiRoutes.ts
+++ b/frontend/utils/apiRoutes.ts
@@ -9,7 +9,9 @@ const API_ROUTES = {
search: {
all: "/search/",
incidents: "/incidents/search",
- officers: "/officers/"
+ officers: "/officers",
+ agencies: "/agencies",
+ units: "/units/"
},
users: {
self: "/users/self"