Skip to content

Commit

Permalink
feat: ✨ add endpoint to contact experts via email
Browse files Browse the repository at this point in the history
  • Loading branch information
thomashbrnrd committed Jul 26, 2024
1 parent 6955bad commit de89ff2
Show file tree
Hide file tree
Showing 13 changed files with 2,550 additions and 8 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ jobs:
S3_BUCKET_NAME: basegun-s3
EMAIL_HOST: mailpit
EMAIL_PORT: 1025
JWKS_URL: https://token.actions.githubusercontent.com/.well-known/jwks
OPENIDCONNECT_URL: https://token.actions.githubusercontent.com/.well-known/openid-configuration
KID: cc413527-173f-5a05-976e-9c52b1d7b431
steps:
- run: cd /app && pytest
services:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v3

- name: Start stack using docker compose
run: docker compose up -d
run: docker compose -f docker-compose.yml -f docker-compose.override.ci.yml up -d

- name: Cypress run
uses: cypress-io/github-action@v6
Expand Down
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ user-agents==2.2.0
boto3==1.28.39
autodynatrace==2.0.0
PyJWT==2.8.0
cryptography==42.0.8
# ML
basegun-ml==1.0.1
# Dev
Expand Down
7 changes: 7 additions & 0 deletions backend/src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from smtplib import SMTP

import boto3
from fastapi.security import OpenIdConnect
from gelfformatter import GelfFormatter
from jwt import PyJWKClient

CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))

Expand Down Expand Up @@ -124,3 +126,8 @@ def get_base_logs(user_agent, user_id: str) -> dict:

# Emails
SMTPClient = SMTP(os.environ["EMAIL_HOST"], os.environ["EMAIL_PORT"])

# Authentication
JWKS_CLIENT = PyJWKClient(os.environ["JWKS_URL"])
PUBLIC_KEY = JWKS_CLIENT.get_signing_key(os.environ["KID"]).key
OAUTH2_SCHEME = OpenIdConnect(openIdConnectUrl=os.environ["OPENIDCONNECT_URL"])
20 changes: 20 additions & 0 deletions backend/src/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from pydantic import BaseModel


class EmailData(BaseModel):
firstname: str
lastname: str
nigend: str
service: str | None
phone: str
email: str
seizure: str
una_or_procedure_number: str
gun_type: str
gun_length: int | None
gun_barrel_length: int | None
markings_description: str | None
right_picture: str
left_picture: str
markings_pictures: str
magazine_picture: str | None
36 changes: 34 additions & 2 deletions backend/src/router.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
import os
import time
from typing import Union
from typing import Annotated, Union
from uuid import uuid4

from basegun_ml.classification import get_typology
Expand All @@ -10,6 +10,7 @@
APIRouter,
BackgroundTasks,
Cookie,
Depends,
File,
Form,
HTTPException,
Expand All @@ -21,7 +22,8 @@
from user_agents import parse

from .config import APP_VERSION, S3_PREFIX, TYPOLOGIES_MEASURED, get_base_logs
from .utils import upload_image
from .models import EmailData
from .utils import get_current_user, send_mail, upload_image

router = APIRouter(prefix="/api")

