Merge branch 'admin-role' into 'master'

Merge admin-role

See merge request adm3981141/fids!4
This commit is contained in:
bsquillari 2023-12-04 21:58:24 +00:00
commit 8b05695ce3
27 changed files with 520 additions and 181 deletions

View File

@ -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 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 pull
- docker compose -f subscription-domain/docker-compose.dev.yml --env-file $ENV_DEV_FILE up -d - 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 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 pull
- docker compose -f ${FOLDER}/docker-compose.dev.yml --env-file $ENV_DEV_FILE up --abort-on-container-exit - 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-frontend
- *changes-backend - *changes-backend
script: script:
- export API_IMAGE=${E2E_TEST_IMAGE_NAME} - export CLIENT_IMAGE=${E2E_TEST_IMAGE_NAME}
- export FOLDER=testing/catcher - export FOLDER=testing/catcher
- *test-integration - *test-integration
after_script: after_script:

View File

@ -1,7 +1,7 @@
from flask.cli import FlaskGroup from flask.cli import FlaskGroup
from src import create_app, db from src import create_app, db
from src.api.models.users import User from src.api.models.users import Roles, User
app = create_app() app = create_app()
cli = FlaskGroup(create_app=create_app) cli = FlaskGroup(create_app=create_app)
@ -21,7 +21,7 @@ def seed_db():
username="lufthansa", username="lufthansa",
email="info@lufthansa.com", email="info@lufthansa.com",
password="password1234", password="password1234",
airline=True, role=Roles.airline,
) )
) )
db.session.add( db.session.add(
@ -29,7 +29,15 @@ def seed_db():
username="ryanair", username="ryanair",
email="info@ryanair.com", email="info@ryanair.com",
password="password1234", password="password1234",
airline=True, role=Roles.airline,
)
)
db.session.add(
User(
username="admin",
email="admin",
password="password1234",
role=Roles.admin,
) )
) )
db.session.add( db.session.add(

View File

@ -3,7 +3,7 @@ from flask import request
from flask_restx import Namespace, Resource from flask_restx import Namespace, Resource
from src import bcrypt 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 from src.api.models.users import User
auth_namespace = Namespace("auth") auth_namespace = Namespace("auth")
@ -19,25 +19,6 @@ parser = auth_namespace.parser()
parser.add_argument("Authorization", location="headers") 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): class Login(Resource):
@auth_namespace.marshal_with(auth_tokens_model) @auth_namespace.marshal_with(auth_tokens_model)
@auth_namespace.expect(auth_login_model, validate=True) @auth_namespace.expect(auth_login_model, validate=True)
@ -53,7 +34,7 @@ class Login(Resource):
if not user or not bcrypt.check_password_hash(user.password, password): if not user or not bcrypt.check_password_hash(user.password, password):
auth_namespace.abort(404, "User does not exist") 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") refresh_token = user.encode_token(user.id, "refresh")
response_object = { response_object = {
@ -81,7 +62,7 @@ class Refresh(Resource):
if not user: if not user:
auth_namespace.abort(401, "Invalid token") 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") refresh_token = user.encode_token(user.id, "refresh")
response_object = { response_object = {
@ -124,7 +105,6 @@ class Status(Resource):
auth_namespace.abort(403, "Token required") auth_namespace.abort(403, "Token required")
auth_namespace.add_resource(Register, "/register")
auth_namespace.add_resource(Login, "/login") auth_namespace.add_resource(Login, "/login")
auth_namespace.add_resource(Refresh, "/refresh") auth_namespace.add_resource(Refresh, "/refresh")
auth_namespace.add_resource(Status, "/status") auth_namespace.add_resource(Status, "/status")

View File

@ -1,5 +1,5 @@
from src import db from src import db
from src.api.models.users import User from src.api.models.users import Roles, User
def get_all_users(): def get_all_users():
@ -21,6 +21,13 @@ def add_user(username, email, password):
return user 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): def update_user(user, username, email):
user.username = username user.username = username
user.email = email user.email = email

View File

@ -1,4 +1,5 @@
import datetime import datetime
from enum import Enum
import jwt import jwt
from flask import current_app from flask import current_app
@ -8,6 +9,12 @@ from sqlalchemy.sql import func
from src import bcrypt, db from src import bcrypt, db
class Roles(Enum):
user = "user"
airline = "airline"
admin = "admin"
class User(db.Model): class User(db.Model):
__tablename__ = "users" __tablename__ = "users"
@ -17,18 +24,18 @@ class User(db.Model):
password = db.Column(db.String(255), nullable=False) password = db.Column(db.String(255), nullable=False)
active = db.Column(db.Boolean(), default=True, nullable=False) active = db.Column(db.Boolean(), default=True, nullable=False)
created_date = db.Column(db.DateTime, default=func.now(), 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.username = username
self.email = email self.email = email
self.password = bcrypt.generate_password_hash( self.password = bcrypt.generate_password_hash(
password, current_app.config.get("BCRYPT_LOG_ROUNDS") password, current_app.config.get("BCRYPT_LOG_ROUNDS")
).decode() ).decode()
self.airline = airline self.role = role.value
@staticmethod @staticmethod
def encode_token(user_id, token_type, airline=False): def encode_token(user_id, token_type, role="user"):
if token_type == "access": if token_type == "access":
seconds = current_app.config.get("ACCESS_TOKEN_EXPIRATION") seconds = current_app.config.get("ACCESS_TOKEN_EXPIRATION")
else: else:
@ -38,7 +45,7 @@ class User(db.Model):
"exp": datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds), "exp": datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds),
"iat": datetime.datetime.utcnow(), "iat": datetime.datetime.utcnow(),
"sub": user_id, "sub": user_id,
"airline": airline, "role": role,
} }
return jwt.encode( return jwt.encode(
payload, current_app.config.get("SECRET_KEY"), algorithm="HS256" payload, current_app.config.get("SECRET_KEY"), algorithm="HS256"
@ -60,7 +67,7 @@ class User(db.Model):
"username": fields.String(required=True), "username": fields.String(required=True),
"email": fields.String(required=True), "email": fields.String(required=True),
"created_date": fields.DateTime, "created_date": fields.DateTime,
"airline": fields.Boolean(readOnly=True), "role": fields.String(readOnly=True),
}, },
) )
@ -91,7 +98,7 @@ class User(db.Model):
"User", "User",
{ {
"id": fields.Integer(required=True), "id": fields.Integer(required=True),
"airline": fields.Boolean(readOnly=True), "role": fields.String(required=True),
}, },
) )

