Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion backend/auth/jwt.py
Original file line number Diff line number Diff line change
@@ -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"])
Expand All @@ -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"])
Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion backend/routes/agencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
2 changes: 1 addition & 1 deletion backend/routes/officers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
244 changes: 145 additions & 99 deletions backend/routes/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion backend/routes/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
7 changes: 3 additions & 4 deletions frontend/components/search/SearchResultsTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,13 @@ export default function SearchResultsTabs({ tab, updateTab }: SearchResultsTabsP
slotProps={{ indicator: { style: { backgroundColor: "black" } } }}
sx={{
"& .MuiTab-root": { color: "black" }
}}
>
}}>
<Tab label="All" />
<Tab label="Officer" />
<Tab label="Complaint" />
<Tab label="Agency" />
<Tab label="Unit" />
<Tab label="Litigation" />
<Tab label="Complaint" disabled sx={{ color: "#888888 !important" }} />
<Tab label="Litigation" disabled sx={{ color: "#888888 !important" }} />
</Tabs>
</Box>
)
Expand Down
Loading
Loading