Add subscribe/unsuscribe logic in browser-domain

Also, fix some backend bugs
This commit is contained in:
Santiago Lo Coco 2023-12-01 19:30:41 -03:00
parent 6c4aa99eb7
commit dc7c6f7439
14 changed files with 404 additions and 34 deletions

View File

@ -1,5 +1,5 @@
import { Axios, AxiosError } from "axios";
import { Credentials, Token, User, Flight, FlightCreate } from "./Types";
import { Credentials, Token, User, Flight, FlightCreate, SubscriptionsCreate } from "./Types";
const instance = new Axios({
baseURL: process.env.REACT_APP_ENDPOINT ? process.env.REACT_APP_ENDPOINT : "http://127.0.0.1:5000/",
@ -24,6 +24,8 @@ instance.interceptors.response.use(
json["count"] = response.headers["x-count"]
console.log(json)
return json
} else if (response.status == 204) {
return response;
}
return JSON.parse(response.data);
},
@ -75,4 +77,35 @@ export const createFlight = (
return instance.post("flights", flight_data, {
headers: { Authorization: `Bearer ${token}` },
});
};
export const subscribeToFlight = (subscription: SubscriptionsCreate, token: string): Promise<SubscriptionsCreate> => {
return instance.post("subscriptions", subscription, {
headers: { Authorization: `Bearer ${token}` },
});
};
export const getChatId = (user_id: number, token: string): Promise<Flight> => {
return instance.get("notifications?user_id=" + user_id, {
headers: { Authorization: `Bearer ${token}` },
});
};
export const getSubscription = (subscription: SubscriptionsCreate, token: string): Promise<SubscriptionsCreate> => {
return instance.get("subscriptions?user_id=" + subscription.user_id + "&flight_id=" +subscription.flight_id, {
headers: { Authorization: `Bearer ${token}` },
});
};
export const unsubscribeFromFlight = (subscription: SubscriptionsCreate, token: string): Promise<any> => {
return instance.delete("subscriptions", {
headers: { Authorization: `Bearer ${token}`},
data: subscription
});
};
export const fetchSubscriptions = (user_id: number, token: string): Promise<SubscriptionsCreate[]> => {
return instance.get("subscriptions?user_id=" + user_id, {
headers: { Authorization: `Bearer ${token}` },
});
};

View File

@ -45,4 +45,9 @@ export interface FlightCreate {
departure_time: string;
arrival_time: string;
gate: string;
}
export interface SubscriptionsCreate {
flight_id: number;
user_id: number;
}

View File

@ -1,10 +1,14 @@
import React from "react";
import { Avatar, Space, Typography, Tag } from "antd";
import React, { useEffect, useState } from "react";
import { Link } 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";
interface FlightProps {
id: number;
flight_code: string;
status: string;
origin: string;
@ -16,11 +20,110 @@ interface FlightProps {
interface CardProps {
flight: FlightProps;
user: User | undefined;
subscribed: boolean;
refresh: any;
}
const { Text } = Typography;
export const Card: React.FC<CardProps> = ({ flight }) => {
export const Card: React.FC<CardProps> = ({ flight, user, subscribed, refresh }) => {
// const [error, setError] = useState<string | null>(null);
// const [subscribed, setSubscribed] = useState<boolean>(false);
// useEffect(() => {
// setError(null);
// const token = localStorage.getItem("token");
// if (!token || !user) {
// setError("No token!");
// return;
// }
// const data = {
// user_id: user.id,
// flight_id: flight.id
// }
// getSubscription(data, token)
// .then((data) => {
// setSubscribed(true);
// })
// .catch((error) => {
// setError(error as string);
// });
// }, [user]);
const [modalVisible, setModalVisible] = useState<boolean>(false);
const handleSubscribe = async (event: React.FormEvent) => {
event.preventDefault();
// setError(null);
const token = localStorage.getItem("token");
if (!token || !user) {
// setError("No token!");
return;
}
const data = {
user_id: user.id,
flight_id: flight.id
}
console.log(data)
subscribeToFlight(data, token)
.then(() => {
refresh()
getChatId(user.id, token)
.then(() => {})
.catch((error) => {
console.log("NO CHAT")
setModalVisible(true);
// setError(error as string);
})
})
.catch((error) => {
// setError(error as string);
});
};
const handleModalClose = () => {
setModalVisible(false);
};
const handleUnsubscribe = async (event: React.FormEvent) => {
event.preventDefault();
// setError(null);
const token = localStorage.getItem("token");
if (!token || !user) {
// setError("No token!");
return;
}
const data = {
user_id: user.id,
flight_id: flight.id
}
console.log(data)
unsubscribeFromFlight(data, token)
.then(() => {
console.log("?")
refresh()
})
.catch((error) => {
// setError(error as string);
});
};
console.log(subscribed)
return (
<div className="flight-card">
<Space size={8} align="center">
@ -32,7 +135,7 @@ export const Card: React.FC<CardProps> = ({ flight }) => {
</div>
</Space>
<div className="flight-details">
<Space size={8} direction="vertical">
<Space size={8} direction="horizontal">
<Text strong>Status:</Text>
<Tag color={flight.status === "Scheduled" ? "green" : "orange"}>{flight.status}</Tag>
</Space>
@ -50,11 +153,41 @@ export const Card: React.FC<CardProps> = ({ flight }) => {
{flight.arrival_time}
</Space>
</Space>
<Space size={8} direction="vertical">
<Space size={8} direction="horizontal">
<Text strong>Gate:</Text>
<Text>{flight.gate}</Text>
</Space>
<Space size={8} direction="horizontal">
<Text strong>ID:</Text>
<Text>{flight.id}</Text>
</Space>
</div>
{!(subscribed) ?
<Button type="primary" onClick={handleSubscribe}>
Subscribe
</Button>
:
<Button type="primary" onClick={handleUnsubscribe}>
Unsubscribe
</Button>
}
<Modal
title="Error"
visible={modalVisible}
onCancel={handleModalClose}
footer={[
<Button key="ok" type="primary" onClick={handleModalClose}>
OK
</Button>
]}
>
<p>To start receiving messages open this link on your smartphone:
</p>
<Link to={`https://t.me/fids_system_bot?start=${user?.id}`} target="_blank">
{`https://t.me/fids_system_bot?start=${user?.id}`}
</Link>
</Modal>
</div>
);
};

View File

@ -1,9 +1,11 @@
import React, { useEffect, useState } from "react";
import { Card } from "./Card/Card";
import { useFetchFlights } from "../../hooks/useFetchFlights";
import { Flight } from "../../Types";
import { Flight, SubscriptionsCreate } from "../../Types";
import { useNavigate } from "react-router";
import useAuth from "../../useAuth";
import { useFetchSubscriptions } from "../../hooks/useFetchSubscriptions";
import { fetchSubscriptions } from "../../Api";
interface Props {
flights?: Flight[];
@ -17,7 +19,9 @@ export const Home: React.FC<Props> = (props) => {
const navigate = useNavigate()
const [currentPage, setCurrentPage] = useState(initialPage);
const { loading, isAirline } = useAuth();
const { loading, isAirline, user, token } = useAuth();
const { subscriptions, loading: subsLoading, fetchData } = useFetchSubscriptions(user, token);
useEffect(() => {
const newParams = new URLSearchParams(window.location.search);
@ -25,6 +29,25 @@ export const Home: React.FC<Props> = (props) => {
navigate(`?${newParams.toString()}`);
}, [currentPage, navigate]);
// const [errorSub, setErrorSub] = useState<string | null>(null);
// const [subscriptions, setSubscriptions] = useState<SubscriptionsCreate[]>([]);
// useEffect(() => {
// setErrorSub(null);
// console.log(user)
// console.log(token)
// if (!user || !token) {
// return;
// }
// fetchSubscriptions(user.id, token)
// .then((data) => {
// setSubscriptions(data);
// })
// .catch((error) => { });
// }, [user, token, loading]);
const goToPrevPage = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
@ -47,13 +70,15 @@ export const Home: React.FC<Props> = (props) => {
return <div>Loading...</div>;
}
// console.log(subscriptions)
return (
<div className="Box">
{isAirline ? <button onClick={() => { navigate("/create-flight") }}>Create flight</button> : <></>}
<h2>Flights</h2>
<div className="Items">
{(props.flights ? props.flights : flights).map((u) => {
return <Card key={u.id} flight={u} />;
{(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} />;
})}
{error ? <div className="Disconnected">{error}</div> : <></>}
</div>

View File

@ -8,6 +8,7 @@ export const LogIn = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const navigate = useNavigate();
console.log(error)
return (
<div className="Box Small">
@ -39,7 +40,7 @@ export const LogIn = () => {
Sign up
</Button>
{error ? (
<div className="Disconnected">{error}</div>
<div className="Disconnected">{error?.message}</div>
) : (
<></>
)}

View File

@ -0,0 +1,46 @@
import React, { useEffect, useCallback } from "react";
import { useState } from "react";
import { User, Flight, SubscriptionsCreate } from "../Types";
import { fetchSubscriptions } from "../Api";
export const useFetchSubscriptions = (user: User | undefined, token: string | undefined) => {
const [error, setError] = useState<string | null>(null);
const [subscriptions, setSubscriptions] = useState<SubscriptionsCreate[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const fetchData = useCallback(async () => {
setError(null);
if (!user || !token || !loading) {
return;
}
fetchSubscriptions(user.id, token)
.then((data) => {
setSubscriptions(data);
setLoading(false)
})
.catch((error) => { });
}, [user, token]);
useEffect(() => {
fetchData()
}, [fetchData]);
// useEffect(() => {
// setError(null);
// if (!user || !token || !loading) {
// return;
// }
// fetchSubscriptions(user.id, token)
// .then((data) => {
// setSubscriptions(data);
// setLoading(false)
// })
// .catch((error) => { });
// }, [user, token]);
return { subscriptions, error, loading, fetchData };
};

View File

@ -8,6 +8,7 @@ interface AuthContextType {
user?: User;
loading: boolean;
isAirline: boolean;
token?: string;
error?: any;
login: (credentials: Credentials) => void;
signUp: (email: string, name: string, password: string) => void;
@ -25,6 +26,7 @@ export function AuthProvider({
}): JSX.Element {
const [user, setUser] = useState<User>();
const [error, setError] = useState<any>();
const [token, setToken] = useState<string>();
const [loading, setLoading] = useState<boolean>(false);
const [loadingInitial, setLoadingInitial] = useState<boolean>(true);
const [isAirline, setIsAirline] = useState(false);
@ -51,7 +53,10 @@ export function AuthProvider({
.then((res) => fetchUserById(res.id, existingToken)
.then((res) => setUser(res))
.catch((_error) => { })
.finally(() => setLoadingInitial(false))
.finally(() => {
setToken(existingToken)
setLoadingInitial(false)
})
)
.catch((_error) => {
setLoadingInitial(false)
@ -73,6 +78,7 @@ export function AuthProvider({
const user = fetchUserById(x.user_id as number, x.access_token)
.then(y => {
setUser(y);
setToken(x.access_token)
navigate("/home")
})
.catch((error) => setError(error))
@ -87,6 +93,7 @@ export function AuthProvider({
function logout() {
localStorage.removeItem("token");
setUser(undefined);
setToken(undefined)
navigate("/login")
}
@ -95,6 +102,7 @@ export function AuthProvider({
user,
loading,
isAirline,
token,
error,
login,
signUp,

View File

@ -49,9 +49,9 @@ async def update_flight(
req: Request,
authorization: Annotated[str | None, Header()] = None,
):
id = await checkAuth(req, authorization, isAirline=True)
user_id = await checkAuth(req, authorization, isAirline=True)
update = flight_update.model_dump()
update["user_id"] = id
update["user_id"] = user_id
request_id = req.state.request_id
header = {"x-api-request-id": request_id}
(response, status, _) = await request(

View File

@ -1,7 +1,10 @@
from typing import Annotated
from asyncreq import request
from fastapi import APIRouter, HTTPException, Request
from fastapi import APIRouter, Header, HTTPException, Request
from src.api.config import API_NOTIFICATIONS
from src.api.routes.auth import checkAuth
from src.api.schemas.notification import Update as Message
router = APIRouter()
@ -18,3 +21,22 @@ async def receive_message(message: Message, req: Request):
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response
@router.get("")
async def get_chat_by_user_id(
req: Request,
user_id: int,
authorization: Annotated[str | None, Header()] = None,
):
await checkAuth(req, authorization)
query = {}
query["user_id"] = user_id
request_id = req.state.request_id
header = {"x-api-request-id": request_id}
(response, status, _) = await request(
f"{API_NOTIFICATIONS}", "GET", query=query, headers=header
)
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response

View File

@ -1,4 +1,4 @@
from typing import Annotated
from typing import Annotated, Optional
from asyncreq import request
from fastapi import APIRouter, Header, HTTPException, Request
@ -25,3 +25,42 @@ async def create_subscription(
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response
@router.delete("")
async def delete_subscription(
subscription: Subscription,
req: Request,
authorization: Annotated[str | None, Header()] = None,
):
await checkAuth(req, authorization)
request_id = req.state.request_id
header = {"x-api-request-id": request_id}
(response, status, _) = await request(
f"{API_SUBSCRIPTIONS}", "DELETE", json=subscription.model_dump(), headers=header
)
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response
@router.get("")
async def get_subscriptions(
req: Request,
user_id: int,
flight_id: Optional[int] = None,
authorization: Annotated[str | None, Header()] = None,
):
await checkAuth(req, authorization)
query = {}
query["user_id"] = user_id
if flight_id:
query["flight_id"] = flight_id
request_id = req.state.request_id
header = {"x-api-request-id": request_id}
(response, status, _) = await request(
f"{API_SUBSCRIPTIONS}", "GET", query=query, headers=header
)
if status < 200 or status > 204:
raise HTTPException(status_code=status, detail=response)
return response

View File

@ -9,7 +9,20 @@ def get_subscriptions(db: Session, user_id: int):
return db.query(Subscription).filter(Subscription.user_id == user_id).all()
def get_subscription(db: Session, user_id: int, flight_id: int):
return (
db.query(Subscription)
.filter(Subscription.user_id == user_id, Subscription.flight_id == flight_id)
.first()
)
def create_subscription(db: Session, subscription: SubscriptionPydantic):
if get_subscription(
db, user_id=subscription.user_id, flight_id=subscription.flight_id
):
raise ValueError
db_subscription = Subscription(
user_id=subscription.user_id,
flight_id=subscription.flight_id,
@ -22,7 +35,7 @@ def create_subscription(db: Session, subscription: SubscriptionPydantic):
def remove_subscription(db: Session, user_id: int, flight_id: int):
db.query(Subscription).filter(
Subscription.user_id == user_id and Subscription.flight_id == flight_id
Subscription.user_id == user_id, Subscription.flight_id == flight_id
).delete()
db.commit()

View File

@ -1,7 +1,7 @@
import re
from asyncreq import request
from fastapi import APIRouter, BackgroundTasks, Depends, Response
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Response
from sqlalchemy.orm import Session
from src.api.config import API_FLIGHTS
@ -10,11 +10,15 @@ from src.api.cruds import subscription as subs_crud
from src.api.db import get_db
from src.api.schemas.chat import Chat, Update
from src.api.utils import telegram
from src.api.utils.messages import get_flight_message, get_invalid_message
from src.api.utils.messages import (
get_flight_message,
get_invalid_message,
get_start_message,
)
router = APIRouter()
msg_options = re.compile(r"^/(flight \d+|stop|start)$")
msg_options = re.compile(r"^/(flight \d+|stop|start \d+)$")
@router.post("")
@ -33,8 +37,11 @@ async def create_chat(
action = text.partition(" ")[0]
if action == "/start":
user_id = int(message["text"].partition(" ")[2])
new_chat = Chat(chat_id=str(message["chat"]["id"]), user_id=user_id)
chat_id = str(message["chat"]["id"])
new_chat = Chat(chat_id=chat_id, user_id=user_id)
notif_crud.create_chat(db=db, chat=new_chat)
msg = get_start_message()
background_tasks.add_task(telegram.send_message, chat_id, msg)
elif action == "/stop":
chat_id = str(message["chat"]["id"])
user_id = notif_crud.get_user_from_chat(db=db, chat_id=chat_id).user_id
@ -50,3 +57,11 @@ async def create_chat(
background_tasks.add_task(telegram.send_message, chat_id, msg)
return Response(status_code=204)
@router.get("")
def get_chat_by_user_id(user_id: int, db: Session = Depends(get_db)):
db_chat = notif_crud.get_chat_id(db=db, user_id=user_id)
if db_chat is None:
raise HTTPException(status_code=404, detail="Chat not found")
return db_chat

View File

@ -1,31 +1,52 @@
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Response
from sqlalchemy.orm import Session
from src.api.cruds import subscription as sub_crud
from src.api.db import get_db
from src.api.schemas.subscription import Subscription, SubscriptionRemove
from src.api.schemas.subscription import Subscription
router = APIRouter()
@router.post("")
def create_subscription(subscription: Subscription, db: Session = Depends(get_db)):
return sub_crud.create_subscription(db=db, subscription=subscription)
try:
db_subscription = sub_crud.create_subscription(db=db, subscription=subscription)
except ValueError:
raise HTTPException(status_code=409, detail="User already suscribed")
# if notif_crud.get_chat_id(subscription.user_id) is None:
# raise HTTPException(status_code=424, detail="First you need to create a chat")
return db_subscription
@router.get("/{user_id}", response_model=list[Subscription])
def get_subscriptions(user_id: int, db: Session = Depends(get_db)):
db_subscriptions = sub_crud.get_subscriptions(db=db, user_id=user_id)
# @router.get("/{user_id}", response_model=list[Subscription])
# def get_subscriptions(user_id: int, db: Session = Depends(get_db)):
# db_subscriptions = sub_crud.get_subscriptions(db=db, user_id=user_id)
# if db_subscriptions is None:
# raise HTTPException(status_code=404, detail="Subscription not found")
# return db_subscriptions
@router.get("")
def get_subscription(
user_id: int, flight_id: Optional[int] = None, db: Session = Depends(get_db)
):
if flight_id:
db_subscriptions = sub_crud.get_subscription(
db=db, user_id=user_id, flight_id=flight_id
)
else:
db_subscriptions = sub_crud.get_subscriptions(db=db, user_id=user_id)
if db_subscriptions is None:
raise HTTPException(status_code=404, detail="Subscription not found")
return db_subscriptions
@router.delete("/{user_id}")
def delete_subscription(
user_id: int, subscription: SubscriptionRemove, db: Session = Depends(get_db)
):
@router.delete("")
def delete_subscription(subscription: Subscription, db: Session = Depends(get_db)):
sub_crud.remove_subscription(
db=db, user_id=user_id, flight_id=subscription.flight_id
db=db, user_id=subscription.user_id, flight_id=subscription.flight_id
)
return Response(status_code=204)

View File

@ -30,7 +30,16 @@ def get_flight_message(flight: dict):
def get_invalid_message():
return (
"Invalid option!\nPlease use:\n"
"\n/flights NUMBER (e.g., /flights 1) for flight details"
"\n/start to start receiving messages"
"\n/stop to manage updates."
"\n/flight NUMBER (e.g., /flight 1) for flight details"
# "\n/start to start receiving messages"
"\n/stop to stop receiving updates."
)
def get_start_message():
return (
"Thanks for using fids! You will now start getting updates from your subscriptions!\n"
"Meanwhile you can type:\n"
"\n/flight NUMBER (e.g., /flight 1) for flight details"
"\n/stop to stop receiving updates."
)