Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add email client with local email server #513

Merged
merged 2 commits into from
Jul 26, 2024
Merged
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
13 changes: 13 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,21 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
S3_URL_ENDPOINT: https://s3.gra.io.cloud.ovh.net/
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:
mailpit:
image: axllent/mailpit
ports:
- 1025:1025
env:
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1

test-frontend-format:
name: Test Frontend Formatting
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
13 changes: 12 additions & 1 deletion backend/src/config.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import os
from datetime import datetime
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 @@ -119,4 +122,12 @@ def get_base_logs(user_agent, user_id: str) -> dict:
"epaule_a_un_coup_par_canon",
"epaule_a_verrou",
"epaule_semi_auto_style_chasse",
]
]

# 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]",
nutfdt marked this conversation as resolved.
Show resolved Hide resolved
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}
""",
)
34 changes: 33 additions & 1 deletion backend/src/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import logging
import time
from datetime import datetime
from email.message import EmailMessage
from typing import Annotated

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

from .config import OAUTH2_SCHEME, PUBLIC_KEY, S3, S3_BUCKET_NAME


def upload_image(content: bytes, image_key: str):
Expand All @@ -21,3 +27,29 @@ def upload_image(content: bytes, image_key: str):
"bg_image_url": image_key,
}
logging.info("Upload successful", extra=extras_logging)


def send_mail(subject: str, to: str, message: str):
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = "[email protected]"
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"],
)
)
14 changes: 14 additions & 0 deletions backend/tests/test_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from fastapi.testclient import TestClient
from src.main import app
from src.utils import send_mail

client = TestClient(app)


class TestEmail:
def test_email(self):
send_mail(
subject="Demande d'avis à un expert",
to="[email protected]",
message="Message de test",
)
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
21 changes: 19 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ services:
- S3_BUCKET_NAME=basegun-s3
- AWS_ACCESS_KEY_ID=minioadmin
- 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 @@ -53,9 +58,21 @@ 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
- 8080:8080
volumes:
- ./realm-export.json:/opt/keycloak/data/import/realm-export.json

# Mock Email server
mailpit:
image: axllent/mailpit
ports:
- 8025:8025
- 1025:1025
environment:
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
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
Loading