Expand Down Expand Up @@ -163,3 +165,33 @@ async def log_identification_dummy(
extras_logging["bg_" + key] = res[key]

logging.info("Identification dummy", extra=extras_logging)


# Currently missing because we don't know if we can send attachements or if target can use S3 link
# Photo face droite : {request.right_picture}
# Photo face gauche : {request.left_picture}
# Photo des marquages : {request.markings_pictures}
# Photo du chargeur : {request.magazine_picture}
@router.post("/expert-contact")
async def expert_contact(
request: EmailData,
current_user: Annotated[dict, Depends(get_current_user)],
):
send_mail(
subject="[Basegun] Demande d'identification",
to="[email protected]",
message=f"""
Nom : {request.lastname}
Prénom : {request.firstname}
NIGEND / matricule : {request.nigend}
Service d'affectation : {request.service}
Téléphone : {request.phone}
Email : {request.email}
Saisie : {request.seizure}
N° de procédure : {request.una_or_procedure_number}
Typologie de l'arme (épaule ou poing) : {request.gun_type}
Longueur de l'arme : {request.gun_length}
Longueur du canon de l'arme : {request.gun_barrel_length}
Précision sur les marquages présents sur l'arme : {request.markings_description}
""",
)
22 changes: 21 additions & 1 deletion backend/src/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
import time
from datetime import datetime
from email.message import EmailMessage
from typing import Annotated

import jwt
from fastapi import Depends, HTTPException, status
from src.config import SMTPClient

from .config import S3, S3_BUCKET_NAME
from .config import OAUTH2_SCHEME, PUBLIC_KEY, S3, S3_BUCKET_NAME


def upload_image(content: bytes, image_key: str):
Expand Down Expand Up @@ -33,3 +36,20 @@ def send_mail(subject: str, to: str, message: str):
msg["To"] = to
msg.set_content(message)
SMTPClient.send_message(msg)


async def get_current_user(token: Annotated[str, Depends(OAUTH2_SCHEME)]):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
return jwt.decode(
token.split()[1],
PUBLIC_KEY,
algorithms=["RS256"],
audience=["master-realm", "account"],
)
except jwt.InvalidTokenError:
raise credentials_exception
4 changes: 2 additions & 2 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import json
import os
import time
from io import BytesIO

import boto3
import pytest
Expand Down Expand Up @@ -119,6 +118,7 @@ def test_headers(self):
for header_to_add in HEADERS_TO_ADD:
assert header_to_add["name"].lower() in CURRENT_HEADERS


class TestUpload:
def test_revolver_without_card(self):
with open("./tests/revolver.jpg", "rb") as f:
Expand Down Expand Up @@ -150,4 +150,4 @@ def test_semi_auto_without_card(self):
assert response.data["confidence_level"] == "high"
assert response.data["gun_length"] is not None
assert response.data["gun_barrel_length"] is not None
assert response.data["conf_card"] is not None
assert response.data["conf_card"] is not None
22 changes: 22 additions & 0 deletions backend/tests/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import jwt
import pytest
from fastapi.testclient import TestClient
from src.config import PUBLIC_KEY
from src.main import app

client = TestClient(app)

token = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItWmVZS3ZiMFEwdmJyZ2tEc2I5Rk5YLTU3QkdEMjNxRWhnUE1kdERHNUY4In0.eyJleHAiOjE3MjE3Mzc5ODksImlhdCI6MTcyMTczNzA4OSwiYXV0aF90aW1lIjoxNzIxNzM2ODk4LCJqdGkiOiI0NTkxMzI5Zi02YjIzLTQxYmMtYjU4Yy03ZmM3NjFhYjIzMzgiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVhbG1zL21hc3RlciIsImF1ZCI6WyJtYXN0ZXItcmVhbG0iLCJhY2NvdW50Il0sInN1YiI6IjQ2YTUzMDM1LWExZGMtNDExOS1hZmYwLTM0NDY5OTJkMzFiOSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImJhc2VndW4iLCJzaWQiOiI5M2RlZjg3My1lZTIxLTRhY2YtOTI4Ny03N2UwNzA3OTcxODEiLCJhY3IiOiIwIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiY3JlYXRlLXJlYWxtIiwiZGVmYXVsdC1yb2xlcy1tYXN0ZXIiLCJvZmZsaW5lX2FjY2VzcyIsImFkbWluIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJtYXN0ZXItcmVhbG0iOnsicm9sZXMiOlsidmlldy1pZGVudGl0eS1wcm92aWRlcnMiLCJ2aWV3LXJlYWxtIiwibWFuYWdlLWlkZW50aXR5LXByb3ZpZGVycyIsImltcGVyc29uYXRpb24iLCJjcmVhdGUtY2xpZW50IiwibWFuYWdlLXVzZXJzIiwicXVlcnktcmVhbG1zIiwidmlldy1hdXRob3JpemF0aW9uIiwicXVlcnktY2xpZW50cyIsInF1ZXJ5LXVzZXJzIiwibWFuYWdlLWV2ZW50cyIsIm1hbmFnZS1yZWFsbSIsInZpZXctZXZlbnRzIiwidmlldy11c2VycyIsInZpZXctY2xpZW50cyIsIm1hbmFnZS1hdXRob3JpemF0aW9uIiwibWFuYWdlLWNsaWVudHMiLCJxdWVyeS1ncm91cHMiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWRtaW4ifQ.hU0hJgMQayorct84UK9jiXDWOoBZ2KgmGa-JK0OOvn5Dfq0i_uZEOwzSjNwsvsYjX5NtHRrVBIxIcYzD-6IubUB2eUk7dgbYTqcVyjkWFrjwuv6ieTYvk-OuUg5iCXSe67ZbFuQwvNCg_8ns3JhEAEgHD4mLmhkPDlln4FkK605vAGZ5bDDYuPbaBI3ao4zpFr837r8zP5BGnzsclzk-T9k03pbTZ2aIv3PqlhfBVl2rKM0KYYKL8n3zLvObFMnZSx22-AuTPKKxkv3IrpCX2Zr_pAv-Bb2dw9LfEy_jxKv1i175Awjy3ayLbltvMcRzDTEzZ7YdBiuKoGoYjdoHng"


@pytest.mark.skip(reason="Cannot currently run in CI.")
class TestAuthentication:
def test_jwks(self):
print(
jwt.decode(
token,
PUBLIC_KEY,
algorithms=["RS256"],
audience=["master-realm", "account"],
)
)
7 changes: 7 additions & 0 deletions docker-compose.override.ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Override environment variables in CI to use dummy OIDC
services:
backend:
environment:
- JWKS_URL=https://token.actions.githubusercontent.com/.well-known/jwks
- OPENIDCONNECT_URL=https://token.actions.githubusercontent.com/.well-known/openid-configuration
- KID=cc413527-173f-5a05-976e-9c52b1d7b431
7 changes: 6 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ services:
- AWS_SECRET_ACCESS_KEY=minioadmin
- EMAIL_HOST=mailpit
- EMAIL_PORT=1025
- JWKS_URL=http://keycloak:8080/realms/master/protocol/openid-connect/certs
- OPENIDCONNECT_URL=http://localhost:8080/realms/master/.well-known/openid-configuration
- KID=aaqEthfKrBUsIizu2Lk7bweeqxyn6Wi2thBoaNS3MQM
- http_proxy
- https_proxy
- no_proxy
Expand Down Expand Up @@ -55,12 +58,14 @@ services:
# Mock Passage2 OIDC
keycloak:
image: quay.io/keycloak/keycloak:25.0.0
command: start-dev
command: start-dev --import-realm
environment:
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=password
ports:
- 8080:8080
volumes:
- ./realm-export.json:/opt/keycloak/data/import/realm-export.json

# Mock Email server
mailpit:
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,16 @@ const routes: RouteRecordRaw[] = [
beforeEnter: (to, from) => {
mgr.getUser().then((user) => {
console.log(user);
if (user === null) mgr.signinRedirect();
if (user === null)
mgr
.signinRedirect()
.then((data) => console.log(data))
.catch((err) => {
console.log(err);
return {
name: "PageNotFound",
};
});
});
},
},
Expand Down
Loading

0 comments on commit de89ff2

Please sign in to comment.