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 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:

View File

@ -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(

View File

@ -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)
@ -53,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 = {
@ -81,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 = {
@ -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")

View File

@ -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

View File

@ -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),
},
)

View File

@ -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!",
@ -88,7 +93,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

View File

@ -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"] == "user"
assert "password" not in data

View File

@ -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

View File

@ -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/",
@ -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<User & { message?: string }> => {
return instance.get("users/" + id, {
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> => {
return instance.post("subscriptions", subscription, {
headers: { Authorization: `Bearer ${token}` },

View File

@ -1,21 +1,25 @@
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";
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 (
<div className="App">
<Routes>
<Route path="/login" element={<LogIn />} />
<Route path="/signup" element={<SignUp />} />
<Route path="/create-airline" element={!isAdmin ? <LogIn /> : <CreateAirline />} />
<Route path="/home" element={!user ? <LogIn /> : <Home />} />
<Route path="/create-flight" element={!isAirline ? <LogIn /> : <CreateFlight />} />
<Route path="/edit-flight/:id" element={!isAirline && !isAdmin ? <LogIn /> : <EditFlight />} />
<Route path="/" element={!user ? <LogIn /> : <Home />} />
</Routes>
<div className="LogoutButton">

View File

@ -11,7 +11,7 @@ export interface Token {
export interface TokenData {
sub: string;
airline: boolean;
role: string;
}
export interface User {
@ -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;

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 { 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;
isAdmin: boolean;
refreshFlights: any;
}
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 navigate = useNavigate();
const handleSubscribe = async (event: React.FormEvent) => {
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 = () => {
setModalVisible(false);
};
@ -83,10 +113,12 @@ export const Card: React.FC<CardProps> = ({ flight, user, subscribed, refresh })
refresh()
})
.catch((error) => {
console.log(error)
});
};
console.log(subscribed)
console.log(flight.user_id)
console.log(user?.id)
return (
<div className="flight-card">
@ -126,14 +158,31 @@ export const Card: React.FC<CardProps> = ({ flight, user, subscribed, refresh })
<Text>{flight.id}</Text>
</Space>
</div>
{!(subscribed) ?
<Button type="primary" onClick={handleSubscribe}>
{!isAirline && !isAdmin ?
(
!(subscribed) ?
<Button type="primary" onClick={handleSubscribe}>
Subscribe
</Button>
:
<Button type="primary" onClick={handleUnsubscribe}>
Unsubscribe
</Button>
</Button>
:
<Button type="primary" onClick={handleUnsubscribe}>
Unsubscribe
</Button>
)
:
(
(user && flight.user_id == user.id) || isAdmin ?
<>
<Button type="primary" onClick={handleEdit}>
Edit
</Button>
<Button type="primary" onClick={handleDelete}>
Delete
</Button>
</>
:
<></>
)
}
<Modal
title="Error"

View File

@ -14,11 +14,11 @@ export const Home: React.FC<Props> = (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);
const { loading, isAirline, user, token } = useAuth();
const { loading, isAirline, user, token, isAdmin } = useAuth();
const { subscriptions, loading: subsLoading, fetchData } = useFetchSubscriptions(user, token);
@ -53,10 +53,13 @@ export const Home: React.FC<Props> = (props) => {
return (
<div className="Box">
{isAirline ? <button onClick={() => { navigate("/create-flight") }}>Create flight</button> : <></>}
{isAdmin ? <button onClick={() => { navigate("/create-airline") }}>Create airline user</button> : <></>}
<h2>Flights</h2>
<div className="Items">
{(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> : <></>}
</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 { 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<Flight[]>([]);
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);
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 };
};

View File

@ -8,6 +8,7 @@ interface AuthContextType {
user?: User;
loading: boolean;
isAirline: boolean;
isAdmin: boolean;
token?: string;
error?: any;
login: (credentials: Credentials) => void;
@ -30,6 +31,7 @@ export function AuthProvider({
const [loading, setLoading] = useState<boolean>(false);
const [loadingInitial, setLoadingInitial] = useState<boolean>(true);
const [isAirline, setIsAirline] = useState(false);
const [isAdmin, setIsAdmin] = useState(false);
const navigate = useNavigate();
useEffect(() => {
@ -39,10 +41,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 +76,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);
@ -102,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 (

View File

@ -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()

View File

@ -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"],
)

View File

@ -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
@ -62,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["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

View File

@ -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,
@ -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(
@ -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,
@ -49,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(
@ -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,

View File

@ -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

View File

@ -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

View File

@ -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):

4
run.sh
View File

@ -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