View File

@ -10,6 +10,7 @@ from src.api.cruds.users import ( # isort:skip
get_user_by_id, get_user_by_id,
update_user, update_user,
delete_user, delete_user,
add_airline,
) )
NAMESPACE = "users" NAMESPACE = "users"
@ -34,6 +35,7 @@ class UsersList(Resource):
username = post_data.get("username") username = post_data.get("username")
email = post_data.get("email") email = post_data.get("email")
password = post_data.get("password") password = post_data.get("password")
role = post_data.get("role")
response_object = {} response_object = {}
user = get_user_by_email(email) user = get_user_by_email(email)
@ -41,7 +43,10 @@ class UsersList(Resource):
response_object["message"] = "Sorry. That email already exists." response_object["message"] = "Sorry. That email already exists."
return response_object, 400 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 = { response_object = {
"message": f"{user.email} was added!", "message": f"{user.email} was added!",
@ -88,7 +93,7 @@ class Users(Resource):
"username": user.username, "username": user.username,
"email": user.email, "email": user.email,
"created_date": user.created_date.strftime("%Y-%m-%d %H:%M:%S"), "created_date": user.created_date.strftime("%Y-%m-%d %H:%M:%S"),
"airline": user.airline, "role": user.role,
} }
return response_object, 200 return response_object, 200

