Add API gateway

This commit is contained in:
Santiago Lo Coco 2023-10-25 09:30:13 -03:00
parent d75fbeed21
commit d7760eefc9
35 changed files with 544 additions and 61 deletions

3
+
View File

@ -1,3 +0,0 @@
#!/bin/bash
ENV_DEV_FILE=/home/shadad/fids/flights-domain/.env.dev.example
sudo docker compose -f flights-domain/docker-compose.yml --env-file $ENV_DEV_FILE up --abort-on-container-exit --renew-anon-volumes

View File

@ -22,6 +22,9 @@ preparation:
- echo "FLIGHTS_INFO_PROD_IMAGE_NAME=${IMAGE_BASE}/flights-information:prod-${BUILD_ID}" >> context.env - echo "FLIGHTS_INFO_PROD_IMAGE_NAME=${IMAGE_BASE}/flights-information:prod-${BUILD_ID}" >> context.env
- echo "FLIGHTS_INFO_TEST_IMAGE_NAME=${IMAGE_BASE}/flights-information:test-${BUILD_ID}" >> context.env - echo "FLIGHTS_INFO_TEST_IMAGE_NAME=${IMAGE_BASE}/flights-information:test-${BUILD_ID}" >> context.env
- echo "GATEWAY_PROD_IMAGE_NAME=${IMAGE_BASE}/gateway:prod-${BUILD_ID}" >> context.env
- echo "GATEWAY_TEST_IMAGE_NAME=${IMAGE_BASE}/gateway:test-${BUILD_ID}" >> context.env
- echo "USER_MANAGER_PROD_IMAGE_NAME=${IMAGE_BASE}/user-manager:prod-${BUILD_ID}" >> context.env - echo "USER_MANAGER_PROD_IMAGE_NAME=${IMAGE_BASE}/user-manager:prod-${BUILD_ID}" >> context.env
- echo "USER_MANAGER_TEST_IMAGE_NAME=${IMAGE_BASE}/user-manager:test-${BUILD_ID}" >> context.env - echo "USER_MANAGER_TEST_IMAGE_NAME=${IMAGE_BASE}/user-manager:test-${BUILD_ID}" >> context.env
@ -116,6 +119,24 @@ build-screen-client:
- job: preparation - job: preparation
artifacts: true artifacts: true
build-gateway:
stage: build
tags:
- dev
script:
- export $(cat context.env | xargs)
- docker build gateway -f gateway/Dockerfile.prod -t ${GATEWAY_PROD_IMAGE_NAME}
- docker build gateway -f gateway/Dockerfile.test --build-arg "BASE_IMAGE=$GATEWAY_PROD_IMAGE_NAME" -t ${GATEWAY_TEST_IMAGE_NAME}
- docker login -u $CI_REGISTRY_USER --password $CI_JOB_TOKEN $CI_REGISTRY
- docker push ${GATEWAY_PROD_IMAGE_NAME}
- docker push ${GATEWAY_TEST_IMAGE_NAME}
needs:
- job: preparation
artifacts: true
test-auth-api: test-auth-api:
stage: test stage: test
tags: tags:
@ -174,6 +195,34 @@ test-flights-api:
- job: build-flights-api - job: build-flights-api
artifacts: true artifacts: true
test-gateway:
stage: test
tags:
- dev
script:
- export $(cat context.env | xargs)
- export API_IMAGE=$GATEWAY_TEST_IMAGE_NAME
- docker login -u $CI_REGISTRY_USER --password $CI_JOB_TOKEN $CI_REGISTRY
- docker compose -f gateway/docker-compose.yml --env-file $ENV_DEV_FILE down
- docker compose -f gateway/docker-compose.yml --env-file $ENV_DEV_FILE pull
- docker compose -f gateway/docker-compose.yml --env-file $ENV_DEV_FILE up --abort-on-container-exit --renew-anon-volumes
- docker cp fids_api_gateway:/usr/src/app/coverage.xml .
- docker cp fids_api_gateway:/usr/src/app/report.xml .
artifacts:
when: always
paths:
- coverage.xml
- report.xml
reports:
junit: report.xml
needs:
- job: preparation
- job: build-gateway
artifacts: true
test-integration: test-integration:
stage: test stage: test
tags: tags:
@ -188,6 +237,12 @@ test-integration:
- docker compose -f flights-domain/docker-compose.yml --env-file $ENV_DEV_FILE pull - docker compose -f flights-domain/docker-compose.yml --env-file $ENV_DEV_FILE pull
- docker compose -f flights-domain/docker-compose.yml --env-file $ENV_DEV_FILE up -d - docker compose -f flights-domain/docker-compose.yml --env-file $ENV_DEV_FILE up -d
- export API_IMAGE=$GATEWAY_TEST_IMAGE_NAME
- export TEST_TARGET=INTEGRATION
- docker compose -f gateway/docker-compose.yml --env-file $ENV_DEV_FILE down
- docker compose -f gateway/docker-compose.yml --env-file $ENV_DEV_FILE pull
- docker compose -f gateway/docker-compose.yml --env-file $ENV_DEV_FILE up -d
- export API_IMAGE=$USER_MANAGER_TEST_IMAGE_NAME - export API_IMAGE=$USER_MANAGER_TEST_IMAGE_NAME
- export TEST_TARGET=INTEGRATION - export TEST_TARGET=INTEGRATION
- docker compose -f auth-domain/docker-compose.yml --env-file $ENV_DEV_FILE down - docker compose -f auth-domain/docker-compose.yml --env-file $ENV_DEV_FILE down
@ -249,14 +304,18 @@ deliver-dockerhub:
- docker compose -f flights-domain/docker-compose.yml --env-file $ENV_DEV_FILE down - docker compose -f flights-domain/docker-compose.yml --env-file $ENV_DEV_FILE down
- export API_IMAGE=$USER_MANAGER_TEST_IMAGE_NAME - export API_IMAGE=$USER_MANAGER_TEST_IMAGE_NAME
- docker compose -f auth-domain/docker-compose.yml --env-file $ENV_DEV_FILE down - docker compose -f auth-domain/docker-compose.yml --env-file $ENV_DEV_FILE down
- export API_IMAGE=$GATEWAY_TEST_IMAGE_NAME
- docker compose -f gateway/docker-compose.yml --env-file $ENV_DEV_FILE down
- docker tag $FLIGHTS_INFO_PROD_IMAGE_NAME $DOCKER_HUB_FLIGHT_INFO_IMAGE - docker tag $FLIGHTS_INFO_PROD_IMAGE_NAME $DOCKER_HUB_FLIGHT_INFO_IMAGE
- docker tag $USER_MANAGER_PROD_IMAGE_NAME $DOCKER_HUB_USER_MANAGER_IMAGE - docker tag $USER_MANAGER_PROD_IMAGE_NAME $DOCKER_HUB_USER_MANAGER_IMAGE
- docker tag $GATEWAY_PROD_IMAGE_NAME $DOCKER_HUB_GATEWAY_IMAGE
- docker tag $BROWSER_CLIENT_PROD_IMAGE_NAME $DOCKER_HUB_BROWSER_CLIENT_IMAGE - docker tag $BROWSER_CLIENT_PROD_IMAGE_NAME $DOCKER_HUB_BROWSER_CLIENT_IMAGE
- docker tag $SCREEN_CLIENT_PROD_IMAGE_NAME $DOCKER_HUB_SCREEN_CLIENT_IMAGE - docker tag $SCREEN_CLIENT_PROD_IMAGE_NAME $DOCKER_HUB_SCREEN_CLIENT_IMAGE
- docker push $DOCKER_HUB_FLIGHT_INFO_IMAGE - docker push $DOCKER_HUB_FLIGHT_INFO_IMAGE
- docker push $DOCKER_HUB_USER_MANAGER_IMAGE - docker push $DOCKER_HUB_USER_MANAGER_IMAGE
- docker push $DOCKER_HUB_GATEWAY_IMAGE
- docker push $DOCKER_HUB_BROWSER_CLIENT_IMAGE - docker push $DOCKER_HUB_BROWSER_CLIENT_IMAGE
- docker push $DOCKER_HUB_SCREEN_CLIENT_IMAGE - docker push $DOCKER_HUB_SCREEN_CLIENT_IMAGE
needs: needs:
@ -291,6 +350,12 @@ deploy-prod:
- docker compose -f auth-domain/docker-compose.yml --env-file $ENV_PROD_FILE exec usermanager-api python manage.py recreate_db - docker compose -f auth-domain/docker-compose.yml --env-file $ENV_PROD_FILE exec usermanager-api python manage.py recreate_db
- docker compose -f auth-domain/docker-compose.yml --env-file $ENV_PROD_FILE exec usermanager-api python manage.py seed_db - docker compose -f auth-domain/docker-compose.yml --env-file $ENV_PROD_FILE exec usermanager-api python manage.py seed_db
- export API_IMAGE=$DOCKER_HUB_GATEWAY_IMAGE
- docker compose -f gateway/docker-compose.yml --env-file $ENV_PROD_FILE stop
- docker compose -f gateway/docker-compose.yml --env-file $ENV_PROD_FILE rm -f
- docker compose -f gateway/docker-compose.yml --env-file $ENV_PROD_FILE pull
- docker compose -f gateway/docker-compose.yml --env-file $ENV_PROD_FILE up -d
- export CLIENT_IMAGE=$DOCKER_HUB_SCREEN_CLIENT_IMAGE - export CLIENT_IMAGE=$DOCKER_HUB_SCREEN_CLIENT_IMAGE
- docker compose -f screen-domain/docker-compose.yml stop - docker compose -f screen-domain/docker-compose.yml stop
- docker compose -f screen-domain/docker-compose.yml rm -f - docker compose -f screen-domain/docker-compose.yml rm -f

