Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User authentication and different accounts #48

Draft
wants to merge 25 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
27870bb
Add users table, extend database patching
pbogre Sep 20, 2024
628583d
Base user authentication router
pbogre Sep 20, 2024
81f42a2
Create default user, simple user methods, add 'is_admin' field to users
pbogre Sep 20, 2024
f374049
Get current user functionality, improve user update method
pbogre Sep 20, 2024
1c8d3f4
Merge with database provider update
pbogre Sep 22, 2024
534e532
Merge branch 'main' of https://github.com/pbogre/jetlog into auth
pbogre Sep 24, 2024
8a85948
Restrict jetlog to authenticated users, Login page
pbogre Oct 6, 2024
396e3b8
Add user_id field to flight model
pbogre Oct 7, 2024
22513c1
(refactor) Use different Flight Model for patches
pbogre Oct 7, 2024
aef5a08
Make flights only accessible to associated user
pbogre Oct 7, 2024
5aebff6
Merge branch 'main' of https://github.com/pbogre/jetlog into auth
pbogre Oct 7, 2024
3527171
Disable all wrapping in flights table
pbogre Oct 7, 2024
8a0e7a4
UI to edit own user, login page improvements
pbogre Oct 15, 2024
03a815d
Restructure users API endpoints
pbogre Oct 16, 2024
5eb7b6e
Use new user endpoints in frontend
pbogre Oct 16, 2024
0f26e6a
UI user management, minor changes and bug fixes
pbogre Oct 16, 2024
ee41a5c
Merge latest main commits
pbogre Oct 30, 2024
c7f1e84
Ability to delete users and their flights
pbogre Oct 30, 2024
7e6f211
Ability to view other users' flights and statistics
pbogre Oct 31, 2024
3114fb0
Display flight owner on single flight view, fix GET flights endpoint
pbogre Oct 31, 2024
c8bba2e
Adapt importing/exporting to support users
pbogre Oct 31, 2024
6c31f53
Use username instead of id as unique identifier for users
pbogre Nov 11, 2024
7dc50e8
Only show edit/delete buttons if flight is your own
pbogre Nov 11, 2024
e14387b
Separation between auth and users endpoints in API
pbogre Nov 11, 2024
46a1a2b
Frontend support for API refactor
pbogre Nov 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ uvicorn = {extras = ["standard"], version = "0.23.2"}
pydantic = "2.3.0"
requests = "2.31.0"
python-multipart = "0.0.9"
pyjwt = "2.9.0"
bcrypt = "4.2.0"

[requires]
python_version = "3.11"
Expand Down
563 changes: 317 additions & 246 deletions Pipfile.lock

Large diffs are not rendered by default.

26 changes: 16 additions & 10 deletions client/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';

import Login from './pages/Login';
import New from './pages/New';
import Home from './pages/Home'
import AllFlights from './pages/AllFlights'
Expand All @@ -12,18 +13,23 @@ import Navbar from './components/Navbar';
export function App() {
return (
<BrowserRouter>
<Navbar />

<div className="p-3 overflow-x-auto">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/new" element={<New />} />
<Route path="/flights" element={<AllFlights />} />
<Route path="/statistics" element={<Statistics />} />
<Route path="/settings" element={<Settings />} />
<Route path="/login" element={<Login />} />
<Route element={
<>
<Navbar />
<div className="h-full p-4 overflow-x-auto">
<Outlet />
</div>
</>}>
<Route path="/" element={<Home />} />
<Route path="/new" element={<New />} />
<Route path="/flights" element={<AllFlights />} />
<Route path="/statistics" element={<Statistics />} />
<Route path="/settings" element={<Settings />} />
</Route>
</Routes>
</div>

</BrowserRouter>
);
}
34 changes: 28 additions & 6 deletions client/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import axios, {Axios} from 'axios';
import TokenStorage from './storage/tokenStorage';