View File

@ -1,75 +1,11 @@
import json import json
import time import time
import pytest
TEST_USERNAME = "fede_auth" TEST_USERNAME = "fede_auth"
TEST_EMAIL = "fede_auth@gmail.com" TEST_EMAIL = "fede_auth@gmail.com"
TEST_PASSWD = "password1234" 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): def test_registered_user_login(test_app, test_database, add_user):
add_user(TEST_USERNAME, TEST_EMAIL, TEST_PASSWD) add_user(TEST_USERNAME, TEST_EMAIL, TEST_PASSWD)
client = test_app.test_client() 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()) data = json.loads(resp.data.decode())
assert resp.status_code == 200 assert resp.status_code == 200
assert resp.content_type == "application/json" assert resp.content_type == "application/json"
assert not data["airline"] assert data["role"] == "user"
assert "password" not in data assert "password" not in data

View File

@ -190,7 +190,7 @@ def test_update_user(test_app, monkeypatch):
"username": username, "username": username,
"email": email, "email": email,
"created_date": datetime.now(), "created_date": datetime.now(),
"airline": False, "role": "user",
} }
) )
return d return d

View File

@ -1,5 +1,5 @@
import { Axios, AxiosError } from "axios"; 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({ 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:5000/",
@ -24,7 +24,7 @@ instance.interceptors.response.use(
json["count"] = response.headers["x-count"] json["count"] = response.headers["x-count"]
console.log(json) console.log(json)
return json return json
} else if (response.status == 204) { } else if (response.status === 204) {
return response; return response;
} }
return JSON.parse(response.data); return JSON.parse(response.data);
@ -41,6 +41,15 @@ export const createUser = (
return instance.post("users", credentials); 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<User & { message?: string }> => { export const fetchUserById = (id: number, token: string): Promise<User & { message?: string }> => {
return instance.get("users/" + id, { return instance.get("users/" + id, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
@ -79,6 +88,24 @@ export const createFlight = (
}); });
}; };
export const editFlight = (
flight_id:string,
fligth_data: FlightEdit,
token: string
):Promise<Flight> => {
return instance.patch("flights/" + flight_id , fligth_data, {
headers: { Authorization: `Bearer ${token}` },
});
};
export const fetchFlight = (
flight_id:string,
):Promise<Flight> => {
return instance.get("flights/" + flight_id);
};
export const subscribeToFlight = (subscription: SubscriptionsCreate, token: string): Promise<SubscriptionsCreate> => { export const subscribeToFlight = (subscription: SubscriptionsCreate, token: string): Promise<SubscriptionsCreate> => {
return instance.post("subscriptions", subscription, { return instance.post("subscriptions", subscription, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },

View File

@ -1,21 +1,25 @@
import { LogIn } from "./components/LogIn/LogIn"; import { LogIn } from "./components/LogIn/LogIn";
import { Navigate, Route, RouteProps, Routes } from "react-router"; import { Navigate, Route, RouteProps, Routes } from "react-router";
import { SignUp } from "./components/SignUp/SignUp"; import { SignUp } from "./components/SignUp/SignUp";
import { CreateAirline } from "./components/SignUp/CreateAirline";
import { Home } from "./components/Home/Home"; import { Home } from "./components/Home/Home";
import { CreateFlight } from "./components/CreateFlight/CreateFlight"; import { CreateFlight } from "./components/CreateFlight/CreateFlight";
import { Button } from "antd"; import { Button } from "antd";
import useAuth, { AuthProvider } from "./useAuth"; import useAuth, { AuthProvider } from "./useAuth";
import { EditFlight } from "./components/CreateFlight/EditFlight";
function Router() { function Router() {
const { user, logout, isAirline } = useAuth(); const { user, logout, isAirline, isAdmin } = useAuth();
return ( return (
<div className="App"> <div className="App">
<Routes> <Routes>
<Route path="/login" element={<LogIn />} /> <Route path="/login" element={<LogIn />} />
<Route path="/signup" element={<SignUp />} /> <Route path="/signup" element={<SignUp />} />
<Route path="/create-airline" element={!isAdmin ? <LogIn /> : <CreateAirline />} />
<Route path="/home" element={!user ? <LogIn /> : <Home />} /> <Route path="/home" element={!user ? <LogIn /> : <Home />} />
<Route path="/create-flight" element={!isAirline ? <LogIn /> : <CreateFlight />} /> <Route path="/create-flight" element={!isAirline ? <LogIn /> : <CreateFlight />} />
<Route path="/edit-flight/:id" element={!isAirline && !isAdmin ? <LogIn /> : <EditFlight />} />
<Route path="/" element={!user ? <LogIn /> : <Home />} /> <Route path="/" element={!user ? <LogIn /> : <Home />} />
</Routes> </Routes>
<div className="LogoutButton"> <div className="LogoutButton">

View File

@ -11,7 +11,7 @@ export interface Token {
export interface TokenData { export interface TokenData {
sub: string; sub: string;
airline: boolean; role: string;
} }
export interface User { export interface User {
@ -35,6 +35,7 @@ export interface Flight {
departure_time: string; departure_time: string;
arrival_time: string; arrival_time: string;
gate: string; gate: string;
user_id: number;
} }
export interface FlightCreate { export interface FlightCreate {
@ -47,6 +48,20 @@ export interface FlightCreate {
gate: string; 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 { export interface SubscriptionsCreate {
flight_id: number; flight_id: number;
user_id: number; user_id: number;

View File

@ -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> = (props) => {
const navigate = useNavigate();
let { id } = useParams();
const [error, setError] = useState<string | null>(null);
const [flight, setFlight] = useState<Flight>();
const { flight: initialData } = useFetchFlight(id);
const [flightData, setFlightData] = useState<FlightEditNotNull>({
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 (
<form onSubmit={handleSubmit}>
<label>
Status:
<input
type="text"
value={flightData?.status}
onChange={(e) =>
setFlightData({ ...flightData, status: e.target.value })
}
/>
</label>
<label>
Departure Time:
<input
type="text"
value={flightData?.departure_time}
onChange={(e) =>
setFlightData({ ...flightData, departure_time: e.target.value })
}
/>
</label>
<label>
Arrival Time:
<input
type="text"
value={flightData?.arrival_time}
onChange={(e) =>
setFlightData({ ...flightData, arrival_time: e.target.value })
}
/>
</label>
<label>
Gate:
<input
type="text"
value={flightData?.gate}
onChange={(e) => setFlightData({ ...flightData, gate: e.target.value })}
/>
</label>
<button type="submit">Submit</button>
</form>
);
};

View File

@ -1,36 +1,40 @@
import React, { useEffect, useState } from "react"; 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 { Avatar, Space, Typography, Tag, Button, Modal } from "antd";
import { RightOutlined, ClockCircleOutlined, SwapOutlined, EnvironmentOutlined, CalendarOutlined } from "@ant-design/icons"; import { RightOutlined, ClockCircleOutlined, SwapOutlined, EnvironmentOutlined, CalendarOutlined } from "@ant-design/icons";
import "./Card.css"; import "./Card.css";
import { getChatId, getSubscription, subscribeToFlight, unsubscribeFromFlight } from "../../../Api"; import { getChatId, getSubscription, subscribeToFlight, unsubscribeFromFlight, editFlight } from "../../../Api";
import { User } from "../../../Types"; import { Flight, FlightEdit, User } from "../../../Types";
interface FlightProps { // interface FlightProps {
id: number; // id: number;
flight_code: string; // flight_code: string;
status: string; // status: string;
origin: string; // origin: string;
destination: string; // destination: string;
departure_time: string; // departure_time: string;
arrival_time: string; // arrival_time: string;
gate: string; // gate: string;
} // }
interface CardProps { interface CardProps {
flight: FlightProps; flight: Flight;
user: User | undefined; user: User | undefined;
subscribed: boolean; subscribed: boolean;
refresh: any; refresh: any;
isAirline: boolean;
isAdmin: boolean;
refreshFlights: any;
} }
const { Text } = Typography; const { Text } = Typography;
export const Card: React.FC<CardProps> = ({ flight, user, subscribed, refresh }) => { export const Card: React.FC<CardProps> = ({ flight, user, subscribed, refresh, refreshFlights, isAirline, isAdmin }) => {
const [modalVisible, setModalVisible] = useState<boolean>(false); const [modalVisible, setModalVisible] = useState<boolean>(false);
const navigate = useNavigate();
const handleSubscribe = async (event: React.FormEvent) => { const handleSubscribe = async (event: React.FormEvent) => {
event.preventDefault(); event.preventDefault();
@ -59,6 +63,32 @@ export const Card: React.FC<CardProps> = ({ 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(() => {
refreshFlights()
})
.catch((error) => {
console.log(error)
});
};
const handleModalClose = () => { const handleModalClose = () => {
setModalVisible(false); setModalVisible(false);
}; };
@ -83,10 +113,12 @@ export const Card: React.FC<CardProps> = ({ flight, user, subscribed, refresh })
refresh() refresh()
}) })
.catch((error) => { .catch((error) => {
console.log(error)
}); });
}; };
console.log(subscribed) console.log(flight.user_id)
console.log(user?.id)
return ( return (
<div className="flight-card"> <div className="flight-card">
@ -126,14 +158,31 @@ export const Card: React.FC<CardProps> = ({ flight, user, subscribed, refresh })
<Text>{flight.id}</Text> <Text>{flight.id}</Text>
</Space> </Space>
</div> </div>
{!(subscribed) ? {!isAirline && !isAdmin ?
<Button type="primary" onClick={handleSubscribe}> (
!(subscribed) ?
<Button type="primary" onClick={handleSubscribe}>
Subscribe Subscribe
</Button> </Button>
: :
<Button type="primary" onClick={handleUnsubscribe}> <Button type="primary" onClick={handleUnsubscribe}>
Unsubscribe Unsubscribe
</Button> </Button>
)
:
(
(user && flight.user_id == user.id) || isAdmin ?
<>
<Button type="primary" onClick={handleEdit}>
Edit
</Button>
<Button type="primary" onClick={handleDelete}>
Delete
</Button>
</>
:
<></>
)
} }
<Modal <Modal
title="Error" title="Error"

View File

@ -14,11 +14,11 @@ export const Home: React.FC<Props> = (props) => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const origin = urlParams.get('origin'); const origin = urlParams.get('origin');
const initialPage = parseInt(urlParams.get('page') || '1', 10); 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 navigate = useNavigate()
const [currentPage, setCurrentPage] = useState(initialPage); const [currentPage, setCurrentPage] = useState(initialPage);
const { loading, isAirline, user, token } = useAuth(); const { loading, isAirline, user, token, isAdmin } = useAuth();
const { subscriptions, loading: subsLoading, fetchData } = useFetchSubscriptions(user, token); const { subscriptions, loading: subsLoading, fetchData } = useFetchSubscriptions(user, token);
@ -53,10 +53,13 @@ export const Home: React.FC<Props> = (props) => {
return ( return (
<div className="Box"> <div className="Box">
{isAirline ? <button onClick={() => { navigate("/create-flight") }}>Create flight</button> : <></>} {isAirline ? <button onClick={() => { navigate("/create-flight") }}>Create flight</button> : <></>}
{isAdmin ? <button onClick={() => { navigate("/create-airline") }}>Create airline user</button> : <></>}
<h2>Flights</h2> <h2>Flights</h2>
<div className="Items"> <div className="Items">
{(props.flights ? props.flights : flights).map((f) => { {(props.flights ? props.flights : flights).map((f) => {
return <Card key={f.id} flight={f} user={user} subscribed={subscriptions.some((i => i.flight_id === f.id))} refresh={fetchData} />; return <Card key={f.id} flight={f} user={user}
subscribed={subscriptions.some((i => i.flight_id === f.id))}
refresh={fetchData} refreshFlights={refreshFlights} isAirline={isAirline} isAdmin={isAdmin} />;
})} })}
{error ? <div className="Disconnected">{error}</div> : <></>} {error ? <div className="Disconnected">{error}</div> : <></>}
</div> </div>

View File

@ -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 (
<div className="Box Small">
<div className="Section">
<div className="Section">
<Input
type="email"
placeholder="Email"
onChange={(ev) => setEmail(ev.target.value)}
/>
<Input
placeholder="Username"
onChange={(ev) => setUsername(ev.target.value)}
/>
<Input.Password
placeholder="Password"
onChange={(ev) => setPassword(ev.target.value)}
/>
<Input.Password
placeholder="Repeat password"
onChange={(ev) => setRepeatPassword(ev.target.value)}
/>
<Button
style={{ width: "100%" }}
onClick={async () =>
await createAirline({ email, password, username })
}
loading={isLoading}
disabled={
email === "" ||
username === "" ||
password === "" ||
password !== repeatPassword
}
>
Create Airline
</Button>
{error ? (
<div className="Disconnected">{error}</div>
) : (
<></>
)}
</div>
</div>
</div>
);
};

View File

@ -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<string | null>(null);
const navigate = useNavigate();
const createAirline = async (credentials: Credentials): Promise<void> => {
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 };
};

View File

@ -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<string | null>(null);
const [flight, setFlight] = useState<Flight>();
const [count, setCount] = useState<number>(0);
useEffect(() => {
setError(null);
if (id == null || id == undefined)
return;
fetchFlight(id)
.then((data) => {
setFlight(data);
})
.catch((error) => { });
}, [id]);
return { flight, count, error };
};

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react"; import React, { useCallback, useEffect } from "react";
import { useState } from "react"; import { useState } from "react";
import { User, Flight } from "../Types"; import { User, Flight } from "../Types";
import { fetchFlights } from "../Api"; import { fetchFlights } from "../Api";
@ -8,16 +8,31 @@ export const useFetchFlights = (origin: string | null, page: number | null) =>
const [flights, setFlights] = useState<Flight[]>([]); const [flights, setFlights] = useState<Flight[]>([]);
const [count, setCount] = useState<number>(0); const [count, setCount] = useState<number>(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); setError(null);
fetchFlights(origin, page) fetchFlights(origin, page)
.then((data) => { .then((data) => {
setCount(data.count) setCount(data.count)
setFlights(data.flights); setFlights(data.flights.filter((e) => e.status != "Deleted" ));
}) })
.catch((error) => { }); .catch((error) => { });
}, [page]); }, [origin, page]);
return { flights, count, error }; useEffect(() => {
fetchData()
}, [fetchData]);
return { flights, count, error, fetchData };
}; };

View File

@ -8,6 +8,7 @@ interface AuthContextType {
user?: User; user?: User;
loading: boolean; loading: boolean;
isAirline: boolean; isAirline: boolean;
isAdmin: boolean;
token?: string; token?: string;
error?: any; error?: any;
login: (credentials: Credentials) => void; login: (credentials: Credentials) => void;
@ -30,6 +31,7 @@ export function AuthProvider({
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [loadingInitial, setLoadingInitial] = useState<boolean>(true); const [loadingInitial, setLoadingInitial] = useState<boolean>(true);
const [isAirline, setIsAirline] = useState(false); const [isAirline, setIsAirline] = useState(false);
const [isAdmin, setIsAdmin] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
@ -39,10 +41,11 @@ export function AuthProvider({
useEffect(() => { useEffect(() => {
const existingToken = localStorage.getItem("token"); const existingToken = localStorage.getItem("token");
if (existingToken) { if (existingToken) {
let airline let role
try { try {
airline = (jwt_decode(existingToken) as TokenData).airline; role = (jwt_decode(existingToken) as TokenData).role;
setIsAirline(airline) setIsAirline(role == "airline")
setIsAdmin(role == "admin")
} catch (err) { } catch (err) {
setLoadingInitial(false); setLoadingInitial(false);
logout() logout()
@ -73,8 +76,9 @@ export function AuthProvider({
const tokens = logIn(credentials) const tokens = logIn(credentials)
.then((x) => { .then((x) => {
localStorage.setItem("token", x.access_token); localStorage.setItem("token", x.access_token);
const airline = (jwt_decode(x.access_token) as TokenData).airline; const role = (jwt_decode(x.access_token) as TokenData).role;
setIsAirline(airline) setIsAirline(role == "airline")
setIsAdmin(role == "admin")
const user = fetchUserById(x.user_id as number, x.access_token) const user = fetchUserById(x.user_id as number, x.access_token)
.then(y => { .then(y => {
setUser(y); setUser(y);
@ -102,13 +106,14 @@ export function AuthProvider({
user, user,
loading, loading,
isAirline, isAirline,
isAdmin,
token, token,
error, error,
login, login,
signUp, signUp,
logout, logout,
}), }),
[user, isAirline, loading, error] [user, isAirline, isAdmin, loading, error]
); );
return ( return (

View File

@ -105,8 +105,8 @@ def update_flight(db: Session, update_data, id):
db_flight = db.query(Flight).filter(Flight.id == id).first() db_flight = db.query(Flight).filter(Flight.id == id).first()
if db_flight is None: if db_flight is None:
raise KeyError raise KeyError
if db_flight.user_id != update_data["user_id"]: # if db_flight.user_id != update_data["user_id"] and role != "admin":
raise PermissionError # raise PermissionError
new_flight = Flight( new_flight = Flight(
**{ **{
@ -135,7 +135,8 @@ def update_flight(db: Session, update_data, id):
raise ValueError("collision") raise ValueError("collision")
for key, value in update_data.items(): 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()) setattr(db_flight, "last_updated", func.now())
db.commit() db.commit()

View File

@ -28,7 +28,7 @@ app.add_middleware(
"http://localhost:3000", "http://localhost:3000",
], ],
allow_credentials=True, allow_credentials=True,
allow_methods=["POST", "GET", "PUT", "DELETE", "OPTIONS"], allow_methods=["POST", "GET", "PUT", "DELETE", "OPTIONS", "PATCH"],
allow_headers=["*"], allow_headers=["*"],
expose_headers=["x-count"], expose_headers=["x-count"],
) )

View File

@ -5,23 +5,11 @@ from fastapi import APIRouter, Header, HTTPException, Request
from src.api.config import API_AUTH from src.api.config import API_AUTH
from src.api.schemas.auth import RefreshToken, Token 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 = 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) @router.post("/login", response_model=Token)
async def login(user: UserLogin, req: Request): async def login(user: UserLogin, req: Request):
request_id = req.state.request_id request_id = req.state.request_id
@ -62,22 +50,19 @@ async def status(req: Request, authorization: Annotated[str | None, Header()] =
async def checkAuth( async def checkAuth(
req: Request, req: Request,
authorization: Annotated[str | None, Header()] = None, authorization: Annotated[str | None, Header()] = None,
isAirline=False, roles=["user", "airline", "admin"],
userId=None, userId=None,
): ):
response = await status(req, authorization) response = await status(req, authorization)
if isAirline: if response["role"] not in roles:
if response["airline"]: raise HTTPException(
return response["id"] status_code=403, detail="You don't have the required permissions."
else: )
raise HTTPException( if userId:
status_code=403, detail="You don't have the required permissions."
)
elif userId:
if response["id"] != int(userId): if response["id"] != int(userId):
raise HTTPException( raise HTTPException(
status_code=403, detail="You don't have the required permissions." status_code=403, detail="You don't have the required permissions."
) )
return None return None
else: else:
return response["id"] return response

View File

@ -5,12 +5,12 @@ from fastapi import APIRouter, Header, HTTPException, Request, Response
from src.api.config import API_FLIGHTS from src.api.config import API_FLIGHTS
from src.api.routes.auth import checkAuth 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 = APIRouter()
@router.get("/{id}", response_model=Flight) @router.get("/{id}", response_model=FlightFull)
async def get_flight_by_id( async def get_flight_by_id(
id: int, id: int,
req: Request, req: Request,
@ -29,9 +29,9 @@ async def create_flight(
req: Request, req: Request,
authorization: Annotated[str | None, Header()] = None, 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 = flight.model_dump()
flight_data["user_id"] = id flight_data["user_id"] = authData["id"]
request_id = req.state.request_id request_id = req.state.request_id
header = {"x-api-request-id": request_id} header = {"x-api-request-id": request_id}
(response, status, _) = await request( (response, status, _) = await request(
@ -42,6 +42,23 @@ async def create_flight(
return response 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) @router.patch("/{id}", response_model=Flight)
async def update_flight( async def update_flight(
id: int, id: int,
@ -49,9 +66,9 @@ async def update_flight(
req: Request, req: Request,
authorization: Annotated[str | None, Header()] = None, 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 = flight_update.model_dump()
update["user_id"] = user_id update["user_id"] = authData["id"]
request_id = req.state.request_id request_id = req.state.request_id
header = {"x-api-request-id": request_id} header = {"x-api-request-id": request_id}
(response, status, _) = await request( (response, status, _) = await request(
@ -62,7 +79,7 @@ async def update_flight(
return response return response
@router.get("", response_model=list[Flight]) @router.get("", response_model=list[FlightFull])
async def get_flights( async def get_flights(
req: Request, req: Request,
res: Response, res: Response,

View File

@ -22,6 +22,25 @@ async def create_users(user: UserRegister, req: Request):
return response 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) @router.get("/{id}", response_model=User)
async def get_user( async def get_user(
id: str, req: Request, authorization: Annotated[str | None, Header()] = None id: str, req: Request, authorization: Annotated[str | None, Header()] = None

View File

@ -21,6 +21,24 @@ class Flight(BaseModel):
return value 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): class FlightCreate(BaseModel):
flight_code: str flight_code: str
status: str status: str

View File

@ -6,7 +6,7 @@ class User(BaseModel):
username: str username: str
email: str email: str
created_date: str created_date: str
airline: bool role: str
class UserMin(BaseModel): class UserMin(BaseModel):
@ -17,7 +17,7 @@ class UserMin(BaseModel):
class UserStatus(BaseModel): class UserStatus(BaseModel):
id: int id: int
airline: bool role: str
class UserRegister(BaseModel): class UserRegister(BaseModel):

4
run.sh
View File

@ -221,12 +221,16 @@ elif [ -n "$domain" ] && [ -z "$down" ]; then
elif [ -n "$down" ]; then elif [ -n "$down" ]; then
export API_IMAGE=$USER/flights-information: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.dev.yml --env-file flights-domain/.env.dev down
export API_IMAGE=$USER/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.dev.yml --env-file auth-domain/.env.dev down
export API_IMAGE=$USER/subs-manager:prod 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.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 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.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 export CLIENT_IMAGE=$USER/screen-client:prod
docker compose -f screen-domain/docker-compose.yml down docker compose -f screen-domain/docker-compose.yml down