From 5e548162eefb64390671e02241ec2dc82d91cf1d Mon Sep 17 00:00:00 2001 From: bsquillari Date: Mon, 4 Dec 2023 12:29:47 +0000 Subject: [PATCH 1/8] Removed unused code --- auth-domain/user-manager/src/api/auth.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/auth-domain/user-manager/src/api/auth.py b/auth-domain/user-manager/src/api/auth.py index 594479c..a33575a 100644 --- a/auth-domain/user-manager/src/api/auth.py +++ b/auth-domain/user-manager/src/api/auth.py @@ -3,7 +3,7 @@ from flask import request from flask_restx import Namespace, Resource from src import bcrypt -from src.api.cruds.users import add_user, get_user_by_email, get_user_by_id +from src.api.cruds.users import get_user_by_email, get_user_by_id from src.api.models.users import User auth_namespace = Namespace("auth") @@ -19,25 +19,6 @@ parser = auth_namespace.parser() parser.add_argument("Authorization", location="headers") -class Register(Resource): - @auth_namespace.marshal_with(auth_user_model) - @auth_namespace.expect(auth_full_user_model, validate=True) - @auth_namespace.response(201, "Success") - @auth_namespace.response(400, "Sorry. That email already exists.") - def post(self): - post_data = request.get_json() - username = post_data.get("username") - email = post_data.get("email") - password = post_data.get("password") - - user = get_user_by_email(email) - if user: - auth_namespace.abort(400, "Sorry. That email already exists.") - user = add_user(username, email, password) - - return user, 201 - - class Login(Resource): @auth_namespace.marshal_with(auth_tokens_model) @auth_namespace.expect(auth_login_model, validate=True) @@ -124,7 +105,6 @@ class Status(Resource): auth_namespace.abort(403, "Token required") -auth_namespace.add_resource(Register, "/register") auth_namespace.add_resource(Login, "/login") auth_namespace.add_resource(Refresh, "/refresh") auth_namespace.add_resource(Status, "/status") From a35a97bfaa82fe5340eb9cb6a46df188affeaf02 Mon Sep 17 00:00:00 2001 From: Santiago Lo Coco Date: Mon, 4 Dec 2023 10:37:30 -0300 Subject: [PATCH 2/8] Add edit and delete flight in the frontend Co-authored-by: shadad00 --- .gitlab-ci.yml | 3 +- browser-domain/src/Api.ts | 20 +++- browser-domain/src/App.tsx | 2 + browser-domain/src/Types.d.ts | 15 +++ .../components/CreateFlight/EditFlight.tsx | 111 ++++++++++++++++++ .../src/components/Home/Card/Card.tsx | 97 +++++++++++---- browser-domain/src/components/Home/Home.tsx | 6 +- browser-domain/src/hooks/useFetchFlight.tsx | 25 ++++ browser-domain/src/hooks/useFetchFlights.tsx | 25 +++- gateway/src/api/main.py | 2 +- gateway/src/api/routes/flights.py | 23 +++- gateway/src/api/schemas/flight.py | 18 +++ run.sh | 4 + 13 files changed, 314 insertions(+), 37 deletions(-) create mode 100644 browser-domain/src/components/CreateFlight/EditFlight.tsx create mode 100644 browser-domain/src/hooks/useFetchFlight.tsx diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d5a3fcf..52552bb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -329,6 +329,7 @@ test-screen-client: - docker compose -f subscription-domain/docker-compose.dev.yml --env-file $ENV_DEV_FILE down - docker compose -f subscription-domain/docker-compose.dev.yml --env-file $ENV_DEV_FILE pull - docker compose -f subscription-domain/docker-compose.dev.yml --env-file $ENV_DEV_FILE up -d + - export API_IMAGE=$CLIENT_IMAGE - docker compose -f ${FOLDER}/docker-compose.dev.yml --env-file $ENV_DEV_FILE down - docker compose -f ${FOLDER}/docker-compose.dev.yml --env-file $ENV_DEV_FILE pull - docker compose -f ${FOLDER}/docker-compose.dev.yml --env-file $ENV_DEV_FILE up --abort-on-container-exit @@ -411,7 +412,7 @@ test-e2e-interface: - *changes-frontend - *changes-backend script: - - export API_IMAGE=${E2E_TEST_IMAGE_NAME} + - export CLIENT_IMAGE=${E2E_TEST_IMAGE_NAME} - export FOLDER=testing/catcher - *test-integration after_script: diff --git a/browser-domain/src/Api.ts b/browser-domain/src/Api.ts index a490e88..9c055f7 100644 --- a/browser-domain/src/Api.ts +++ b/browser-domain/src/Api.ts @@ -1,5 +1,5 @@ import { Axios, AxiosError } from "axios"; -import { Credentials, Token, User, Flight, FlightCreate, SubscriptionsCreate } from "./Types"; +import { Credentials, Token, User, Flight, FlightCreate, SubscriptionsCreate, FlightEdit } from "./Types"; const instance = new Axios({ baseURL: process.env.REACT_APP_ENDPOINT ? process.env.REACT_APP_ENDPOINT : "http://127.0.0.1:5000/", @@ -79,6 +79,24 @@ export const createFlight = ( }); }; + +export const editFlight = ( + flight_id:string, + fligth_data: FlightEdit, + token: string +):Promise => { + return instance.patch("flights/" + flight_id , fligth_data, { + headers: { Authorization: `Bearer ${token}` }, + }); +}; + +export const fetchFlight = ( + flight_id:string, +):Promise => { + return instance.get("flights/" + flight_id); +}; + + export const subscribeToFlight = (subscription: SubscriptionsCreate, token: string): Promise => { return instance.post("subscriptions", subscription, { headers: { Authorization: `Bearer ${token}` }, diff --git a/browser-domain/src/App.tsx b/browser-domain/src/App.tsx index 84d009e..20da666 100644 --- a/browser-domain/src/App.tsx +++ b/browser-domain/src/App.tsx @@ -5,6 +5,7 @@ import { Home } from "./components/Home/Home"; import { CreateFlight } from "./components/CreateFlight/CreateFlight"; import { Button } from "antd"; import useAuth, { AuthProvider } from "./useAuth"; +import { EditFlight } from "./components/CreateFlight/EditFlight"; function Router() { const { user, logout, isAirline } = useAuth(); @@ -16,6 +17,7 @@ function Router() { } /> : } /> : } /> + : } /> : } />
diff --git a/browser-domain/src/Types.d.ts b/browser-domain/src/Types.d.ts index 81b8ce7..09b9c2a 100644 --- a/browser-domain/src/Types.d.ts +++ b/browser-domain/src/Types.d.ts @@ -35,6 +35,7 @@ export interface Flight { departure_time: string; arrival_time: string; gate: string; + user_id: number; } export interface FlightCreate { @@ -47,6 +48,20 @@ export interface FlightCreate { gate: string; } +export interface FlightEditNotNull { + departure_time: string, + arrival_time: string, + status: string, + gate: string +} + +export interface FlightEdit { + departure_time: string?, + arrival_time: string?, + status: string?, + gate: string? +} + export interface SubscriptionsCreate { flight_id: number; user_id: number; diff --git a/browser-domain/src/components/CreateFlight/EditFlight.tsx b/browser-domain/src/components/CreateFlight/EditFlight.tsx new file mode 100644 index 0000000..ccb8505 --- /dev/null +++ b/browser-domain/src/components/CreateFlight/EditFlight.tsx @@ -0,0 +1,111 @@ +import React, { useEffect, useState } from "react"; +import { FlightEditNotNull, Flight, FlightEdit } from "../../Types"; +import { useNavigate, useParams } from "react-router"; +import "./FlightForm.css"; +import { createFlight, editFlight } from "../../Api"; +import { useFetchFlight } from "../../hooks/useFetchFlight"; + +interface Props { + flight?: Flight; +} + +export const EditFlight: React.FC = (props) => { + const navigate = useNavigate(); + let { id } = useParams(); + const [error, setError] = useState(null); + const [flight, setFlight] = useState(); + + const { flight: initialData } = useFetchFlight(id); + + const [flightData, setFlightData] = useState({ + status: "", + departure_time: "", + arrival_time: "", + gate: "" + }); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + setError(null); + + const token = localStorage.getItem("token"); + if (!token) { + setError("No token!"); + return; + } + + let data: any = {} + if (flightData.arrival_time != "") { + data["arrival_time"] = flightData.arrival_time + } + if (flightData.departure_time != ""){ + data["departure_time"] = flightData.departure_time + } + + if (flightData.status != ""){ + data["status"] = flightData.status + } + if (flightData.gate != ""){ + data["gate"] = flightData.gate + } + + + if (id == null || id == undefined) + return; + + editFlight(id, data, token) + .then((data) => { + setFlight(data); + navigate("/home") + }) + .catch((error) => { + setError(error as string); + }); + }; + + return ( +
+ + + + + + +
+ ); +}; diff --git a/browser-domain/src/components/Home/Card/Card.tsx b/browser-domain/src/components/Home/Card/Card.tsx index 6900065..696712a 100644 --- a/browser-domain/src/components/Home/Card/Card.tsx +++ b/browser-domain/src/components/Home/Card/Card.tsx @@ -1,36 +1,39 @@ import React, { useEffect, useState } from "react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { Avatar, Space, Typography, Tag, Button, Modal } from "antd"; import { RightOutlined, ClockCircleOutlined, SwapOutlined, EnvironmentOutlined, CalendarOutlined } from "@ant-design/icons"; import "./Card.css"; -import { getChatId, getSubscription, subscribeToFlight, unsubscribeFromFlight } from "../../../Api"; -import { User } from "../../../Types"; +import { getChatId, getSubscription, subscribeToFlight, unsubscribeFromFlight, editFlight } from "../../../Api"; +import { Flight, FlightEdit, User } from "../../../Types"; -interface FlightProps { - id: number; - flight_code: string; - status: string; - origin: string; - destination: string; - departure_time: string; - arrival_time: string; - gate: string; -} +// interface FlightProps { +// id: number; +// flight_code: string; +// status: string; +// origin: string; +// destination: string; +// departure_time: string; +// arrival_time: string; +// gate: string; +// } interface CardProps { - flight: FlightProps; + flight: Flight; user: User | undefined; subscribed: boolean; refresh: any; + isAirline: boolean; + refreshFlights: any; } const { Text } = Typography; -export const Card: React.FC = ({ flight, user, subscribed, refresh }) => { +export const Card: React.FC = ({ flight, user, subscribed, refresh, refreshFlights, isAirline }) => { const [modalVisible, setModalVisible] = useState(false); - + const navigate = useNavigate(); + const handleSubscribe = async (event: React.FormEvent) => { event.preventDefault(); @@ -59,6 +62,33 @@ export const Card: React.FC = ({ flight, user, subscribed, refresh }) }); }; + const handleEdit = async (event: React.FormEvent) => { + event.preventDefault(); + navigate(`/edit-flight/${flight.id}`); + }; + + const handleDelete = async (event: React.FormEvent) => { + event.preventDefault(); + + const token = localStorage.getItem("token"); + if (!token || !user) { + return; + } + + let data: any = { + status: "Deleted" + } + + editFlight("" + flight.id, data, token) + .then(() => { + console.log("culicagado") + refreshFlights() + }) + .catch((error) => { + console.log(error) + }); + }; + const handleModalClose = () => { setModalVisible(false); }; @@ -83,10 +113,12 @@ export const Card: React.FC = ({ flight, user, subscribed, refresh }) refresh() }) .catch((error) => { + console.log(error) }); }; - console.log(subscribed) + console.log(flight.user_id) + console.log(user?.id) return (
@@ -126,14 +158,31 @@ export const Card: React.FC = ({ flight, user, subscribed, refresh }) {flight.id}
- {!(subscribed) ? - - : - + + : + + ) + : + ( + user && flight.user_id == user.id ? + <> + + + + : + <> + ) } = (props) => { const urlParams = new URLSearchParams(window.location.search); const origin = urlParams.get('origin'); const initialPage = parseInt(urlParams.get('page') || '1', 10); - const { flights, count, error } = useFetchFlights(origin, initialPage); + const { flights, count, error, fetchData: refreshFlights } = useFetchFlights(origin, initialPage); const navigate = useNavigate() const [currentPage, setCurrentPage] = useState(initialPage); @@ -56,7 +56,9 @@ export const Home: React.FC = (props) => {

Flights

{(props.flights ? props.flights : flights).map((f) => { - return i.flight_id === f.id))} refresh={fetchData} />; + return i.flight_id === f.id))} + refresh={fetchData} refreshFlights={refreshFlights} isAirline={isAirline} />; })} {error ?
{error}
: <>}
diff --git a/browser-domain/src/hooks/useFetchFlight.tsx b/browser-domain/src/hooks/useFetchFlight.tsx new file mode 100644 index 0000000..4fec754 --- /dev/null +++ b/browser-domain/src/hooks/useFetchFlight.tsx @@ -0,0 +1,25 @@ +import { useEffect } from "react"; +import { useState } from "react"; +import { Flight } from "../Types"; +import { fetchFlight } from "../Api"; + +export const useFetchFlight = (id: string | undefined) => { + const [error, setError] = useState(null); + const [flight, setFlight] = useState(); + const [count, setCount] = useState(0); + + useEffect(() => { + setError(null); + + if (id == null || id == undefined) + return; + + fetchFlight(id) + .then((data) => { + setFlight(data); + }) + .catch((error) => { }); + }, [id]); + + return { flight, count, error }; +}; diff --git a/browser-domain/src/hooks/useFetchFlights.tsx b/browser-domain/src/hooks/useFetchFlights.tsx index d72f228..a5a5ee4 100644 --- a/browser-domain/src/hooks/useFetchFlights.tsx +++ b/browser-domain/src/hooks/useFetchFlights.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useCallback, useEffect } from "react"; import { useState } from "react"; import { User, Flight } from "../Types"; import { fetchFlights } from "../Api"; @@ -8,16 +8,31 @@ export const useFetchFlights = (origin: string | null, page: number | null) => const [flights, setFlights] = useState([]); const [count, setCount] = useState(0); - useEffect(() => { + // useEffect(() => { + // setError(null); + + // fetchFlights(origin, page) + // .then((data) => { + // setCount(data.count) + // setFlights(data.flights.filter((e) => e.status != "Deleted" )); + // }) + // .catch((error) => { }); + // }, [page]); + + const fetchData = useCallback(async () => { setError(null); fetchFlights(origin, page) .then((data) => { setCount(data.count) - setFlights(data.flights); + setFlights(data.flights.filter((e) => e.status != "Deleted" )); }) .catch((error) => { }); - }, [page]); + }, [origin, page]); - return { flights, count, error }; + useEffect(() => { + fetchData() + }, [fetchData]); + + return { flights, count, error, fetchData }; }; diff --git a/gateway/src/api/main.py b/gateway/src/api/main.py index 771e61c..bc08dfe 100644 --- a/gateway/src/api/main.py +++ b/gateway/src/api/main.py @@ -28,7 +28,7 @@ app.add_middleware( "http://localhost:3000", ], allow_credentials=True, - allow_methods=["POST", "GET", "PUT", "DELETE", "OPTIONS"], + allow_methods=["POST", "GET", "PUT", "DELETE", "OPTIONS", "PATCH"], allow_headers=["*"], expose_headers=["x-count"], ) diff --git a/gateway/src/api/routes/flights.py b/gateway/src/api/routes/flights.py index 2e67137..2e98758 100644 --- a/gateway/src/api/routes/flights.py +++ b/gateway/src/api/routes/flights.py @@ -5,12 +5,12 @@ from fastapi import APIRouter, Header, HTTPException, Request, Response from src.api.config import API_FLIGHTS from src.api.routes.auth import checkAuth -from src.api.schemas.flight import Flight, FlightCreate, FlightUpdate +from src.api.schemas.flight import Flight, FlightCreate, FlightFull, FlightUpdate router = APIRouter() -@router.get("/{id}", response_model=Flight) +@router.get("/{id}", response_model=FlightFull) async def get_flight_by_id( id: int, req: Request, @@ -42,6 +42,23 @@ async def create_flight( return response +# @router.delete("/{id}") +# async def delete_flight( +# id: int, +# req: Request, +# authorization: Annotated[str | None, Header()] = None, +# ): +# id = await checkAuth(req, authorization, isAirline=True) +# request_id = req.state.request_id +# header = {"x-api-request-id": request_id} +# (response, status, _) = await request( +# f"{API_FLIGHTS}/{id}", "DELETE", headers=header +# ) +# 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, @@ -62,7 +79,7 @@ async def update_flight( return response -@router.get("", response_model=list[Flight]) +@router.get("", response_model=list[FlightFull]) async def get_flights( req: Request, res: Response, diff --git a/gateway/src/api/schemas/flight.py b/gateway/src/api/schemas/flight.py index 76dcb5c..d06ccd4 100644 --- a/gateway/src/api/schemas/flight.py +++ b/gateway/src/api/schemas/flight.py @@ -21,6 +21,24 @@ class Flight(BaseModel): return value +class FlightFull(BaseModel): + id: int + flight_code: str + status: str + origin: str + destination: str + departure_time: str + arrival_time: str + gate: str = None + user_id: int + + @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 diff --git a/run.sh b/run.sh index 76f7214..483f9ad 100755 --- a/run.sh +++ b/run.sh @@ -221,12 +221,16 @@ elif [ -n "$domain" ] && [ -z "$down" ]; then elif [ -n "$down" ]; then 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.dev.yml --env-file flights-domain/.env.dev down 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.dev.yml --env-file auth-domain/.env.dev down export API_IMAGE=$USER/subs-manager:prod docker compose -f subscription-domain/docker-compose.yml --env-file subscription-domain/.env.prod down + docker compose -f subscription-domain/docker-compose.dev.yml --env-file subscription-domain/.env.dev down export API_IMAGE=$USER/gateway:prod docker compose -f gateway/docker-compose.yml --env-file gateway/.env.prod down + docker compose -f gateway/docker-compose.dev.yml --env-file gateway/.env.dev down export CLIENT_IMAGE=$USER/screen-client:prod docker compose -f screen-domain/docker-compose.yml down From 1dee061e974b8bed403413b667016b680a469771 Mon Sep 17 00:00:00 2001 From: bsquillari Date: Mon, 4 Dec 2023 14:04:08 +0000 Subject: [PATCH 3/8] Delete unused code --- .../src/tests/functional/test_auth.py | 66 +------------------ gateway/src/api/routes/auth.py | 16 +---- 2 files changed, 3 insertions(+), 79 deletions(-) diff --git a/auth-domain/user-manager/src/tests/functional/test_auth.py b/auth-domain/user-manager/src/tests/functional/test_auth.py index 844b191..2f29906 100644 --- a/auth-domain/user-manager/src/tests/functional/test_auth.py +++ b/auth-domain/user-manager/src/tests/functional/test_auth.py @@ -1,75 +1,11 @@ import json import time -import pytest - TEST_USERNAME = "fede_auth" TEST_EMAIL = "fede_auth@gmail.com" TEST_PASSWD = "password1234" -def test_user_registration(test_app, test_database): - client = test_app.test_client() - resp = client.post( - "/auth/register", - data=json.dumps( - { - "username": TEST_USERNAME, - "email": TEST_EMAIL, - "password": TEST_PASSWD, - } - ), - content_type="application/json", - ) - data = json.loads(resp.data.decode()) - assert resp.status_code == 201 - assert resp.content_type == "application/json" - assert TEST_USERNAME in data["username"] - assert TEST_EMAIL in data["email"] - assert "password" not in data - - -def test_user_registration_duplicate_email(test_app, test_database, add_user): - add_user(TEST_USERNAME, TEST_EMAIL, TEST_PASSWD) - client = test_app.test_client() - resp = client.post( - "/auth/register", - data=json.dumps( - {"username": "martin", "email": TEST_EMAIL, "password": "test"} - ), - content_type="application/json", - ) - data = json.loads(resp.data.decode()) - assert resp.status_code == 400 - assert resp.content_type == "application/json" - assert "Sorry. That email already exists." == data["message"] - - -@pytest.mark.parametrize( - "payload", - [ - {}, - {"email": TEST_EMAIL, "password": TEST_PASSWD}, - {"username": TEST_USERNAME, "password": TEST_PASSWD}, - {"email": TEST_EMAIL, "username": TEST_USERNAME}, - {"mail": TEST_EMAIL, "username": TEST_USERNAME, "password": TEST_PASSWD}, - {"email": TEST_EMAIL, "user": TEST_USERNAME, "password": TEST_PASSWD}, - {"email": TEST_EMAIL, "username": TEST_USERNAME, "passwd": TEST_PASSWD}, - ], -) -def test_user_registration_invalid_json(test_app, test_database, payload): - client = test_app.test_client() - resp = client.post( - "/auth/register", - data=json.dumps(payload), - content_type="application/json", - ) - data = json.loads(resp.data.decode()) - assert resp.status_code == 400 - assert resp.content_type == "application/json" - assert "Input payload validation failed" in data["message"] - - def test_registered_user_login(test_app, test_database, add_user): add_user(TEST_USERNAME, TEST_EMAIL, TEST_PASSWD) client = test_app.test_client() @@ -174,7 +110,7 @@ def test_user_status(test_app, test_database, add_user): data = json.loads(resp.data.decode()) assert resp.status_code == 200 assert resp.content_type == "application/json" - assert not data["airline"] + assert data["role"] == 0 assert "password" not in data diff --git a/gateway/src/api/routes/auth.py b/gateway/src/api/routes/auth.py index e9d2ec7..b404917 100644 --- a/gateway/src/api/routes/auth.py +++ b/gateway/src/api/routes/auth.py @@ -5,23 +5,11 @@ from fastapi import APIRouter, Header, HTTPException, Request from src.api.config import API_AUTH from src.api.schemas.auth import RefreshToken, Token -from src.api.schemas.user import UserLogin, UserMin, UserRegister, UserStatus +from src.api.schemas.user import UserLogin, UserStatus router = APIRouter() -@router.post("/register", response_model=UserMin) -async def register(user: UserRegister, req: Request): - request_id = req.state.request_id - header = {"x-api-request-id": request_id} - (response, status, _) = await request( - f"{API_AUTH}/register", "POST", json=user.model_dump(), headers=header - ) - 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, req: Request): request_id = req.state.request_id @@ -67,7 +55,7 @@ async def checkAuth( ): response = await status(req, authorization) if isAirline: - if response["airline"]: + if response["role"] == 1: return response["id"] else: raise HTTPException( From e3a58741968cf0c228fef1d4c29d7ec1d99b07b3 Mon Sep 17 00:00:00 2001 From: bsquillari Date: Mon, 4 Dec 2023 14:41:39 +0000 Subject: [PATCH 4/8] Add role fields (user, airlie, admin) --- auth-domain/user-manager/manage.py | 14 ++++++++++--- auth-domain/user-manager/src/api/auth.py | 4 ++-- .../user-manager/src/api/models/users.py | 21 ++++++++++++------- auth-domain/user-manager/src/api/users.py | 2 +- .../src/tests/functional/test_auth.py | 2 +- .../user-manager/src/tests/unit/test_users.py | 2 +- browser-domain/src/Types.d.ts | 2 +- browser-domain/src/useAuth.tsx | 13 +++++++----- gateway/src/api/routes/auth.py | 2 +- gateway/src/api/schemas/user.py | 4 ++-- 10 files changed, 42 insertions(+), 24 deletions(-) diff --git a/auth-domain/user-manager/manage.py b/auth-domain/user-manager/manage.py index 9f5ee0f..782c64a 100644 --- a/auth-domain/user-manager/manage.py +++ b/auth-domain/user-manager/manage.py @@ -1,7 +1,7 @@ from flask.cli import FlaskGroup from src import create_app, db -from src.api.models.users import User +from src.api.models.users import Roles, User app = create_app() cli = FlaskGroup(create_app=create_app) @@ -21,7 +21,7 @@ def seed_db(): username="lufthansa", email="info@lufthansa.com", password="password1234", - airline=True, + role=Roles.airline, ) ) db.session.add( @@ -29,7 +29,15 @@ def seed_db(): username="ryanair", email="info@ryanair.com", password="password1234", - airline=True, + role=Roles.airline, + ) + ) + db.session.add( + User( + username="admin", + email="admin", + password="password1234", + role=Roles.admin, ) ) db.session.add( diff --git a/auth-domain/user-manager/src/api/auth.py b/auth-domain/user-manager/src/api/auth.py index a33575a..017ded9 100644 --- a/auth-domain/user-manager/src/api/auth.py +++ b/auth-domain/user-manager/src/api/auth.py @@ -34,7 +34,7 @@ class Login(Resource): if not user or not bcrypt.check_password_hash(user.password, password): auth_namespace.abort(404, "User does not exist") - access_token = user.encode_token(user.id, "access", user.airline) + access_token = user.encode_token(user.id, "access", user.role) refresh_token = user.encode_token(user.id, "refresh") response_object = { @@ -62,7 +62,7 @@ class Refresh(Resource): if not user: auth_namespace.abort(401, "Invalid token") - access_token = user.encode_token(user.id, "access", user.airline) + access_token = user.encode_token(user.id, "access", user.role) refresh_token = user.encode_token(user.id, "refresh") response_object = { diff --git a/auth-domain/user-manager/src/api/models/users.py b/auth-domain/user-manager/src/api/models/users.py index 992d775..1205617 100644 --- a/auth-domain/user-manager/src/api/models/users.py +++ b/auth-domain/user-manager/src/api/models/users.py @@ -1,4 +1,5 @@ import datetime +from enum import Enum import jwt from flask import current_app @@ -8,6 +9,12 @@ from sqlalchemy.sql import func from src import bcrypt, db +class Roles(Enum): + user = "user" + airline = "airline" + admin = "admin" + + class User(db.Model): __tablename__ = "users" @@ -17,18 +24,18 @@ class User(db.Model): password = db.Column(db.String(255), nullable=False) active = db.Column(db.Boolean(), default=True, nullable=False) created_date = db.Column(db.DateTime, default=func.now(), nullable=False) - airline = db.Column(db.Boolean(), default=False, nullable=False) + role = db.Column(db.String(128), default=Roles.user.value, nullable=False) - def __init__(self, username, email, password, airline=False): + def __init__(self, username, email, password, role=Roles.user): self.username = username self.email = email self.password = bcrypt.generate_password_hash( password, current_app.config.get("BCRYPT_LOG_ROUNDS") ).decode() - self.airline = airline + self.role = role.value @staticmethod - def encode_token(user_id, token_type, airline=False): + def encode_token(user_id, token_type, role="user"): if token_type == "access": seconds = current_app.config.get("ACCESS_TOKEN_EXPIRATION") else: @@ -38,7 +45,7 @@ class User(db.Model): "exp": datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds), "iat": datetime.datetime.utcnow(), "sub": user_id, - "airline": airline, + "role": role, } return jwt.encode( payload, current_app.config.get("SECRET_KEY"), algorithm="HS256" @@ -60,7 +67,7 @@ class User(db.Model): "username": fields.String(required=True), "email": fields.String(required=True), "created_date": fields.DateTime, - "airline": fields.Boolean(readOnly=True), + "role": fields.String(readOnly=True), }, ) @@ -91,7 +98,7 @@ class User(db.Model): "User", { "id": fields.Integer(required=True), - "airline": fields.Boolean(readOnly=True), + "role": fields.String(required=True), }, ) diff --git a/auth-domain/user-manager/src/api/users.py b/auth-domain/user-manager/src/api/users.py index 81e5566..05b927d 100644 --- a/auth-domain/user-manager/src/api/users.py +++ b/auth-domain/user-manager/src/api/users.py @@ -88,7 +88,7 @@ class Users(Resource): "username": user.username, "email": user.email, "created_date": user.created_date.strftime("%Y-%m-%d %H:%M:%S"), - "airline": user.airline, + "role": user.role, } return response_object, 200 diff --git a/auth-domain/user-manager/src/tests/functional/test_auth.py b/auth-domain/user-manager/src/tests/functional/test_auth.py index 2f29906..096506a 100644 --- a/auth-domain/user-manager/src/tests/functional/test_auth.py +++ b/auth-domain/user-manager/src/tests/functional/test_auth.py @@ -110,7 +110,7 @@ def test_user_status(test_app, test_database, add_user): data = json.loads(resp.data.decode()) assert resp.status_code == 200 assert resp.content_type == "application/json" - assert data["role"] == 0 + assert data["role"] == "user" assert "password" not in data diff --git a/auth-domain/user-manager/src/tests/unit/test_users.py b/auth-domain/user-manager/src/tests/unit/test_users.py index 5baffc9..5465894 100644 --- a/auth-domain/user-manager/src/tests/unit/test_users.py +++ b/auth-domain/user-manager/src/tests/unit/test_users.py @@ -190,7 +190,7 @@ def test_update_user(test_app, monkeypatch): "username": username, "email": email, "created_date": datetime.now(), - "airline": False, + "role": "user", } ) return d diff --git a/browser-domain/src/Types.d.ts b/browser-domain/src/Types.d.ts index 81b8ce7..574d4c3 100644 --- a/browser-domain/src/Types.d.ts +++ b/browser-domain/src/Types.d.ts @@ -11,7 +11,7 @@ export interface Token { export interface TokenData { sub: string; - airline: boolean; + role: string; } export interface User { diff --git a/browser-domain/src/useAuth.tsx b/browser-domain/src/useAuth.tsx index 9d8e993..4415a47 100644 --- a/browser-domain/src/useAuth.tsx +++ b/browser-domain/src/useAuth.tsx @@ -30,6 +30,7 @@ export function AuthProvider({ const [loading, setLoading] = useState(false); const [loadingInitial, setLoadingInitial] = useState(true); const [isAirline, setIsAirline] = useState(false); + const [isAdmin, setIsAdmin] = useState(false); const navigate = useNavigate(); useEffect(() => { @@ -39,10 +40,11 @@ export function AuthProvider({ useEffect(() => { const existingToken = localStorage.getItem("token"); if (existingToken) { - let airline + let role try { - airline = (jwt_decode(existingToken) as TokenData).airline; - setIsAirline(airline) + role = (jwt_decode(existingToken) as TokenData).role; + setIsAirline(role == "airline") + setIsAdmin(role == "admin") } catch (err) { setLoadingInitial(false); logout() @@ -73,8 +75,9 @@ export function AuthProvider({ const tokens = logIn(credentials) .then((x) => { localStorage.setItem("token", x.access_token); - const airline = (jwt_decode(x.access_token) as TokenData).airline; - setIsAirline(airline) + const role = (jwt_decode(x.access_token) as TokenData).role; + setIsAirline(role == "airline") + setIsAdmin(role == "admin") const user = fetchUserById(x.user_id as number, x.access_token) .then(y => { setUser(y); diff --git a/gateway/src/api/routes/auth.py b/gateway/src/api/routes/auth.py index b404917..782f457 100644 --- a/gateway/src/api/routes/auth.py +++ b/gateway/src/api/routes/auth.py @@ -55,7 +55,7 @@ async def checkAuth( ): response = await status(req, authorization) if isAirline: - if response["role"] == 1: + if response["role"] == "airline": return response["id"] else: raise HTTPException( diff --git a/gateway/src/api/schemas/user.py b/gateway/src/api/schemas/user.py index 8f49615..daee193 100644 --- a/gateway/src/api/schemas/user.py +++ b/gateway/src/api/schemas/user.py @@ -6,7 +6,7 @@ class User(BaseModel): username: str email: str created_date: str - airline: bool + role: str class UserMin(BaseModel): @@ -17,7 +17,7 @@ class UserMin(BaseModel): class UserStatus(BaseModel): id: int - airline: bool + role: str class UserRegister(BaseModel): From d8aa2dde1930546e2267f5154ba8faf745786695 Mon Sep 17 00:00:00 2001 From: bsquillari Date: Mon, 4 Dec 2023 19:54:29 +0000 Subject: [PATCH 5/8] Update checkAuth with roles --- .../flights-information/src/api/cruds/flight.py | 7 ++++--- gateway/src/api/routes/auth.py | 17 +++++++---------- gateway/src/api/routes/flights.py | 8 ++++---- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/flights-domain/flights-information/src/api/cruds/flight.py b/flights-domain/flights-information/src/api/cruds/flight.py index 5c8b22a..cb32cc4 100644 --- a/flights-domain/flights-information/src/api/cruds/flight.py +++ b/flights-domain/flights-information/src/api/cruds/flight.py @@ -105,8 +105,8 @@ def update_flight(db: Session, update_data, id): db_flight = db.query(Flight).filter(Flight.id == id).first() if db_flight is None: raise KeyError - if db_flight.user_id != update_data["user_id"]: - raise PermissionError + # if db_flight.user_id != update_data["user_id"] and role != "admin": + # raise PermissionError new_flight = Flight( **{ @@ -135,7 +135,8 @@ def update_flight(db: Session, update_data, id): raise ValueError("collision") for key, value in update_data.items(): - setattr(db_flight, key, value) + if key != "user_id": + setattr(db_flight, key, value) setattr(db_flight, "last_updated", func.now()) db.commit() diff --git a/gateway/src/api/routes/auth.py b/gateway/src/api/routes/auth.py index 782f457..9795af9 100644 --- a/gateway/src/api/routes/auth.py +++ b/gateway/src/api/routes/auth.py @@ -50,22 +50,19 @@ async def status(req: Request, authorization: Annotated[str | None, Header()] = async def checkAuth( req: Request, authorization: Annotated[str | None, Header()] = None, - isAirline=False, + roles=["user", "airline", "admin"], userId=None, ): response = await status(req, authorization) - if isAirline: - if response["role"] == "airline": - return response["id"] - else: - raise HTTPException( - status_code=403, detail="You don't have the required permissions." - ) - elif userId: + if response["role"] not in roles: + raise HTTPException( + status_code=403, detail="You don't have the required permissions." + ) + if userId: if response["id"] != int(userId): raise HTTPException( status_code=403, detail="You don't have the required permissions." ) return None else: - return response["id"] + return response diff --git a/gateway/src/api/routes/flights.py b/gateway/src/api/routes/flights.py index 2e98758..782645b 100644 --- a/gateway/src/api/routes/flights.py +++ b/gateway/src/api/routes/flights.py @@ -29,9 +29,9 @@ async def create_flight( req: Request, authorization: Annotated[str | None, Header()] = None, ): - id = await checkAuth(req, authorization, isAirline=True) + authData = await checkAuth(req, authorization, roles=["airline"]) flight_data = flight.model_dump() - flight_data["user_id"] = id + flight_data["user_id"] = authData["id"] request_id = req.state.request_id header = {"x-api-request-id": request_id} (response, status, _) = await request( @@ -66,9 +66,9 @@ async def update_flight( req: Request, authorization: Annotated[str | None, Header()] = None, ): - user_id = await checkAuth(req, authorization, isAirline=True) + authData = await checkAuth(req, authorization, roles=["airline", "admin"]) update = flight_update.model_dump() - update["user_id"] = user_id + update["user_id"] = authData["id"] request_id = req.state.request_id header = {"x-api-request-id": request_id} (response, status, _) = await request( From 1ef259a7b44e8e905aad0c1b814347017b651c96 Mon Sep 17 00:00:00 2001 From: bsquillari Date: Mon, 4 Dec 2023 19:57:39 +0000 Subject: [PATCH 6/8] Add admin role to front --- browser-domain/src/App.tsx | 4 ++-- browser-domain/src/components/Home/Card/Card.tsx | 8 ++++---- browser-domain/src/components/Home/Home.tsx | 4 ++-- browser-domain/src/useAuth.tsx | 4 +++- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/browser-domain/src/App.tsx b/browser-domain/src/App.tsx index 20da666..b5995c3 100644 --- a/browser-domain/src/App.tsx +++ b/browser-domain/src/App.tsx @@ -8,7 +8,7 @@ import useAuth, { AuthProvider } from "./useAuth"; import { EditFlight } from "./components/CreateFlight/EditFlight"; function Router() { - const { user, logout, isAirline } = useAuth(); + const { user, logout, isAirline, isAdmin } = useAuth(); return (
@@ -17,7 +17,7 @@ function Router() { } /> : } /> : } /> - : } /> + : } /> : } />
diff --git a/browser-domain/src/components/Home/Card/Card.tsx b/browser-domain/src/components/Home/Card/Card.tsx index 696712a..051ed9b 100644 --- a/browser-domain/src/components/Home/Card/Card.tsx +++ b/browser-domain/src/components/Home/Card/Card.tsx @@ -24,12 +24,13 @@ interface CardProps { subscribed: boolean; refresh: any; isAirline: boolean; + isAdmin: boolean; refreshFlights: any; } const { Text } = Typography; -export const Card: React.FC = ({ flight, user, subscribed, refresh, refreshFlights, isAirline }) => { +export const Card: React.FC = ({ flight, user, subscribed, refresh, refreshFlights, isAirline, isAdmin }) => { const [modalVisible, setModalVisible] = useState(false); const navigate = useNavigate(); @@ -81,7 +82,6 @@ export const Card: React.FC = ({ flight, user, subscribed, refresh, r editFlight("" + flight.id, data, token) .then(() => { - console.log("culicagado") refreshFlights() }) .catch((error) => { @@ -158,7 +158,7 @@ export const Card: React.FC = ({ flight, user, subscribed, refresh, r {flight.id}
- {!isAirline ? + {!isAirline && !isAdmin ? ( !(subscribed) ?
diff --git a/browser-domain/src/useAuth.tsx b/browser-domain/src/useAuth.tsx index 4415a47..0323f4d 100644 --- a/browser-domain/src/useAuth.tsx +++ b/browser-domain/src/useAuth.tsx @@ -8,6 +8,7 @@ interface AuthContextType { user?: User; loading: boolean; isAirline: boolean; + isAdmin: boolean; token?: string; error?: any; login: (credentials: Credentials) => void; @@ -105,13 +106,14 @@ export function AuthProvider({ user, loading, isAirline, + isAdmin, token, error, login, signUp, logout, }), - [user, isAirline, loading, error] + [user, isAirline, isAdmin, loading, error] ); return ( From 00f016f2290b2b2cbd10e619bfd06b6ba52e0a03 Mon Sep 17 00:00:00 2001 From: bsquillari Date: Mon, 4 Dec 2023 20:51:13 +0000 Subject: [PATCH 7/8] Add post user with airline role --- .../user-manager/src/api/cruds/users.py | 9 ++++++++- auth-domain/user-manager/src/api/users.py | 7 ++++++- gateway/src/api/routes/users.py | 19 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/auth-domain/user-manager/src/api/cruds/users.py b/auth-domain/user-manager/src/api/cruds/users.py index fed4c66..087f023 100644 --- a/auth-domain/user-manager/src/api/cruds/users.py +++ b/auth-domain/user-manager/src/api/cruds/users.py @@ -1,5 +1,5 @@ from src import db -from src.api.models.users import User +from src.api.models.users import Roles, User def get_all_users(): @@ -21,6 +21,13 @@ def add_user(username, email, password): return user +def add_airline(username, email, password): + user = User(username=username, email=email, password=password, role=Roles.airline) + db.session.add(user) + db.session.commit() + return user + + def update_user(user, username, email): user.username = username user.email = email diff --git a/auth-domain/user-manager/src/api/users.py b/auth-domain/user-manager/src/api/users.py index 05b927d..0d59168 100644 --- a/auth-domain/user-manager/src/api/users.py +++ b/auth-domain/user-manager/src/api/users.py @@ -10,6 +10,7 @@ from src.api.cruds.users import ( # isort:skip get_user_by_id, update_user, delete_user, + add_airline, ) NAMESPACE = "users" @@ -34,6 +35,7 @@ class UsersList(Resource): username = post_data.get("username") email = post_data.get("email") password = post_data.get("password") + role = post_data.get("role") response_object = {} user = get_user_by_email(email) @@ -41,7 +43,10 @@ class UsersList(Resource): response_object["message"] = "Sorry. That email already exists." return response_object, 400 - user = add_user(username, email, password) + if role == "airline": + user = add_airline(username, email, password) + else: + user = add_user(username, email, password) response_object = { "message": f"{user.email} was added!", diff --git a/gateway/src/api/routes/users.py b/gateway/src/api/routes/users.py index 6390339..ea2baa0 100644 --- a/gateway/src/api/routes/users.py +++ b/gateway/src/api/routes/users.py @@ -22,6 +22,25 @@ async def create_users(user: UserRegister, req: Request): return response +@router.post("/airline", response_model=UserMin) +async def create_airline( + user: UserRegister, + req: Request, + authorization: Annotated[str | None, Header()] = None, +): + await checkAuth(req, authorization, roles=["admin"]) + request_id = req.state.request_id + header = {"x-api-request-id": request_id} + data = user.model_dump() + data["role"] = "airline" + (response, status, _) = await request( + f"{API_USERS}", "POST", json=data, headers=header + ) + 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, req: Request, authorization: Annotated[str | None, Header()] = None From 9b3039bead19473928f642f25b61ca4907b20272 Mon Sep 17 00:00:00 2001 From: bsquillari Date: Mon, 4 Dec 2023 20:54:09 +0000 Subject: [PATCH 8/8] Add create airline flow to browser --- browser-domain/src/Api.ts | 11 +++- browser-domain/src/App.tsx | 2 + browser-domain/src/components/Home/Home.tsx | 1 + .../src/components/SignUp/CreateAirline.tsx | 58 +++++++++++++++++++ browser-domain/src/hooks/useCreateAirline.tsx | 39 +++++++++++++ 5 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 browser-domain/src/components/SignUp/CreateAirline.tsx create mode 100644 browser-domain/src/hooks/useCreateAirline.tsx diff --git a/browser-domain/src/Api.ts b/browser-domain/src/Api.ts index 9c055f7..1f31e7b 100644 --- a/browser-domain/src/Api.ts +++ b/browser-domain/src/Api.ts @@ -24,7 +24,7 @@ instance.interceptors.response.use( json["count"] = response.headers["x-count"] console.log(json) return json - } else if (response.status == 204) { + } else if (response.status === 204) { return response; } return JSON.parse(response.data); @@ -41,6 +41,15 @@ export const createUser = ( return instance.post("users", credentials); }; +export const createAirline = ( + credentials: Credentials, + token: string +): Promise<{ id?: string; message: string }> => { + return instance.post("users/airline", credentials, { + headers: { Authorization: `Bearer ${token}` }, + }); +}; + export const fetchUserById = (id: number, token: string): Promise => { return instance.get("users/" + id, { headers: { Authorization: `Bearer ${token}` }, diff --git a/browser-domain/src/App.tsx b/browser-domain/src/App.tsx index b5995c3..0e82d7e 100644 --- a/browser-domain/src/App.tsx +++ b/browser-domain/src/App.tsx @@ -1,6 +1,7 @@ import { LogIn } from "./components/LogIn/LogIn"; import { Navigate, Route, RouteProps, Routes } from "react-router"; import { SignUp } from "./components/SignUp/SignUp"; +import { CreateAirline } from "./components/SignUp/CreateAirline"; import { Home } from "./components/Home/Home"; import { CreateFlight } from "./components/CreateFlight/CreateFlight"; import { Button } from "antd"; @@ -15,6 +16,7 @@ function Router() { } /> } /> + : } /> : } /> : } /> : } /> diff --git a/browser-domain/src/components/Home/Home.tsx b/browser-domain/src/components/Home/Home.tsx index a4a369c..0002654 100644 --- a/browser-domain/src/components/Home/Home.tsx +++ b/browser-domain/src/components/Home/Home.tsx @@ -53,6 +53,7 @@ export const Home: React.FC = (props) => { return (
{isAirline ? : <>} + {isAdmin ? : <>}

Flights

{(props.flights ? props.flights : flights).map((f) => { diff --git a/browser-domain/src/components/SignUp/CreateAirline.tsx b/browser-domain/src/components/SignUp/CreateAirline.tsx new file mode 100644 index 0000000..6640477 --- /dev/null +++ b/browser-domain/src/components/SignUp/CreateAirline.tsx @@ -0,0 +1,58 @@ +import React, { useState } from "react"; +import { Button, Input } from "antd"; +import { useCreateAirline } from "../../hooks/useCreateAirline"; + +export const CreateAirline = () => { + const [username, setUsername] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [repeatPassword, setRepeatPassword] = useState(""); + + const { createAirline, isLoading, error } = useCreateAirline(); + + return ( +
+
+
+ setEmail(ev.target.value)} + /> + setUsername(ev.target.value)} + /> + setPassword(ev.target.value)} + /> + setRepeatPassword(ev.target.value)} + /> + + {error ? ( +
{error}
+ ) : ( + <> + )} +
+
+
+ ); +}; diff --git a/browser-domain/src/hooks/useCreateAirline.tsx b/browser-domain/src/hooks/useCreateAirline.tsx new file mode 100644 index 0000000..63ea8e4 --- /dev/null +++ b/browser-domain/src/hooks/useCreateAirline.tsx @@ -0,0 +1,39 @@ +import { useState } from "react"; +import { Credentials } from "../Types"; +import { createAirline as createAirlineAPI } from "../Api"; +import useAuth from "../useAuth"; +import { useNavigate } from "react-router"; + +export const useCreateAirline = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const navigate = useNavigate(); + + const createAirline = async (credentials: Credentials): Promise => { + try { + setIsLoading(true); + setError(null); + + const token = localStorage.getItem("token"); + if (!token) { + setError("No token!"); + setIsLoading(false); + return; + } + + const createResponse = await createAirlineAPI(credentials, token); + + if (createResponse.id) { + navigate("/home"); + } else { + setError(createResponse.message); + } + } catch (error) { + setError(error as string); + } finally { + setIsLoading(false); + } + }; + + return { createAirline, isLoading, error }; +};