// TODO improve this because there's a lot of repetition (get, post, delete are pretty much exactly the same)
// perhaps one method for each endpoint? i.e. API.getFlights(), ...
class APIClass {
private client: Axios;

Expand All @@ -10,11 +12,35 @@ class APIClass {
baseURL: "/api/",
timeout: 10000
})

// use token for authorization header
this.client.interceptors.request.use(
(config) => {
if (config.url !== "/api/auth/token") {
const token = TokenStorage.getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
}

return config;
},
(error) => {
return Promise.reject(error);
}
)
}

private handleError(err: any) {
if(err.response) {
alert("Bad response: " + err.response.data.detail);
if (err.response) {
if (err.response.status === 401) {
if (window.location.pathname !== "/login") {
window.location.href = "/login";
}
}
else {
alert("Bad response: " + err.response.data.detail);
}
}
else if (err.request) {
alert("Bad request: " + err.request);
Expand All @@ -23,10 +49,6 @@ class APIClass {
alert("Unknown error: " + err);
}
}

// TODO these functions are literally all the same



async get(endpoint: string, parameters: Object = {}) {
endpoint = endpoint.trim();
Expand Down
2 changes: 1 addition & 1 deletion client/components/AirportInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function AirportInput({ name, placeholder }: AirportInputProps) {

if (value.length > 1) {
API.get(`/airports?q=${value}`)
.then((data) => setAirportsData(data))
.then((data: Airport[]) => setAirportsData(data))
}
else setAirportsData([]);
}
Expand Down
22 changes: 13 additions & 9 deletions client/components/Elements.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {ChangeEvent} from 'react';
import React, {ChangeEvent, useState} from 'react';

interface HeadingProps {
text: string;
Expand Down Expand Up @@ -71,7 +71,7 @@ export function Button({ text,
};

return (
<button type={submit ? "submit": undefined}
<button type={submit ? "submit": "button"}
className={`py-1 px-2 my-1 mr-1 rounded-md cursor-pointer ${colors}
disabled:opacity-60 disabled:cursor-not-allowed
${right ? "float-right" : ""}`}
Expand All @@ -83,7 +83,7 @@ export function Button({ text,
}

interface InputProps {
type: "text"|"number"|"date"|"time"|"file";
type: "text"|"password"|"number"|"date"|"time"|"file";
name?: string;
defaultValue?: string;
maxLength?: number;
Expand All @@ -99,7 +99,7 @@ export function Input({ type,
required = false,
placeholder}: InputProps) {
return (
<input className={`${type == "text" ? "w-full" : ""} px-1 mb-4 bg-white rounded-none outline-none font-mono box-border
<input className={`${type == "text" || type == "password" ? "w-full" : ""} px-1 mb-4 bg-white rounded-none outline-none font-mono box-border
placeholder:italic border-b-2 border-gray-200 focus:border-primary-400`}
type={type}
accept={type == "file" ? ".csv,.db" : undefined}
Expand Down Expand Up @@ -182,30 +182,34 @@ export function Select({name,

interface DialogProps {
title: string;
buttonLevel?: "default"|"success"|"danger";
formBody: any; // ?
onSubmit: React.FormEventHandler<HTMLFormElement>;
}
export function Dialog({ title, formBody, onSubmit }: DialogProps) {
export function Dialog({ title, buttonLevel = "default", formBody, onSubmit }: DialogProps) {
const modalId = Math.random().toString(36).slice(2, 10); // to support multiple modals in one page

const openModal = () => {
const modalElement = document.getElementById("modal") as HTMLDialogElement;
const modalElement = document.getElementById(modalId) as HTMLDialogElement;
modalElement.showModal();
}

const closeModal = () => {
const modalElement = document.getElementById("modal") as HTMLDialogElement;
const modalElement = document.getElementById(modalId) as HTMLDialogElement;
modalElement.close();
}

const handleSubmit = (event) => {
closeModal();
event.preventDefault();
onSubmit(event);
}

return (
<>
<Button text={title} onClick={openModal}/>
<Button text={title} onClick={openModal} level={buttonLevel}/>

<dialog id="modal" className="md:w-2/3 max-md:w-4/5 rounded-md">
<dialog id={modalId} className="md:w-2/3 max-md:w-4/5 rounded-md">
<form className="flex flex-col" onSubmit={handleSubmit}>

<div className="pl-5 pt-2 border-b border-b-gray-400">
Expand Down
27 changes: 19 additions & 8 deletions client/components/SingleFlight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,31 @@ import React, {useEffect, useState} from 'react';
import {useNavigate} from 'react-router-dom';

import { Button, Heading, Input, Select, Subheading, TextArea } from '../components/Elements'
import { Flight } from '../models';
import { Flight, User } from '../models';
import AirportInput from './AirportInput';

import { SettingsManager } from '../settingsManager';
import API from '../api';
import ConfigStorage from '../storage/configStorage';
import { objectFromForm } from '../utils';

export default function SingleFlight({ flightID }) {
const [flight, setFlight] = useState<Flight>();
const [selfUsername, setSelfUsername] = useState<string>();
const [editMode, setEditMode] = useState<Boolean>(false);

const navigate = useNavigate();
const metricUnits = SettingsManager.getSetting("metricUnits");
const metricUnits = ConfigStorage.getSetting("metricUnits");

useEffect(() => {
API.get(`/flights?id=${flightID}&metric=${metricUnits}`)
.then((data) => {
.then((data: Flight) => {
setFlight(data);
});

API.get("/users/me")
.then((data: User) => {
setSelfUsername(data.username);
});
}, []);

if(flight === undefined) {
Expand Down Expand Up @@ -58,7 +65,7 @@ export default function SingleFlight({ flightID }) {
return (
<>
<Heading text={`${flight.origin.iata || flight.origin.icao } to ${flight.destination.iata || flight.destination.icao}`} />
<h2 className="-mt-4 mb-4 text-xl">{flight.date}</h2>
<h2 className="-mt-4 mb-4 text-xl">{flight.username} on {flight.date}</h2>

<form onSubmit={updateFlight}>
<div className="flex flex-wrap">
Expand Down Expand Up @@ -93,7 +100,7 @@ export default function SingleFlight({ flightID }) {
</>
:
<>
<p>Distance: <span>{flight.distance ? flight.distance + (metricUnits === "false" ? " mi" : " km") : "N/A"}</span></p>
<p className="mb-2">Distance: <span>{flight.distance ? flight.distance + (metricUnits === "false" ? " mi" : " km") : "N/A"}</span></p>
<p className="font-bold">Origin</p>
<ul className="mb-2">
<li>ICAO/IATA: <span>{flight.origin.icao}/{flight.origin.iata}</span></li>
Expand Down Expand Up @@ -152,8 +159,12 @@ export default function SingleFlight({ flightID }) {
level="success"
submit/>
}
<Button text={editMode ? "Cancel" : "Edit" } level="default" onClick={toggleEditMode} />
<Button text="Delete" level="danger" onClick={deleteFlight} />
{ selfUsername === flight.username &&
<>
<Button text={editMode ? "Cancel" : "Edit" } level="default" onClick={toggleEditMode} />
<Button text="Delete" level="danger" onClick={deleteFlight} />
</>
}
</form>
</>
);
Expand Down
10 changes: 5 additions & 5 deletions client/components/Stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, {useState, useMemo, useEffect} from 'react';

import { Subheading, Whisper } from './Elements';
import { Statistics } from '../models';
import { SettingsManager } from '../settingsManager';
import ConfigStorage from '../storage/configStorage';
import API from '../api';

function StatBox({stat, description}) {
Expand All @@ -16,12 +16,12 @@ function StatBox({stat, description}) {

export function ShortStats() {
const [statistics, setStatistics] = useState<Statistics>()
const metricUnits = SettingsManager.getSetting("metricUnits");
const metricUnits = ConfigStorage.getSetting("metricUnits");

// runs before render
useMemo(() => {
API.get(`/statistics?metric=${metricUnits}`)
.then((data) => {
.then((data: Statistics) => {
setStatistics(data);
});
}, []);
Expand Down Expand Up @@ -77,11 +77,11 @@ function StatFrequency({ object, measure }) {

export function AllStats({ filters }) {
const [statistics, setStatistics] = useState<Statistics>()
const metricUnits = SettingsManager.getSetting("metricUnits");
const metricUnits = ConfigStorage.getSetting("metricUnits");

useEffect(() => {
API.get(`/statistics?metric=${metricUnits}`, filters)
.then((data) => {
.then((data: Statistics) => {
setStatistics(data);
});
}, [filters]);
Expand Down
26 changes: 26 additions & 0 deletions client/components/UserSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { useState, useEffect } from 'react';

import { Select } from './Elements';

import API from '../api';

export default function UserSelect() {
const [users, setUsers] = useState<string[]>([]);

useEffect(() => {
API.get("/users")
.then((res) => {
setUsers(res)
});
}, [])

return (
<Select name="username" options={[
{ text: "You", value: "" },
...users.map((username) => ({
text: username,
value: username
}))
]}/>
)
}
23 changes: 15 additions & 8 deletions client/components/WorldMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,34 @@ import React, {useState, useEffect} from 'react';
import { ComposableMap, ZoomableGroup, Geographies, Geography, Marker, Line } from "react-simple-maps";

import API from '../api';
import { SettingsManager } from '../settingsManager';
import ConfigStorage from '../storage/configStorage';
import { Coord, Trajectory } from '../models';

export default function WorldMap() {
const geoUrl = "/api/geography/world";
const [world, setWorld] = useState<object>()
const [markers, setMarkers] = useState<Coord[]>([])
const [lines, setLines] = useState<Trajectory[]>([])

useEffect(() => {
API.get("/geography/world")
.then((data) => setWorld(data));

API.get("/geography/markers")
.then((data) => setMarkers(data));
.then((data: Coord[]) => setMarkers(data));

API.get("/geography/lines")
.then((data) => setLines(data));
.then((data: Trajectory[]) => setLines(data));
}, []);

if (world === undefined) {
return;
}

return (
<>
<ComposableMap width={1000} height={470}>
<ZoomableGroup maxZoom={10} center={[0, 0]} translateExtent={[[0, 0], [1000, 470]]}>
<Geographies geography={geoUrl}>
<Geographies geography={world}>
{({ geographies }) =>
geographies.map((geo) => (
<Geography
Expand All @@ -41,7 +48,7 @@ export default function WorldMap() {
to={[line.second.longitude, line.second.latitude]}
stroke="#FF5533CC"
strokeWidth={
SettingsManager.getSetting("frequencyBasedLine") === "true" ?
ConfigStorage.getSetting("frequencyBasedLine") === "true" ?
Math.min(1 + Math.floor(line.frequency / 3), 6)
: 1
}
Expand All @@ -51,12 +58,12 @@ export default function WorldMap() {
{ markers.map((marker) => (
<Marker coordinates={[marker.longitude, marker.latitude]}>
<circle r={
SettingsManager.getSetting("frequencyBasedMarker") === "true" ?
ConfigStorage.getSetting("frequencyBasedMarker") === "true" ?
Math.min(3 + Math.floor(marker.frequency / 3), 6)
: 3
}
fill={
SettingsManager.getSetting("frequencyBasedMarker") === "true" ?
ConfigStorage.getSetting("frequencyBasedMarker") === "true" ?
"#FFA50080"
: "#FFA500"
}
Expand Down
4 changes: 4 additions & 0 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,9 @@
* {
transition-duration: 200ms;
}

html, body, #app {
height: 100%;
}
</style>
</html>
Loading