From 7147de0bc535bd34a82492b4fe66f8bd392a3a47 Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Sat, 9 Mar 2024 23:06:18 -0500 Subject: [PATCH 01/21] added create_incident tests --- backend/routes/incidents.py | 26 ++++++-- backend/tests/test_incidents.py | 106 ++++++++++++++++++++++++++++++-- 2 files changed, 120 insertions(+), 12 deletions(-) diff --git a/backend/routes/incidents.py b/backend/routes/incidents.py index 168a8f5c..0f59c10c 100644 --- a/backend/routes/incidents.py +++ b/backend/routes/incidents.py @@ -5,7 +5,7 @@ from backend.auth.jwt import min_role_required, contributor_has_partner from backend.mixpanel.mix import track_to_mp from mixpanel import MixpanelException -from flask import Blueprint, abort, current_app, request +from flask import Blueprint, abort, request from flask_jwt_extended.view_decorators import jwt_required from flask_jwt_extended import get_jwt from pydantic import BaseModel @@ -47,14 +47,28 @@ def get_incident(incident_id: int): @validate(json=CreateIncidentSchema) def create_incident(): """Create a single incident. - - Cannot be called in production environments """ - if current_app.env == "production": - abort(418) + body = request.context.json + jwt_decoded: dict[str, str] = get_jwt() + user_id = jwt_decoded["sub"] + permission = PartnerMember.query.filter( + PartnerMember.user_id == user_id, + PartnerMember.role.in_((MemberRole.PUBLISHER, MemberRole.ADMIN)), + ).first() + if permission is None: + abort(403) + existing_incident = Incident.query.filter_by( + source_id=body.source_id, + time_of_incident=body.time_of_incident, + longitude=body.longitude, + latitude=body.latitude, + location=body.location, + ).first() + if existing_incident: + abort(409, "Incident already exists") try: - incident = incident_to_orm(request.context.json) + incident = incident_to_orm(body) except Exception: abort(400) diff --git a/backend/tests/test_incidents.py b/backend/tests/test_incidents.py index acd61011..47e6401b 100644 --- a/backend/tests/test_incidents.py +++ b/backend/tests/test_incidents.py @@ -5,6 +5,8 @@ from backend.database import Incident, Partner, PrivacyStatus, User from typing import Any +member_email = "joe@partner.com" +example_password = "my_password" mock_partners = { "cpdp": {"name": "Citizens Police Data Project"}, "mpv": {"name": "Mapping Police Violence"}, @@ -53,18 +55,25 @@ @pytest.fixture -def example_incidents(db_session, client, contributor_access_token): +def example_incidents(db_session, client , partner_admin): for id, mock in mock_partners.items(): db_session.add(Partner(**mock)) db_session.commit() created = {} + access_token = res = client.post( + "api/v1/auth/login", + json={ + "email": partner_admin.email , + "password": "my_password" + }, + ).json["access_token"] for name, mock in mock_incidents.items(): res = client.post( "/api/v1/incidents/create", json=mock, headers={ - "Authorization": "Bearer {0}".format(contributor_access_token) + "Authorization": "Bearer {0}".format(access_token) }, ) assert res.status_code == 200 @@ -73,9 +82,6 @@ def example_incidents(db_session, client, contributor_access_token): def test_create_incident(db_session, example_incidents): - # TODO: test that the User actually has permission to create an - # incident for the partner - # expected = mock_incidents["domestic"] created = example_incidents["domestic"] incident_obj = ( @@ -88,7 +94,95 @@ def test_create_incident(db_session, example_incidents): incident_obj.perpetrators[i].id == created["perpetrators"][i]["id"] ) assert incident_obj.use_of_force[0].id == created["use_of_force"][0]["id"] - # assert incident_obj.source == expected["source"] + assert incident_obj.location == created["location"] + assert incident_obj.description == created["description"] + + +""" +test for creating a new incident and +creating same incident +""" + + +def test_create_incident_exists( + client, + partner_admin, + +): + created = {} + access_token = client.post( + "api/v1/auth/login", + json={ + "email": partner_admin.email , + "password": "my_password" + }, + ).json["access_token"] + # creating new incident + res = client.post( + "/api/v1/incidents/create", + headers={"Authorization": f"Bearer {access_token}"}, + json=mock_incidents["domestic"] + ) + created["domestic"] = res.json + domestic_instance = created["domestic"] + print(created) + assert res.status_code == 200 + expected = mock_incidents["domestic"] + incident_obj = Incident.query.filter_by( + time_of_incident=expected["time_of_incident"] + ).first() + date_format = '%Y-%m-%d %H:%M:%S' + date_obj = datetime.strptime( + expected["time_of_incident"], + date_format) + assert incident_obj.time_of_incident == date_obj + for i in [0, 1]: + assert ( + incident_obj.perpetrators[i].id == + domestic_instance["perpetrators"][i]["id"] + ) + assert (incident_obj.use_of_force[0].id == + domestic_instance["use_of_force"][0]["id"]) + assert incident_obj.location == domestic_instance["location"] + assert incident_obj.description == domestic_instance["description"] + + # creating the same incident + # should not be able to create incident + res = client.post( + "/api/v1/incidents/create", + headers={"Authorization": f"Bearer {access_token}"}, + json=mock_incidents["domestic"] + ) + + assert res.status_code == 409 + + +""" +creating incident when user +does not have permission +""" + + +def test_create_incident_no_permission( + client, + example_user + +): + access_token = client.post( + "api/v1/auth/login", + json={ + "email": example_user.email, + "password": "my_password" + }, + ).json["access_token"] + print(access_token) + # creating new incident + res = client.post( + "/api/v1/incidents/create", + headers={"Authorization": f"Bearer {access_token}"}, + json=mock_incidents["domestic"] + ) + assert res.status_code == 403 def test_get_incident(app, client, db_session, access_token): From d03729941831d9111a22c5e59d249133948107ae Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Sat, 9 Mar 2024 23:07:16 -0500 Subject: [PATCH 02/21] fix format --- backend/tests/test_incidents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/test_incidents.py b/backend/tests/test_incidents.py index 47e6401b..d07a2f9a 100644 --- a/backend/tests/test_incidents.py +++ b/backend/tests/test_incidents.py @@ -116,7 +116,7 @@ def test_create_incident_exists( "email": partner_admin.email , "password": "my_password" }, - ).json["access_token"] + ).json["access_token"] # creating new incident res = client.post( "/api/v1/incidents/create", From 7436e121cb7030da95e437e490a59b9054cd4453 Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Fri, 15 Mar 2024 23:03:28 -0400 Subject: [PATCH 03/21] PR review changes --- backend/routes/incidents.py | 75 +++++++++++++++++---------------- backend/routes/partners.py | 60 ++++++++++++++++++++++++-- backend/tests/test_incidents.py | 14 ++---- backend/tests/test_partners.py | 30 +++++++++++++ 4 files changed, 128 insertions(+), 51 deletions(-) diff --git a/backend/routes/incidents.py b/backend/routes/incidents.py index 0f59c10c..914248a3 100644 --- a/backend/routes/incidents.py +++ b/backend/routes/incidents.py @@ -11,6 +11,7 @@ from pydantic import BaseModel from typing import Any + from ..database import ( Incident, db, @@ -40,43 +41,6 @@ def get_incident(incident_id: int): return incident_orm_to_json(Incident.get(incident_id)) -@bp.route("/create", methods=["POST"]) -@jwt_required() -@min_role_required(UserRole.CONTRIBUTOR) -@contributor_has_partner() -@validate(json=CreateIncidentSchema) -def create_incident(): - """Create a single incident. - """ - body = request.context.json - jwt_decoded: dict[str, str] = get_jwt() - user_id = jwt_decoded["sub"] - permission = PartnerMember.query.filter( - PartnerMember.user_id == user_id, - PartnerMember.role.in_((MemberRole.PUBLISHER, MemberRole.ADMIN)), - ).first() - if permission is None: - abort(403) - - existing_incident = Incident.query.filter_by( - source_id=body.source_id, - time_of_incident=body.time_of_incident, - longitude=body.longitude, - latitude=body.latitude, - location=body.location, - ).first() - if existing_incident: - abort(409, "Incident already exists") - try: - incident = incident_to_orm(body) - except Exception: - abort(400) - - created = incident.create() - track_to_mp(request, "create_incident", {"source_id": incident.source_id}) - return incident_orm_to_json(created) - - class SearchIncidentsSchema(BaseModel): location: Optional[str] = None dateStart: Optional[str] = None @@ -255,3 +219,40 @@ def delete_incident(incident_id: int): incident.delete() return {"message": "Incident deleted successfully"}, 204 + + +@bp.route("/create", methods=["POST"]) +@jwt_required() +@min_role_required(UserRole.CONTRIBUTOR) +@contributor_has_partner() +@validate(json=CreateIncidentSchema) +def create_incident(): + """Create a single incident. + """ + body = request.context.json + jwt_decoded: dict[str, str] = get_jwt() + user_id = jwt_decoded["sub"] + permission = PartnerMember.query.filter( + PartnerMember.user_id == user_id, + PartnerMember.role.in_((MemberRole.PUBLISHER, MemberRole.ADMIN)), + ).first() + if permission is None: + abort(403) + + existing_incident = Incident.query.filter_by( + source_id=body.source_id, + time_of_incident=body.time_of_incident, + longitude=body.longitude, + latitude=body.latitude, + location=body.location, + ).first() + if existing_incident: + abort(409, "Incident already exists") + try: + incident = incident_to_orm(body) + except Exception: + abort(400) + + created = incident.create() + track_to_mp(request, "create_incident", {"source_id": incident.source_id}) + return incident_orm_to_json(created) diff --git a/backend/routes/partners.py b/backend/routes/partners.py index 78745437..930ab623 100644 --- a/backend/routes/partners.py +++ b/backend/routes/partners.py @@ -1,6 +1,6 @@ from datetime import datetime -from backend.auth.jwt import min_role_required +from backend.auth.jwt import min_role_required, contributor_has_partner from backend.mixpanel.mix import track_to_mp from backend.database.models.user import User, UserRole from flask import Blueprint, abort, current_app, request, jsonify @@ -16,6 +16,7 @@ db, Invitation, StagedInvitation, + Incident, ) from ..dto import InviteUserDTO from flask_mail import Message @@ -27,7 +28,10 @@ partner_to_orm, validate, AddMemberSchema, - partner_member_to_orm + partner_member_to_orm, + CreateIncidentSchema, + incident_orm_to_json, + incident_to_orm ) @@ -69,6 +73,13 @@ def create_partner(): ) make_admin.create() + # update to UserRole contributor status + user_id = get_jwt()["sub"] + user = User.query.filter_by( + id=user_id + ).first() + user.role = UserRole.CONTRIBUTOR + track_to_mp( request, "create_partner", @@ -399,6 +410,11 @@ def role_change(): if user_found and user_found.role != "Administrator": user_found.role = body["role"] db.session.commit() + if body["role"] == "Adminstrator" or body["role"] == "Publisher": + user_instance = User.query.filter_by( + id=body["user_id"] + ).first() + user_instance.role = UserRole.CONTRIBUTOR return { "status" : "ok", "message" : "Role has been updated!" @@ -406,7 +422,8 @@ def role_change(): else: return { "status" : "Error", - "message" : "User not found in this organization" + "message" : ("User not found in this organization or can't\ + change the role of Admin") }, 400 except Exception as e: db.session.rollback @@ -520,3 +537,40 @@ def add_member_to_partner_testing(partner_id: int): }, ) return partner_member_orm_to_json(created) + + +@bp.route("/create-incident", methods=["POST"]) +@jwt_required() +@min_role_required(UserRole.CONTRIBUTOR) +@contributor_has_partner() +@validate(json=CreateIncidentSchema) +def create_incident(): + """Create a single incident. + """ + body = request.context.json + jwt_decoded: dict[str, str] = get_jwt() + user_id = jwt_decoded["sub"] + permission = PartnerMember.query.filter( + PartnerMember.user_id == user_id, + PartnerMember.role.in_((MemberRole.PUBLISHER, MemberRole.ADMIN)), + ).first() + if permission is None: + abort(403) + + existing_incident = Incident.query.filter_by( + source_id=body.source_id, + time_of_incident=body.time_of_incident, + longitude=body.longitude, + latitude=body.latitude, + location=body.location, + ).first() + if existing_incident: + abort(409, "Incident already exists") + try: + incident = incident_to_orm(body) + except Exception: + abort(400) + + created = incident.create() + track_to_mp(request, "create_incident", {"source_id": incident.source_id}) + return incident_orm_to_json(created) diff --git a/backend/tests/test_incidents.py b/backend/tests/test_incidents.py index d07a2f9a..5ccdc308 100644 --- a/backend/tests/test_incidents.py +++ b/backend/tests/test_incidents.py @@ -55,25 +55,18 @@ @pytest.fixture -def example_incidents(db_session, client , partner_admin): +def example_incidents(db_session, client , contributor_access_token): for id, mock in mock_partners.items(): db_session.add(Partner(**mock)) db_session.commit() created = {} - access_token = res = client.post( - "api/v1/auth/login", - json={ - "email": partner_admin.email , - "password": "my_password" - }, - ).json["access_token"] for name, mock in mock_incidents.items(): res = client.post( "/api/v1/incidents/create", json=mock, headers={ - "Authorization": "Bearer {0}".format(access_token) + "Authorization": "Bearer {0}".format(contributor_access_token) }, ) assert res.status_code == 200 @@ -125,7 +118,6 @@ def test_create_incident_exists( ) created["domestic"] = res.json domestic_instance = created["domestic"] - print(created) assert res.status_code == 200 expected = mock_incidents["domestic"] incident_obj = Incident.query.filter_by( @@ -474,4 +466,4 @@ def test_delete_incident_nonexsitent_incident( f"/api/v1/incidents/{999}", headers={"Authorization": f"Bearer {access_token}"}, ) - assert res.status_code == 404 + assert res.status_code == 404 \ No newline at end of file diff --git a/backend/tests/test_partners.py b/backend/tests/test_partners.py index 3687fef2..702061d9 100644 --- a/backend/tests/test_partners.py +++ b/backend/tests/test_partners.py @@ -703,6 +703,36 @@ def test_role_change( partner_id=example_partner.id, ).first() assert role_change.role == "Publisher" and role_change is not None + """assertion to see if UserRole is updated to Contributor + after MemberRole changed to Admin or Publisher + """ + user_instance = User.query.filter_by( + id=example_members["member2"]["user_id"] + ).first() + assert user_instance.role == UserRole.CONTRIBUTOR + + res = client.patch( + "/api/v1/partners/role_change", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "user_id" : example_members["member2"]["user_id"], + "partner_id": example_partner.id, + "role": "Administrator" + } + ) + assert res.status_code == 200 + role_change = PartnerMember.query.filter_by( + user_id=example_members["member2"]["user_id"], + partner_id=example_partner.id, + ).first() + assert role_change.role == "Administrator" and role_change is not None + """assertion to see if UserRole is updated to Contributor + after MemberRole changed to Admin or Publisher + """ + user_instance = User.query.filter_by( + id=example_members["member2"]["user_id"] + ).first() + assert user_instance.role == UserRole.CONTRIBUTOR """ From 2dc32a2c87007cd3ec8a5b57c9524e3c8b447d47 Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Fri, 15 Mar 2024 23:31:04 -0400 Subject: [PATCH 04/21] Style fix --- backend/tests/test_incidents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/test_incidents.py b/backend/tests/test_incidents.py index 5ccdc308..456ab0c8 100644 --- a/backend/tests/test_incidents.py +++ b/backend/tests/test_incidents.py @@ -466,4 +466,4 @@ def test_delete_incident_nonexsitent_incident( f"/api/v1/incidents/{999}", headers={"Authorization": f"Bearer {access_token}"}, ) - assert res.status_code == 404 \ No newline at end of file + assert res.status_code == 404 From 1c6d6464ecfd0fedf10dd49ec8b607565cd78308 Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Mon, 18 Mar 2024 19:11:06 -0400 Subject: [PATCH 05/21] removing redundant code --- backend/routes/partners.py | 43 +------------------------------------- 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/backend/routes/partners.py b/backend/routes/partners.py index 930ab623..f8205782 100644 --- a/backend/routes/partners.py +++ b/backend/routes/partners.py @@ -1,6 +1,6 @@ from datetime import datetime -from backend.auth.jwt import min_role_required, contributor_has_partner +from backend.auth.jwt import min_role_required from backend.mixpanel.mix import track_to_mp from backend.database.models.user import User, UserRole from flask import Blueprint, abort, current_app, request, jsonify @@ -16,7 +16,6 @@ db, Invitation, StagedInvitation, - Incident, ) from ..dto import InviteUserDTO from flask_mail import Message @@ -29,9 +28,6 @@ validate, AddMemberSchema, partner_member_to_orm, - CreateIncidentSchema, - incident_orm_to_json, - incident_to_orm ) @@ -537,40 +533,3 @@ def add_member_to_partner_testing(partner_id: int): }, ) return partner_member_orm_to_json(created) - - -@bp.route("/create-incident", methods=["POST"]) -@jwt_required() -@min_role_required(UserRole.CONTRIBUTOR) -@contributor_has_partner() -@validate(json=CreateIncidentSchema) -def create_incident(): - """Create a single incident. - """ - body = request.context.json - jwt_decoded: dict[str, str] = get_jwt() - user_id = jwt_decoded["sub"] - permission = PartnerMember.query.filter( - PartnerMember.user_id == user_id, - PartnerMember.role.in_((MemberRole.PUBLISHER, MemberRole.ADMIN)), - ).first() - if permission is None: - abort(403) - - existing_incident = Incident.query.filter_by( - source_id=body.source_id, - time_of_incident=body.time_of_incident, - longitude=body.longitude, - latitude=body.latitude, - location=body.location, - ).first() - if existing_incident: - abort(409, "Incident already exists") - try: - incident = incident_to_orm(body) - except Exception: - abort(400) - - created = incident.create() - track_to_mp(request, "create_incident", {"source_id": incident.source_id}) - return incident_orm_to_json(created) From df487722b492aab204753d5df552079291fd89ff Mon Sep 17 00:00:00 2001 From: Mike Yavorsky Date: Tue, 5 Mar 2024 21:11:21 -0500 Subject: [PATCH 06/21] Update README.md (#353) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3840feba..573575ce 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ MIXPANEL_TOKEN=your_mixpanel_token > **Note** > When running locally, you may need to update one of the ports in the `.env` file if it conflicts with another application on your machine. -3. Build and run the project with `docker-compose build; docker-compose up -d; docker-compose logs -f app` +3. Build and run the project with `docker-compose build && docker-compose up -d && docker-compose logs -f` ## Installation (Frontend Only) From 3bc368c553157aae59bdcc17b3de5b9a61607ebe Mon Sep 17 00:00:00 2001 From: Joshua Rodriguez <97762447+joshua-rdrgz@users.noreply.github.com> Date: Wed, 6 Mar 2024 08:34:54 -0600 Subject: [PATCH 07/21] refactor api helpers for readability/scalability (#348) --- frontend/helpers/api/api.ts | 215 -------------------- frontend/helpers/api/auth/auth.ts | 61 ++++++ frontend/helpers/api/auth/index.ts | 2 + frontend/helpers/api/auth/types.ts | 41 ++++ frontend/helpers/api/base.ts | 31 +++ frontend/helpers/api/config.ts | 1 + frontend/helpers/api/incidents/incidents.ts | 27 +++ frontend/helpers/api/incidents/index.ts | 2 + frontend/helpers/api/incidents/types.ts | 73 +++++++ frontend/helpers/api/index.ts | 5 +- 10 files changed, 242 insertions(+), 216 deletions(-) delete mode 100644 frontend/helpers/api/api.ts create mode 100644 frontend/helpers/api/auth/auth.ts create mode 100644 frontend/helpers/api/auth/index.ts create mode 100644 frontend/helpers/api/auth/types.ts create mode 100644 frontend/helpers/api/base.ts create mode 100644 frontend/helpers/api/config.ts create mode 100644 frontend/helpers/api/incidents/incidents.ts create mode 100644 frontend/helpers/api/incidents/index.ts create mode 100644 frontend/helpers/api/incidents/types.ts diff --git a/frontend/helpers/api/api.ts b/frontend/helpers/api/api.ts deleted file mode 100644 index 981e2c0d..00000000 --- a/frontend/helpers/api/api.ts +++ /dev/null @@ -1,215 +0,0 @@ -import axiosModule, { AxiosRequestConfig } from "axios" - -export type AccessToken = string - -export interface User { - active: boolean - role: string - email: string - emailConfirmedAt?: string - firstName?: string - lastName?: string - phoneNumber?: string -} - -export interface NewUser { - email: string - password: string - firstName?: string - lastName?: string - phoneNumber?: string -} - -export interface LoginCredentials { - email: string - password: string -} - -export interface ForgotPassword { - email: string -} - -export interface ResetPasswordRequest extends AuthenticatedRequest { - accessToken: string - password: string -} - -export interface ResetPasswordResponse { - message: string -} - -export interface Source { - name?: string - id?: number - url?: string - contact_email?: string -} - -export interface Perpetrator { - first_name?: string - last_name?: string -} - -export enum Rank { - TECHNICIAN = "Technician", - OFFICER = "Officer", - DETECTIVE = "Detective", - CORPORAL = "Corporal", - SERGEANT = "Sergeant", - LIEUTENANT = "Lieutenant", - CAPTAIN = "Captain", - DEPUTY = "Deputy", - CHIEF = "Chief" -} - -export interface Officer { - id?: number - first_name?: string - last_name?: string - race?: string - ethnicity?: string - gender?: string - rank?: Rank - star?: string - date_of_birth?: Date -} - -export interface UseOfForce { - item?: string -} -export interface Incident { - id: number - source?: Source - source_id?: number - location?: string - locationLonLat?: [number, number] //TODO: Backend data does not return locationLonLat attribute. Remove this and refactor frontend - latitude?: number - longitude?: number - time_of_incident?: string - department?: string - perpetrators: Perpetrator[] - description?: string - use_of_force?: UseOfForce[] -} - -interface AuthenticatedRequest { - accessToken: AccessToken -} - -export type RegisterRequest = NewUser -export type LoginRequest = LoginCredentials -export type WhoamiRequest = AuthenticatedRequest -export interface IncidentSearchRequest extends AuthenticatedRequest { - description?: string - dateStart?: string - dateEnd?: string - location?: string - source?: string - page?: number - perPage?: number -} - -export type IncidentSearchResponse = { - results: Incident[] - page: number - totalPages: number - totalResults: number -} - -export const baseURL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:5000/api/v1" - -const axios = axiosModule.create({ - baseURL, - timeout: 5000 -}) - -export function login(data: LoginRequest): Promise { - return request({ - url: "/auth/login", - method: "POST", - data - }).then(({ access_token }) => access_token) -} - -export function register(data: RegisterRequest): Promise { - return request({ - url: "/auth/register", - method: "POST", - data - }).then(({ access_token }) => access_token) -} - -export function forgotPassowrd(data: ForgotPassword): Promise { - return request({ - url: "/auth/forgotPassword", - method: "POST", - data - }) -} - -export function resetPassword(req: ResetPasswordRequest): Promise { - const { accessToken } = req - - return request({ - url: `/auth/resetPassword`, - method: "POST", - data: { password: req.password }, - accessToken - }) -} - -export function whoami({ accessToken }: WhoamiRequest): Promise { - return request({ - url: "/auth/whoami", - method: "GET", - accessToken - }).then(({ active, email, email_confirmed_at, first_name, last_name, phone_number, role }) => ({ - active, - email, - emailConfirmedAt: email_confirmed_at, - firstName: first_name, - lastName: last_name, - phoneNumber: phone_number, - role: role - })) -} - -export function searchIncidents({ - accessToken, - dateStart, - dateEnd, - ...rest -}: IncidentSearchRequest): Promise { - if (dateStart) dateStart = new Date(dateStart).toISOString().slice(0, -1) - if (dateEnd) dateEnd = new Date(dateEnd).toISOString().slice(0, -1) - - return request({ - url: "/incidents/search", - method: "POST", - accessToken, - data: { dateStart, dateEnd, ...rest } - }) -} - -export async function getIncidentById(id: number, accessToken: string): Promise { - return request({ - url: `/incidents/get/${id}`, - method: "GET", - accessToken - }) -} - -function request({ accessToken, ...config }: AxiosRequestConfig & { accessToken?: AccessToken }) { - let { headers, ...rest } = config - if (accessToken) { - headers = { - Authorization: `Bearer ${accessToken}`, - ...headers - } - } - - return axios({ - headers, - ...rest - }).then((response) => response.data) -} diff --git a/frontend/helpers/api/auth/auth.ts b/frontend/helpers/api/auth/auth.ts new file mode 100644 index 00000000..169e377d --- /dev/null +++ b/frontend/helpers/api/auth/auth.ts @@ -0,0 +1,61 @@ +import { request, AccessToken } from "../base" +import { + User, + RegisterRequest, + LoginRequest, + ForgotPassword, + ResetPasswordRequest, + ResetPasswordResponse, + WhoamiRequest +} from "./types" + +export function login(data: LoginRequest): Promise { + return request({ + url: "/auth/login", + method: "POST", + data + }).then(({ access_token }) => access_token) +} + +export function register(data: RegisterRequest): Promise { + return request({ + url: "/auth/register", + method: "POST", + data + }).then(({ access_token }) => access_token) +} + +export function forgotPassowrd(data: ForgotPassword): Promise { + return request({ + url: "/auth/forgotPassword", + method: "POST", + data + }) +} + +export function resetPassword(req: ResetPasswordRequest): Promise { + const { accessToken } = req + + return request({ + url: `/auth/resetPassword`, + method: "POST", + data: { password: req.password }, + accessToken + }) +} + +export function whoami({ accessToken }: WhoamiRequest): Promise { + return request({ + url: "/auth/whoami", + method: "GET", + accessToken + }).then(({ active, email, email_confirmed_at, first_name, last_name, phone_number, role }) => ({ + active, + email, + emailConfirmedAt: email_confirmed_at, + firstName: first_name, + lastName: last_name, + phoneNumber: phone_number, + role: role + })) +} diff --git a/frontend/helpers/api/auth/index.ts b/frontend/helpers/api/auth/index.ts new file mode 100644 index 00000000..6f7e0d0b --- /dev/null +++ b/frontend/helpers/api/auth/index.ts @@ -0,0 +1,2 @@ +export * from "./types" +export * from "./auth" diff --git a/frontend/helpers/api/auth/types.ts b/frontend/helpers/api/auth/types.ts new file mode 100644 index 00000000..e5f735ed --- /dev/null +++ b/frontend/helpers/api/auth/types.ts @@ -0,0 +1,41 @@ +import { AuthenticatedRequest } from "../base" + +export interface User { + active: boolean + role: string + email: string + emailConfirmedAt?: string + firstName?: string + lastName?: string + phoneNumber?: string +} + +export interface NewUser { + email: string + password: string + firstName?: string + lastName?: string + phoneNumber?: string +} + +export interface LoginCredentials { + email: string + password: string +} + +export interface ForgotPassword { + email: string +} + +export interface ResetPasswordRequest extends AuthenticatedRequest { + accessToken: string + password: string +} + +export interface ResetPasswordResponse { + message: string +} + +export type RegisterRequest = NewUser +export type LoginRequest = LoginCredentials +export type WhoamiRequest = AuthenticatedRequest diff --git a/frontend/helpers/api/base.ts b/frontend/helpers/api/base.ts new file mode 100644 index 00000000..43ecf4c5 --- /dev/null +++ b/frontend/helpers/api/base.ts @@ -0,0 +1,31 @@ +import axiosModule, { AxiosRequestConfig } from "axios" +import { baseURL } from "./config" + +export type AccessToken = string + +export interface AuthenticatedRequest { + accessToken: AccessToken +} + +const axios = axiosModule.create({ + baseURL, + timeout: 5000 +}) + +export function request({ + accessToken, + ...config +}: AxiosRequestConfig & { accessToken?: AccessToken }) { + let { headers, ...rest } = config + if (accessToken) { + headers = { + Authorization: `Bearer ${accessToken}`, + ...headers + } + } + + return axios({ + headers, + ...rest + }).then((response) => response.data) +} diff --git a/frontend/helpers/api/config.ts b/frontend/helpers/api/config.ts new file mode 100644 index 00000000..d0de1afb --- /dev/null +++ b/frontend/helpers/api/config.ts @@ -0,0 +1 @@ +export const baseURL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:5000/api/v1" diff --git a/frontend/helpers/api/incidents/incidents.ts b/frontend/helpers/api/incidents/incidents.ts new file mode 100644 index 00000000..1497d89b --- /dev/null +++ b/frontend/helpers/api/incidents/incidents.ts @@ -0,0 +1,27 @@ +import { AccessToken, request } from "../base" +import { Incident, IncidentSearchRequest, IncidentSearchResponse } from "./types" + +export function searchIncidents({ + accessToken, + dateStart, + dateEnd, + ...rest +}: IncidentSearchRequest): Promise { + if (dateStart) dateStart = new Date(dateStart).toISOString().slice(0, -1) + if (dateEnd) dateEnd = new Date(dateEnd).toISOString().slice(0, -1) + + return request({ + url: "/incidents/search", + method: "POST", + accessToken, + data: { dateStart, dateEnd, ...rest } + }) +} + +export async function getIncidentById(id: number, accessToken: AccessToken): Promise { + return request({ + url: `/incidents/get/${id}`, + method: "GET", + accessToken + }) +} diff --git a/frontend/helpers/api/incidents/index.ts b/frontend/helpers/api/incidents/index.ts new file mode 100644 index 00000000..4708ead6 --- /dev/null +++ b/frontend/helpers/api/incidents/index.ts @@ -0,0 +1,2 @@ +export * from "./types" +export * from "./incidents" diff --git a/frontend/helpers/api/incidents/types.ts b/frontend/helpers/api/incidents/types.ts new file mode 100644 index 00000000..184966b9 --- /dev/null +++ b/frontend/helpers/api/incidents/types.ts @@ -0,0 +1,73 @@ +import { AuthenticatedRequest } from "../base" + +export interface Source { + name?: string + id?: number + url?: string + contact_email?: string +} + +export interface Perpetrator { + first_name?: string + last_name?: string +} + +export enum Rank { + TECHNICIAN = "Technician", + OFFICER = "Officer", + DETECTIVE = "Detective", + CORPORAL = "Corporal", + SERGEANT = "Sergeant", + LIEUTENANT = "Lieutenant", + CAPTAIN = "Captain", + DEPUTY = "Deputy", + CHIEF = "Chief" +} + +export interface Officer { + id?: number + first_name?: string + last_name?: string + race?: string + ethnicity?: string + gender?: string + rank?: Rank + star?: string + date_of_birth?: Date +} + +export interface UseOfForce { + item?: string +} + +export interface Incident { + id: number + source?: Source + source_id?: number + location?: string + locationLonLat?: [number, number] //TODO: Backend data does not return locationLonLat attribute. Remove this and refactor frontend + latitude?: number + longitude?: number + time_of_incident?: string + department?: string + perpetrators: Perpetrator[] + description?: string + use_of_force?: UseOfForce[] +} + +export interface IncidentSearchRequest extends AuthenticatedRequest { + description?: string + dateStart?: string + dateEnd?: string + location?: string + source?: string + page?: number + perPage?: number +} + +export type IncidentSearchResponse = { + results: Incident[] + page: number + totalPages: number + totalResults: number +} diff --git a/frontend/helpers/api/index.ts b/frontend/helpers/api/index.ts index 502ce65f..fe98c512 100644 --- a/frontend/helpers/api/index.ts +++ b/frontend/helpers/api/index.ts @@ -1,2 +1,5 @@ -export * from "./api" +export * from "./base" +export * from "./config" +export * from "./auth" +export * from "./incidents" export { useMockServiceWorker, apiMode } from "./mocks/browser.setup" From 5e677a586e6064a727f8630199064f4d3db83a30 Mon Sep 17 00:00:00 2001 From: Joshua Rodriguez <97762447+joshua-rdrgz@users.noreply.github.com> Date: Fri, 8 Mar 2024 12:46:37 -0600 Subject: [PATCH 08/21] Frontend: React Query Installation for API Integration (#349) * Install necessary dependencies for API integration: @tanstack/react-query @tanstack/react-query-devtools react-hot-toast (for displaying results of API calls) * Add React Query/react-hot-toast setup in app providers * Update test snapshots & jest config Add dependencies to `esmNodeModules` for Jest to transform Update snapshots to include react-hot-toast toast wrapper div * Update @tanstack dependencies (to 4.36.1) --- frontend/helpers/providers.tsx | 31 ++- frontend/jest.config.js | 4 +- frontend/package-lock.json | 236 +++++++++++++++++- frontend/package.json | 3 + .../__snapshots__/forgot.test.tsx.snap | 3 + .../__snapshots__/incident.test.tsx.snap | 6 + .../__snapshots__/login.test.tsx.snap | 3 + .../__snapshots__/officer.test.tsx.snap | 6 + .../__snapshots__/passport.test.tsx.snap | 3 + .../__snapshots__/profile.test.tsx.snap | 3 + .../__snapshots__/register.test.tsx.snap | 3 + .../__snapshots__/reset.test.tsx.snap | 3 + .../__snapshots__/results-alert.test.tsx.snap | 6 + .../__snapshots__/search.test.tsx.snap | 3 + .../visualizations.test.tsx.snap | 5 +- frontend/yarn.lock | 82 +++++- 16 files changed, 380 insertions(+), 20 deletions(-) diff --git a/frontend/helpers/providers.tsx b/frontend/helpers/providers.tsx index 295cce83..fa902519 100644 --- a/frontend/helpers/providers.tsx +++ b/frontend/helpers/providers.tsx @@ -1,12 +1,35 @@ -import { SearchProvider } from "./search" +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { ReactQueryDevtools } from "@tanstack/react-query-devtools" +import { Toaster } from "react-hot-toast" import { AuthProvider } from "./auth" +import { SearchProvider } from "./search" + +/** + * Query Client responsible for housing cache of + * network requests, serves as basis + * for React Query functionality + */ +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 0, + retry: false + } + } +}) /** * Wraps components in application providers, which set up contexts to provide * services to components. */ export const Providers: React.FC = ({ children }) => ( - - {children} - + <> + + + + + {children} + + + ) diff --git a/frontend/jest.config.js b/frontend/jest.config.js index a02346f5..6c7b59e9 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -9,7 +9,9 @@ const esmNodeModules = [ "delaunator", "robust-predicates", "node-fetch", - "fetch-blob" + "fetch-blob", + "copy-anything", // dependency of @tanstack/react-query-devtools + "is-what" // dependency of @tanstack/react-query-devtools ] module.exports = { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9ee69373..a6e96bf6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,8 @@ "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toggle-group": "^1.0.4", + "@tanstack/react-query": "^4.36.1", + "@tanstack/react-query-devtools": "^4.36.1", "axios": "^0.21.1", "classnames": "^2.3.1", "d3": "^7.0.0", @@ -23,6 +25,7 @@ "react-accessible-dropdown-menu-hook": "^3.1.0", "react-dom": "^17.0.2", "react-hook-form": "^7.25.0", + "react-hot-toast": "^2.4.1", "react-responsive": "^9.0.0-beta.4", "react-tooltip": "^5.25.1", "topojson-client": "^3.1.0", @@ -11402,6 +11405,75 @@ "react": ">= 0.14.0" } }, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.11.8", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.11.8.tgz", + "integrity": "sha512-3VPh0SYMGCa5dWQEqNab87UpCMk+ANWHDP4ALs5PeEW9EpfTAbrezzaOk/OiM52IESViefkoAOYuxdoa04p6aA==", + "dependencies": { + "remove-accents": "0.4.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", + "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", + "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", + "dependencies": { + "@tanstack/query-core": "4.36.1", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.36.1.tgz", + "integrity": "sha512-WYku83CKP3OevnYSG8Y/QO9g0rT75v1om5IvcWUwiUZJ4LanYGLVCZ8TdFG5jfsq4Ej/lu2wwDAULEUnRIMBSw==", + "dependencies": { + "@tanstack/match-sorter-utils": "^8.7.0", + "superjson": "^1.10.0", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^4.36.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@testing-library/dom": { "version": "7.31.2", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.31.2.tgz", @@ -15822,6 +15894,20 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", "dev": true }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -16630,10 +16716,9 @@ "dev": true }, "node_modules/csstype": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", - "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==", - "devOptional": true + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/cyclist": { "version": "1.0.1", @@ -20589,6 +20674,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz", + "integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/graceful-fs": { "version": "4.2.9", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", @@ -22198,6 +22291,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/is-whitespace-character": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", @@ -30607,6 +30711,21 @@ "react": "^16.8.0 || ^17" } }, + "node_modules/react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "dependencies": { + "goober": "^2.1.10" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-inspector": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-5.1.1.tgz", @@ -31320,6 +31439,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -32944,6 +33068,17 @@ "stylis": "^3.5.0" } }, + "node_modules/superjson": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.3.tgz", + "integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -34450,6 +34585,14 @@ "react": "^16.8.0 || ^17.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util": { "version": "0.12.4", "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", @@ -44147,6 +44290,38 @@ } } }, + "@tanstack/match-sorter-utils": { + "version": "8.11.8", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.11.8.tgz", + "integrity": "sha512-3VPh0SYMGCa5dWQEqNab87UpCMk+ANWHDP4ALs5PeEW9EpfTAbrezzaOk/OiM52IESViefkoAOYuxdoa04p6aA==", + "requires": { + "remove-accents": "0.4.2" + } + }, + "@tanstack/query-core": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", + "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==" + }, + "@tanstack/react-query": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", + "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", + "requires": { + "@tanstack/query-core": "4.36.1", + "use-sync-external-store": "^1.2.0" + } + }, + "@tanstack/react-query-devtools": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.36.1.tgz", + "integrity": "sha512-WYku83CKP3OevnYSG8Y/QO9g0rT75v1om5IvcWUwiUZJ4LanYGLVCZ8TdFG5jfsq4Ej/lu2wwDAULEUnRIMBSw==", + "requires": { + "@tanstack/match-sorter-utils": "^8.7.0", + "superjson": "^1.10.0", + "use-sync-external-store": "^1.2.0" + } + }, "@testing-library/dom": { "version": "7.31.2", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.31.2.tgz", @@ -47825,6 +48000,14 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", "dev": true }, + "copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "requires": { + "is-what": "^4.1.8" + } + }, "copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -48459,10 +48642,9 @@ } }, "csstype": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", - "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==", - "devOptional": true + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "cyclist": { "version": "1.0.1", @@ -51564,6 +51746,12 @@ "slash": "^3.0.0" } }, + "goober": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz", + "integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==", + "requires": {} + }, "graceful-fs": { "version": "4.2.9", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", @@ -52747,6 +52935,11 @@ "call-bind": "^1.0.0" } }, + "is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==" + }, "is-whitespace-character": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", @@ -59246,6 +59439,14 @@ "integrity": "sha512-MyF4YXegIT/vfyZloTm98mpJwLUPfULdX37yPzXeijT1hePCkV8DN1IAnEufxgtqCpc7aFGRinegQwisUGZCnA==", "requires": {} }, + "react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "requires": { + "goober": "^2.1.10" + } + }, "react-inspector": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-5.1.1.tgz", @@ -59770,6 +59971,11 @@ "mdast-squeeze-paragraphs": "^4.0.0" } }, + "remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -61046,6 +61252,14 @@ "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==", "requires": {} }, + "superjson": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.3.tgz", + "integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==", + "requires": { + "copy-anything": "^3.0.2" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -62164,6 +62378,12 @@ "object-assign": "^4.1.1" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "util": { "version": "0.12.4", "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9b6fa86d..cbfe8f69 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,6 +35,8 @@ "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toggle-group": "^1.0.4", + "@tanstack/react-query": "^4.36.1", + "@tanstack/react-query-devtools": "^4.36.1", "axios": "^0.21.1", "classnames": "^2.3.1", "d3": "^7.0.0", @@ -43,6 +45,7 @@ "react-accessible-dropdown-menu-hook": "^3.1.0", "react-dom": "^17.0.2", "react-hook-form": "^7.25.0", + "react-hot-toast": "^2.4.1", "react-responsive": "^9.0.0-beta.4", "react-tooltip": "^5.25.1", "topojson-client": "^3.1.0", diff --git a/frontend/tests/snapshots/__snapshots__/forgot.test.tsx.snap b/frontend/tests/snapshots/__snapshots__/forgot.test.tsx.snap index 8e7cac9f..e95c4736 100644 --- a/frontend/tests/snapshots/__snapshots__/forgot.test.tsx.snap +++ b/frontend/tests/snapshots/__snapshots__/forgot.test.tsx.snap @@ -2,6 +2,9 @@ exports[`renders Forgot correctly 1`] = `
+
+
@@ -85,6 +88,9 @@ Object {
, "container":
+
diff --git a/frontend/tests/snapshots/__snapshots__/login.test.tsx.snap b/frontend/tests/snapshots/__snapshots__/login.test.tsx.snap index 7b77cb77..512627f4 100644 --- a/frontend/tests/snapshots/__snapshots__/login.test.tsx.snap +++ b/frontend/tests/snapshots/__snapshots__/login.test.tsx.snap @@ -2,6 +2,9 @@ exports[`renders Login correctly 1`] = `
+
+
@@ -431,6 +434,9 @@ Object {
, "container":
+
diff --git a/frontend/tests/snapshots/__snapshots__/passport.test.tsx.snap b/frontend/tests/snapshots/__snapshots__/passport.test.tsx.snap index 70615214..1cf2088a 100644 --- a/frontend/tests/snapshots/__snapshots__/passport.test.tsx.snap +++ b/frontend/tests/snapshots/__snapshots__/passport.test.tsx.snap @@ -2,6 +2,9 @@ exports[`renders Passport correctly 1`] = `
+
+
+
+
+