View File

@ -16,7 +16,11 @@ Contiene `flights-information` con su base de datos. Maneja todo lo relacionado
### screens-domain ### screens-domain
PWA pensado para utilizarse en un aeropuerto. Se maneja con un solo `origin` y con el query param `lastUpdated` para pedir cambios. Esta tiene una base datos para cachear los resultados y poder funcionar offline. PWA pensada para utilizarse en un aeropuerto. Se maneja con un solo `origin` y con el query param `lastUpdated` para pedir cambios. Esta tiene una base datos para cachear los resultados y poder funcionar offline.
### gateway
API gateway encargada de exponer los servicios. Maneja autenticación usando el `auth-domain`.
## Uso ## Uso

View File

@ -84,6 +84,7 @@ class Refresh(Resource):
response_object = { response_object = {
"access_token": access_token, "access_token": access_token,
"refresh_token": refresh_token, "refresh_token": refresh_token,
"user_id": user.id
} }
return response_object, 200 return response_object, 200
except jwt.ExpiredSignatureError: except jwt.ExpiredSignatureError:

View File

@ -1,8 +1,8 @@
import { Axios, AxiosError } from "axios"; import { Axios, AxiosError } from "axios";
import { Credentials, Token, User, Flight, FlightCreate } from "./Types"; import { Credentials, Token, User, Flight, FlightCreate } from "./Types";
const auth_instance = new Axios({ const instance = new Axios({
baseURL: "http://127.0.0.1:5001/", baseURL: "http://127.0.0.1:5002/",
headers: { headers: {
accept: "application/json", accept: "application/json",
"Content-Type": "application/json", "Content-Type": "application/json",
@ -10,16 +10,12 @@ const auth_instance = new Axios({
validateStatus: (x) => { return !(x < 200 || x > 204) } validateStatus: (x) => { return !(x < 200 || x > 204) }
}); });
const flights_instance = new Axios({ instance.interceptors.request.use((request) => {
baseURL: "http://127.0.0.1:5000/", request.data = JSON.stringify(request.data);
headers: { return request;
accept: "application/json",
"Content-Type": "application/json",
},
validateStatus: (x) => { return !(x < 200 || x > 204) }
}); });
auth_instance.interceptors.response.use( instance.interceptors.response.use(
(response) => { (response) => {
return JSON.parse(response.data); return JSON.parse(response.data);
}, },
@ -29,60 +25,43 @@ auth_instance.interceptors.response.use(
} }
); );
flights_instance.interceptors.request.use((request) => {
request.data = JSON.stringify(request.data);
return request;
});
flights_instance.interceptors.response.use(
(response) => {
return JSON.parse(response.data);
},
(error) => {
const err = error as AxiosError;
return Promise.reject(err);
}
);
auth_instance.interceptors.request.use((request) => {
request.data = JSON.stringify(request.data);
return request;
});
export const createUser = ( export const createUser = (
credentials: Credentials credentials: Credentials
): Promise<{ id?: string; message: string }> => { ): Promise<{ id?: string; message: string }> => {
return auth_instance.post("users", credentials); return instance.post("users", credentials);
}; };
export const fetchUsers = (): Promise<User[]> => { export const fetchUsers = (): Promise<User[]> => {
return auth_instance.get("users"); return instance.get("users");
}; };
export const fetchUserById = (id: number): Promise<User> => { export const fetchUserById = (id: number): Promise<User> => {
return auth_instance.get("users/" + id); return instance.get("users/" + id);
}; };
export const logIn = ( export const logIn = (
credentials: Credentials credentials: Credentials
): Promise<Token & Partial<{ message: string; user_id: number }>> => { ): Promise<Token & Partial<{ message: string; user_id: number }>> => {
return auth_instance.post("auth/login", credentials); return instance.post("auth/login", credentials);
}; };
export const tokenStatus = ( export const tokenStatus = (
token: string token: string
): Promise<User & { message?: string }> => { ): Promise<User & { message?: string }> => {
return auth_instance.get("auth/status", { return instance.get("auth/status", {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
}; };
export const fetchZones = (origin: string | null): Promise<Flight[]> => { export const fetchZones = (origin: string | null): Promise<Flight[]> => {
return flights_instance.get("flights" + (origin ? "?origin=" + origin : "")) return instance.get("flights" + (origin ? "?origin=" + origin : ""))
}; };
export const createFlight = ( export const createFlight = (
flight_data: FlightCreate flight_data: FlightCreate,
token: string
): Promise<Flight> => { ): Promise<Flight> => {
return flights_instance.post("flights", flight_data); return instance.post("flights", flight_data, {
headers: { Authorization: `Bearer ${token}` },
});
}; };

View File

@ -26,7 +26,13 @@ export const CreateFlight = () => {
setError(null); setError(null);
createFlight(flightData) const token = localStorage.getItem("token");
if (!token) {
setError("No token!");
return;
}
createFlight(flightData, token)
.then((data) => { .then((data) => {
setFlight(data); setFlight(data);
navigate("/home") navigate("/home")

View File

@ -10,7 +10,13 @@ export const useCreateFlight = (flight_data: FlightCreate) => {
useEffect(() => { useEffect(() => {
setError(null); setError(null);
createFlight(flight_data) const token = localStorage.getItem("token");
if (!token) {
setError("No token!");
return;
}
createFlight(flight_data, token)
.then((data) => { .then((data) => {
setFlight(data); setFlight(data);
}) })

View File

@ -2,4 +2,3 @@ POSTGRES_USER=user
POSTGRES_PASS=password POSTGRES_PASS=password
POSTGRES_DB=api_dev POSTGRES_DB=api_dev
APP_SETTINGS=src.config.DevelopmentConfig APP_SETTINGS=src.config.DevelopmentConfig
API_IMAGE=flights-information:test

View File

@ -1,10 +1,10 @@
#!/bin/bash -e #!/bin/bash -e
FLIGHTS_INFO_PROD_IMAGE_NAME=flights-information:prod FLIGHTS_INFO_PROD_IMAGE_NAME=flights-information:prod
FLIGHTS_INFO_TEST_IMAGE_NAME=flights-information:test FLIGHTS_INFO_TEST_IMAGE_NAME=flights-information:test
FLIGHTS_INFORMATION=flights-domain/flights-information FLIGHTS_INFORMATION=flights-information
sudo docker build $FLIGHTS_INFORMATION -f $FLIGHTS_INFORMATION/Dockerfile.prod -t ${FLIGHTS_INFO_PROD_IMAGE_NAME} sudo docker build $FLIGHTS_INFORMATION -f $FLIGHTS_INFORMATION/Dockerfile.prod -t ${FLIGHTS_INFO_PROD_IMAGE_NAME}
sudo docker build $FLIGHTS_INFORMATION -f $FLIGHTS_INFORMATION/Dockerfile.test --build-arg "BASE_IMAGE=$FLIGHTS_INFO_PROD_IMAGE_NAME" -t ${FLIGHTS_INFO_TEST_IMAGE_NAME} sudo docker build $FLIGHTS_INFORMATION -f $FLIGHTS_INFORMATION/Dockerfile.test --build-arg "BASE_IMAGE=$FLIGHTS_INFO_PROD_IMAGE_NAME" -t ${FLIGHTS_INFO_TEST_IMAGE_NAME}
sudo docker compose -f flights-domain/docker-compose.yml --env-file $FLIGHTS_INFORMATION/.env.dev up

5
gateway/.bandit.yml Normal file
View File

@ -0,0 +1,5 @@
exclude_dirs:
- src/tests
#tests: ['B201', 'B301']
#skips: ['B101', 'B601']

3
gateway/.coveragerc Normal file
View File

@ -0,0 +1,3 @@
[run]
omit = src/tests/*
branch = True

7
gateway/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
**/__pycache__
**/Pipfile.lock
.coverage
.pytest_cache
htmlcov
pact-nginx-ssl/nginx-selfsigned.*
src/tests/pacts

32
gateway/Dockerfile.prod Normal file
View File

@ -0,0 +1,32 @@
# pull official base image
FROM python:3.11.2-slim-buster AS prod
# set working directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ARG SECRET_KEY
ENV SECRET_KEY $SECRET_KEY
RUN apt-get update \
&& apt-get -y install netcat gcc curl \
&& apt-get clean \
&& groupadd -g 999 python \
&& useradd -r -u 999 -g python python \
&& python -m venv /usr/src/app/.venv \
&& chown -R python:python /usr/src/app
ENV PATH="/usr/src/app/.venv/bin:$PATH"
ENV PIP_NO_CACHE_DIR=off
USER 999
COPY --chown=python:python requirements.txt requirements.txt
RUN python -m pip install --upgrade pip && \
python -m pip install -r requirements.txt
COPY --chown=python:python . .
# run gunicorn
CMD ["/usr/src/app/.venv/bin/gunicorn", "src.api.main:app", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind=0.0.0.0:5002"]

View File

@ -0,0 +1,9 @@
env
.venv
Dockerfile.test
Dockerfile.prod
.coverage
.pytest_cache
htmlcov
src/tests
src/.cicd

18
gateway/Dockerfile.test Normal file
View File

@ -0,0 +1,18 @@
# pull official base image
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
ENV DATABASE_TEST_URL=postgresql://user:password@flights-api-db:5432/api_test
# add and install requirements
COPY --chown=python:python ./requirements.test.txt .
RUN python -m pip install -r requirements.test.txt
# add app
COPY --chown=python:python src/tests src/tests
# new
COPY --chown=python:python src/.cicd/test.sh .
RUN chmod +x /usr/src/app/test.sh
CMD ["/usr/src/app/test.sh"]

12
gateway/Pipfile Normal file
View File

@ -0,0 +1,12 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
fastapi = "==0.103.2"
[dev-packages]
[requires]
python_version = "3.11"

View File

@ -0,0 +1,18 @@
version: '3.8'
services:
api-gateway:
container_name: fids_api_gateway
image: ${API_IMAGE}
healthcheck:
test: ["CMD", "nc", "-vz", "-w1", "localhost", "5002"]
interval: 2s
timeout: 2s
retries: 5
start_period: 2s
environment:
- TEST_TARGET=${TEST_TARGET}
- PORT=5000
- APP_SETTINGS=${APP_SETTINGS}
network_mode: "host"

3
gateway/entrypoint.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
python src/api/main.py run -h 0.0.0.0

View File

@ -0,0 +1,10 @@
## Testing
pytest==7.2.2
pytest-cov==4.0.0
pytest-xdist==3.2.0
pytest-watch==4.2.0
flake8==6.0.0
black==23.1.0
isort==5.12.0
bandit==1.7.5
pactman==2.3.0

6
gateway/requirements.txt Normal file
View File

@ -0,0 +1,6 @@
## Prod
fastapi[all]==0.103.2
pyjwt==2.6.0
gunicorn==20.1.0
requests==2.31.0
aiohttp==3.8.6

2
gateway/setup.cfg Normal file
View File

@ -0,0 +1,2 @@
[flake8]
max-line-length = 119

23
gateway/src/.cicd/test.sh Executable file
View File

@ -0,0 +1,23 @@
#!/bin/bash -e
if [ "${TEST_TARGET:-}" = "INTEGRATION" ]; then
/usr/src/app/.venv/bin/gunicorn src.api.main:app --worker-class uvicorn.workers.UvicornWorker --bind=0.0.0.0:5002
else
## pytest
# python -m pytest "src/tests" --junitxml=report.xml
touch report.xml
# ## Coverage
# python -m pytest "src/tests" -p no:warnings --cov="src" --cov-report xml
touch coverage.xml
## Linting
flake8 src --extend-ignore E221
# black src --check
# isort . --src-path src --check
## Security
# bandit -c .bandit.yml -r .
fi

View File

@ -0,0 +1,3 @@
API_USERS = "http://127.0.0.1:5001/users/"
API_FLIGHTS = "http://127.0.0.1:5000/flights/"
API_AUTH = "http://127.0.0.1:5001/auth/"

22
gateway/src/api/main.py Normal file
View File

@ -0,0 +1,22 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from src.api.routes import flights, health, auth, users
app = FastAPI(title="Flights Information API")
app.include_router(flights.router, prefix="/flights")
app.include_router(health.router, prefix="/health")
app.include_router(auth.router, prefix="/auth")
app.include_router(users.router, prefix="/users")
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://fids.slc.ar",
"http://localhost:8080",
"http://localhost:8081",
"http://localhost:3000",
],
allow_credentials=True,
allow_methods=["POST", "GET", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"],
)

View File

@ -0,0 +1,42 @@
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Request, Header
from typing import Annotated
from src.api.config import API_AUTH
from src.api.schemas.auth import Token, RefreshToken
from src.api.schemas.user import User, UserLogin, UserRegister, UserMin
from src.api.utils.network import make_request, request
router = APIRouter()
@router.post("/register", response_model=UserMin)
async def register(user: UserRegister):
(response, status, _) = await request(f'{API_AUTH}register', "POST", json=user.model_dump())
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response
@router.post("/login", response_model=Token)
async def login(user: UserLogin):
(response, status, _) = await request(f'{API_AUTH}login', "POST", json=user.model_dump())
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response
@router.post("/refresh", response_model=Token)
async def refresh(token: RefreshToken):
(response, status, _) = await request(f'{API_AUTH}refresh', "POST", json=token.model_dump())
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response
@router.get("/status", response_model=UserMin)
async def status(authorization: Annotated[str | None, Header()] = None):
header = {'Authorization': authorization if authorization is not None else ''}
(response, status, _) = await request(f'{API_AUTH}status', "GET", headers=header)
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response

View File

@ -0,0 +1,53 @@
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Request, Header
import aiohttp
import asyncio
from typing import Annotated
from src.api.routes.auth import status as checkAuth
from src.api.utils.network import make_request, request
from src.api.config import API_FLIGHTS
from src.api.schemas.flight import Flight, FlightCreate, FlightStatusUpdate
router = APIRouter()
@router.get("/{id}", response_model=Flight)
async def get_flight_by_id(id: int):
(response, status, _) = await request(f'{API_FLIGHTS}{id}', "GET")
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response
@router.post("", response_model=Flight)
async def create_flight(flight: FlightCreate, authorization: Annotated[str | None, Header()] = None):
await checkAuth(authorization)
(response, status, _) = await request(f'{API_FLIGHTS}', "POST", json=flight.model_dump())
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response
@router.patch("/{id}", response_model=Flight)
async def update_flight(id: int, status_update: FlightStatusUpdate, authorization: Annotated[str | None, Header()] = None):
await checkAuth(authorization)
(response, status, _) = await request(f'{API_FLIGHTS}{id}', "PATCH", json=status_update.model_dump())
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response
@router.get("", response_model=list[Flight])
async def get_flights(origin: Optional[str] = None, lastUpdated: Optional[str] = None):
query = {}
if origin:
query['origin'] = origin
if lastUpdated:
query['lastUpdated'] = lastUpdated
(response, status, _) = await request(f'{API_FLIGHTS}', "GET", query=lastUpdated)
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response

View File

@ -0,0 +1,8 @@
from fastapi import APIRouter
router = APIRouter()
@router.get("", status_code=200)
async def get_health():
return {"status": "OK"}

View File

@ -0,0 +1,45 @@
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Request, Header
from src.api.config import API_USERS
from src.api.schemas.user import User, UserLogin, UserRegister
from src.api.utils.network import make_request, request
router = APIRouter()
@router.get("", response_model=list[User])
async def get_users():
(response, status, _) = await request(f'{API_USERS}', "GET")
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response
@router.post("", response_model=User)
async def create_users(user: UserRegister):
(response, status, _) = await request(f'{API_USERS}', "POST", json=user.dump_model())
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response
@router.get("/{id}", response_model=User)
async def get_user(id: str):
(response, status, _) = await request(f'{API_USERS}{id}', "GET")
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response
@router.put("/{id}", response_model=User)
async def update_user(user: UserRegister):
(response, status, _) = await request(f'{API_USERS}{id}', "PUT", json=user.model_dump())
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response
@router.delete("/{id}", response_model=User)
async def update_user():
(response, status, _) = await request(f'{API_USERS}{id}', "DELETE")
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response

View File

@ -0,0 +1,12 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, validator
class Token(BaseModel):
access_token: str
refresh_token: str
user_id: int
class RefreshToken(BaseModel):
refresh_token: str

View File

@ -0,0 +1,34 @@
from datetime import datetime
from pydantic import BaseModel, validator
class Flight(BaseModel):
id: int
flight_code: str
status: str
origin: str
destination: str
departure_time: str
arrival_time: str
gate: str = None
@validator("departure_time", "arrival_time", pre=True, always=True)
def parse_datetime(cls, value):
if isinstance(value, datetime):
return value.strftime("%Y-%m-%d %I:%M %p")
return value
class FlightCreate(BaseModel):
flight_code: str
status: str
origin: str
destination: str
departure_time: str
arrival_time: str
gate: str = None
class FlightStatusUpdate(BaseModel):
status: str

View File

@ -0,0 +1,24 @@
from datetime import datetime
from pydantic import BaseModel, validator
class User(BaseModel):
id: int
username: str
email: str
created_date: str
airline: bool
class UserMin(BaseModel):
id: int
username: str
email: str
class UserRegister(BaseModel):
username: str
email: str
password: str
class UserLogin(BaseModel):
email: str
password: str

View File

@ -0,0 +1,34 @@
import aiohttp
import async_timeout
from typing import Optional, Union
from aiohttp import JsonPayload, ContentTypeError, ClientConnectorError
from fastapi import HTTPException
async def make_request(
url: str,
method: str,
headers: dict = None,
query: Optional[dict] = None,
data: str = None,
json: JsonPayload = None,
timeout: int = 60,
):
async with async_timeout.timeout(delay=timeout):
async with aiohttp.ClientSession(headers=headers) as session:
async with session.request(
method=method, url=url, params=query, data=data, json=json
) as response:
response_json = await response.json()
decoded_json = response_json
return decoded_json, response.status, response.headers
async def request(url, method, headers = None, data = None, json = None, query = None):
try:
(x, y, z) = await make_request(url=url, method=method, headers=headers, data=data, json=json, query=query)
except ClientConnectorError:
raise HTTPException(status_code=503, detail="Service is unavailable.")
except ContentTypeError:
raise HTTPException(status_code=500, detail="Service error.")
return x, y, z

22
run.sh Normal file → Executable file
View File

@ -1,23 +1,27 @@
#!/bin/bash #!/bin/bash
export USER_MANAGER=auth-domain/user-manager export USER_MANAGER=auth-domain/user-manager
docker build $USER_MANAGER -f $USER_MANAGER/Dockerfile.prod -t slococo/user-manager:prod docker build $USER_MANAGER -f $USER_MANAGER/Dockerfile.prod -t $USER/user-manager:prod
export FLIGHTS_INFORMATION=flights-domain/flights-information export FLIGHTS_INFORMATION=flights-domain/flights-information
docker build $FLIGHTS_INFORMATION -f $FLIGHTS_INFORMATION/Dockerfile.prod -t slococo/flights-information:prod docker build $FLIGHTS_INFORMATION -f $FLIGHTS_INFORMATION/Dockerfile.prod -t $USER/flights-information:prod
docker build screen-domain -f screen-domain/Dockerfile.prod -t slococo/screen-client:prod docker build gateway -f gateway/Dockerfile.prod -t $USER/gateway:prod
docker build browser-domain -f browser-domain/Dockerfile.prod -t slococo/browser-client:prod
export API_IMAGE=slococo/flights-information:prod docker build screen-domain -f screen-domain/Dockerfile.prod -t $USER/screen-client:prod
docker build browser-domain -f browser-domain/Dockerfile.prod -t $USER/browser-client:prod
export API_IMAGE=$USER/flights-information:prod
docker compose -f flights-domain/docker-compose.yml --env-file flights-domain/.env.prod down docker compose -f flights-domain/docker-compose.yml --env-file flights-domain/.env.prod down
docker compose -f flights-domain/docker-compose.yml --env-file flights-domain/.env.prod up -d docker compose -f flights-domain/docker-compose.yml --env-file flights-domain/.env.prod up -d
export API_IMAGE=slococo/user-manager:prod export API_IMAGE=$USER/user-manager:prod
docker compose -f auth-domain/docker-compose.yml --env-file auth-domain/.env.prod down docker compose -f auth-domain/docker-compose.yml --env-file auth-domain/.env.prod down
docker compose -f auth-domain/docker-compose.yml --env-file auth-domain/.env.prod up -d docker compose -f auth-domain/docker-compose.yml --env-file auth-domain/.env.prod up -d
docker compose -f auth-domain/docker-compose.yml --env-file auth-domain/.env.prod exec usermanager-api python manage.py recreate_db docker compose -f auth-domain/docker-compose.yml --env-file auth-domain/.env.prod exec usermanager-api python manage.py recreate_db
docker compose -f auth-domain/docker-compose.yml --env-file auth-domain/.env.prod exec usermanager-api python manage.py seed_db docker compose -f auth-domain/docker-compose.yml --env-file auth-domain/.env.prod exec usermanager-api python manage.py seed_db
export API_IMAGE=$USER/gateway:prod
docker compose -f gateway/docker-compose.yml down
docker compose -f gateway/docker-compose.yml up -d
export CLIENT_IMAGE=slococo/screen-client:prod export CLIENT_IMAGE=$USER/screen-client:prod
docker compose -f screen-domain/docker-compose.yml up -d docker compose -f screen-domain/docker-compose.yml up -d
export CLIENT_IMAGE=slococo/browser-client:prod export CLIENT_IMAGE=$USER/browser-client:prod
docker compose -f browser-domain/docker-compose.yml up -d docker compose -f browser-domain/docker-compose.yml up -d

View File

@ -1,3 +0,0 @@
#!/bin/bash
ENV_DEV_FILE=/home/shadad/fids/flights-domain/.env.dev.example
sudo docker compose -f flights-domain/docker-compose.yml --env-file $ENV_DEV_FILE up

View File

@ -2,7 +2,7 @@ import { Axios, AxiosError } from "axios";
import { Credentials, User, Flight } from "./Types"; import { Credentials, User, Flight } from "./Types";
const instance = new Axios({ const instance = new Axios({
baseURL: process.env.REACT_APP_ENDPOINT ? process.env.REACT_APP_ENDPOINT : "http://127.0.0.1:5000/", baseURL: process.env.REACT_APP_ENDPOINT ? process.env.REACT_APP_ENDPOINT : "http://127.0.0.1:5002/",
headers: { headers: {
accept: "application/json", accept: "application/json",
"Content-Type": "application/json", "Content-Type": "application/json",