From 27870bb5db9f825ac5f2c024aac734819693cd79 Mon Sep 17 00:00:00 2001 From: Pietro Bonaldo Date: Fri, 20 Sep 2024 13:59:51 +0200 Subject: [PATCH 01/21] Add users table, extend database patching --- server/database.py | 102 +++++++++++++++++++++++++++++---------------- server/models.py | 6 +++ 2 files changed, 72 insertions(+), 36 deletions(-) diff --git a/server/database.py b/server/database.py index 10c4b2a..1c5ebad 100644 --- a/server/database.py +++ b/server/database.py @@ -3,28 +3,44 @@ from pathlib import Path from fastapi import HTTPException -from server.models import FlightModel +from server.models import FlightModel, User from server.environment import DATA_PATH class Database(): connection: sqlite3.Connection - flights_table = """ - ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - date TEXT NOT NULL, - origin TEXT NOT NULL, - destination TEXT NOT NULL, - departure_time TEXT, - arrival_time TEXT, - arrival_date TEXT, - seat TEXT NULL CHECK(seat IN ('aisle', 'middle', 'window')), - ticket_class TEXT NULL CHECK(ticket_class IN ('private', 'first', 'business', 'economy+', 'economy')), - duration INTEGER, - distance INTEGER, - airplane TEXT, - flight_number TEXT, - notes TEXT - )""" + tables = { + "flights": { + "pragma": """ + ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + origin TEXT NOT NULL, + destination TEXT NOT NULL, + departure_time TEXT, + arrival_time TEXT, + arrival_date TEXT, + seat TEXT NULL CHECK(seat IN ('aisle', 'middle', 'window')), + ticket_class TEXT NULL CHECK(ticket_class IN ('private', 'first', 'business', 'economy+', 'economy')), + duration INTEGER, + distance INTEGER, + airplane TEXT, + flight_number TEXT, + notes TEXT + )""", + "model": FlightModel + }, + "users": { + "pragma": """ + ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + password_hash TEXT NOT NULL, + last_login DATETIME, + created_on DATETIME NOT NULL DEFAULT current_timestamp + )""", + "model": User + } + } def __init__(self, db_dir: str): print("Initializing database connection") @@ -34,19 +50,28 @@ def __init__(self, db_dir: str): if os.path.isfile(db_path): self.connection = sqlite3.connect(db_path) - # verify that all fields are in the table + # verify that all tables are up-to-date # (backward compatibility) - table_info = self.execute_read_query("PRAGMA table_info(flights);") - column_names = [ col[1] for col in table_info ] + for table in self.tables: + table_info = self.execute_read_query(f"PRAGMA table_info({table});") + column_names = [ col[1] for col in table_info ] - needs_patch = False - for key in FlightModel.get_attributes(): - if key not in column_names: - print(f"Detected missing column in flights table: '{key}'. Scheduled a patch...") - needs_patch = True + table_pragma = self.tables[table]["pragma"] + table_model = self.tables[table]["model"] - if needs_patch: - self.patch_flights_table(column_names) + if not column_names: + print(f"Missing table '{table}'. Creating it...") + self.execute_query(f"CREATE TABLE {table} {table_pragma};") + continue + + needs_patch = False + for key in table_model.get_attributes(): + if key not in column_names: + print(f"Detected missing column in table '{table}': '{key}'. Scheduled a patch...") + needs_patch = True + + if needs_patch: + self.patch_table(table, column_names) else: print("Database file not found, creating it...") @@ -68,8 +93,10 @@ def __init__(self, db_dir: str): def initialize_tables(self): airports_db_path = Path(__file__).parent.parent / 'data' / 'airports.db' - - self.execute_query(f"CREATE TABLE flights {self.flights_table};") + + for table in self.tables: + table_pragma = self.tables[table]["pragma"] + self.execute_query(f"CREATE TABLE {table} {table_pragma};") self.execute_query(""" CREATE TABLE airports ( @@ -86,12 +113,15 @@ def initialize_tables(self): self.execute_query("INSERT INTO main.airports SELECT * FROM a.airports;") self.execute_query("DETACH a;") - def patch_flights_table(self, present: list[str]): - print("Patching flights table...") - self.execute_query(f"CREATE TABLE _flights {self.flights_table};") - self.execute_query(f"INSERT INTO _flights ({', '.join(present)}) SELECT * FROM flights;") - self.execute_query("DROP TABLE flights;") - self.execute_query("ALTER TABLE _flights RENAME TO flights;") + def patch_table(self, table: str, present: list[str]): + print(f"Patching table '{table}'...") + + table_pragma = self.tables[table]["pragma"] + + self.execute_query(f"CREATE TABLE _{table} {table_pragma};") + self.execute_query(f"INSERT INTO _{table} ({', '.join(present)}) SELECT * FROM {table};") + self.execute_query(f"DROP TABLE {table};") + self.execute_query(f"ALTER TABLE _{table} RENAME TO {table};") def execute_query(self, query: str, parameters=[]) -> int: try: diff --git a/server/models.py b/server/models.py index f35682a..d180f0d 100644 --- a/server/models.py +++ b/server/models.py @@ -58,6 +58,12 @@ def empty(self) -> bool: return True +class User(CustomModel): + username: str + password_hash: str + last_login: datetime.datetime|None + created_on: datetime.datetime + class SeatType(str, Enum): WINDOW = "window" MIDDLE = "middle" From 628583d66ea8cad9a80caca609dc80aafef63699 Mon Sep 17 00:00:00 2001 From: Pietro Bonaldo Date: Fri, 20 Sep 2024 14:53:06 +0200 Subject: [PATCH 02/21] Base user authentication router --- Pipfile | 2 + Pipfile.lock | 571 +++++++++++++++++++++++------------------ server/database.py | 2 +- server/main.py | 4 +- server/routers/auth.py | 67 +++++ 5 files changed, 398 insertions(+), 248 deletions(-) create mode 100644 server/routers/auth.py diff --git a/Pipfile b/Pipfile index 027133b..875673f 100644 --- a/Pipfile +++ b/Pipfile @@ -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" +passlib = {extras = ["bcrypt"], version = "1.7.4"} [requires] python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock index 0ea21ed..a952a8d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5ddf268ca8c49642cceb797554ea2e32aef1a3adda5c3f2393c63838fc2aa02e" + "sha256": "410ce79982cfdff73e6b831c160e26675e47f3fcb51f7f01edf897ce39e8e340" }, "pipfile-spec": 6, "requires": { @@ -32,13 +32,45 @@ "markers": "python_version >= '3.7'", "version": "==3.7.1" }, + "bcrypt": { + "hashes": [ + "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb", + "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399", + "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291", + "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d", + "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7", + "sha256:1ff39b78a52cf03fdf902635e4c81e544714861ba3f0efc56558979dd4f09170", + "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d", + "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe", + "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060", + "sha256:373db9abe198e8e2c70d12b479464e0d5092cc122b20ec504097b5f2297ed184", + "sha256:39e1d30c7233cfc54f5c3f2c825156fe044efdd3e0b9d309512cc514a263ec2a", + "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68", + "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c", + "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458", + "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9", + "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328", + "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7", + "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34", + "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e", + "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2", + "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5", + "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae", + "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00", + "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841", + "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8", + "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221", + "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db" + ], + "version": "==4.2.0" + }, "certifi": { "hashes": [ - "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", - "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" ], "markers": "python_version >= '3.6'", - "version": "==2024.7.4" + "version": "==2024.8.30" }, "charset-normalizer": { "hashes": [ @@ -204,11 +236,21 @@ }, "idna": { "hashes": [ - "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, + "passlib": { + "extras": [ + "bcrypt" + ], + "hashes": [ + "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", + "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04" ], - "markers": "python_version >= '3.5'", - "version": "==3.7" + "version": "==1.7.4" }, "pydantic": { "hashes": [ @@ -331,6 +373,15 @@ "markers": "python_version >= '3.7'", "version": "==2.6.3" }, + "pyjwt": { + "hashes": [ + "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", + "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.9.0" + }, "python-dotenv": { "hashes": [ "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", @@ -349,61 +400,61 @@ }, "pyyaml": { "hashes": [ - "sha256:0101357af42f5c9fc7e9acc5c5ab8c3049f50db7425de175b6c7a5959cb6023d", - "sha256:0ae563b7e3ed5e918cd0184060e28b48b7e672b975bf7c6f4a892cee9d886ada", - "sha256:0fe2c1c5401a3a98f06337fed48f57340cf652a685484834b44f5ceeadb772ba", - "sha256:1eb00dd3344da80264261ab126c95481824669ed9e5ecc82fb2d88b1fce668ee", - "sha256:2086b30215c433c1e480c08c1db8b43c1edd36c59cf43d36b424e6f35fcaf1ad", - "sha256:29b4a67915232f79506211e69943e3102e211c616181ceff0adf34e21b469357", - "sha256:2e9bc8a34797f0621f56160b961d47a088644370f79d34bedc934fb89e3f47dd", - "sha256:30ec6b9afc17353a9abcff109880edf6e8d5b924eb1eeed7fe9376febc1f9800", - "sha256:31573d7e161d2f905311f036b12e65c058389b474dbd35740f4880b91e2ca2be", - "sha256:36d7bf63558843ea2a81de9d0c3e9c56c353b1df8e6c1faaec86df5adedf2e02", - "sha256:3af6b36bc195d741cd5b511810246cad143b99c953b4591e679e194a820d7b7c", - "sha256:414629800a1ddccd7303471650843fc801801cc579a195d2fe617b5b455409e3", - "sha256:459113f2b9cd68881201a3bd1a858ece3281dc0e92ece6e917d23b128f0fcb31", - "sha256:46e4fae38d00b40a62d32d60f1baa1b9ef33aff28c2aafd96b05d5cc770f1583", - "sha256:4bf821ccd51e8d5bc1a4021b8bd85a92b498832ac1cd1a53b399f0eb7c1c4258", - "sha256:50bd6560a6df3de59336b9a9086cbdea5aa9eee5361661448ee45c21eeb0da68", - "sha256:53056b51f111223e603bed1db5367f54596d44cacfa50f07e082a11929612957", - "sha256:53c5f0749a93e3296078262c9acf632de246241ff2f22bbedfe49d4b55e9bbdd", - "sha256:54c754cee6937bb9b72d6a16163160dec80b93a43020ac6fc9f13729c030c30b", - "sha256:58cc18ccbade0c48fb55102aa971a5b4e571e2b22187d083dda33f8708fa4ee7", - "sha256:5921fd128fbf27ab7c7ad1a566d2cd9557b84ade130743a7c110a55e7dec3b3c", - "sha256:5c758cc29713c9166750a30156ca3d90ac2515d5dea3c874377ae8829cf03087", - "sha256:60bf91e73354c96754220a9c04a9502c2ad063231cd754b59f8e4511157e32e2", - "sha256:6f0f728a88c6eb58a3b762726b965bb6acf12d97f8ea2cb4fecf856a727f9bdc", - "sha256:6f31c5935310da69ea0efe996a962d488f080312f0eb43beff1717acb5fe9bed", - "sha256:728b447d0cedec409ea1a3f0ad1a6cc3cec0a8d086611b45f038a9230a2242f3", - "sha256:72ffbc5c0cc71877104387548a450f2b7b7c4926b40dc9443e7598fe92aa13d9", - "sha256:73d8b233309ecd45c33c51cd55aa1be1dcab1799a9e54f6c753d8cab054b8c34", - "sha256:765029d1cf96e9e761329ee1c20f1ca2de8644e7350a151b198260698b96e30f", - "sha256:7ee3d180d886a3bc50f753b76340f1c314f9e8c507f5b107212112214c3a66fd", - "sha256:826fb4d5ac2c48b9d6e71423def2669d4646c93b6c13612a71b3ac7bb345304b", - "sha256:84c39ceec517cd8f01cb144efb08904a32050be51c55b7a59bc7958c8091568d", - "sha256:88bfe675bb19ae12a9c77c52322a28a8e2a8d3d213fbcfcded5c3f5ca3ead352", - "sha256:8e0a1ebd5c5842595365bf90db3ef7e9a8d6a79c9aedb1d05b675c81c7267fd3", - "sha256:9426067a10b369474396bf57fdf895b899045a25d1848798844693780b147436", - "sha256:9c5c0de7ec50d4df88b62f4b019ab7b3bb2883c826a1044268e9afb344c57b17", - "sha256:ad0c172fe15beffc32e3a8260f18e6708eb0e15ae82c9b3f80fbe04de0ef5729", - "sha256:ad206c7f5f08d393b872d3399f597246fdc6ebebff09c5ae5268ac45aebf4f8d", - "sha256:b0a163f4f84d1e0fe6a07ccad3b02e9b243790b8370ff0408ae5932c50c4d96d", - "sha256:b0dd9c7497d60126445e79e542ff01351c6b6dc121299d89787f5685b382c626", - "sha256:b1de10c488d6f02e498eb6956b89081bea31abf3133223c17749e7137734da75", - "sha256:b408f36eeb4e2be6f802f1be82daf1b578f3de5a51917c6e467aedb46187d827", - "sha256:bae077a01367e4bf5fddf00fd6c8b743e676385911c7c615e29e1c45ace8813b", - "sha256:bc3c3600fec6c2a719106381d6282061d8c108369cdec58b6f280610eba41e09", - "sha256:c16522bf91daa4ea9dedc1243b56b5a226357ab98b3133089ca627ef99baae6f", - "sha256:ca5136a77e2d64b4cf5106fb940376650ae232c74c09a8ff29dbb1e262495b31", - "sha256:d6e0f7ee5f8d851b1d91149a3e5074dbf5aacbb63e4b771fcce16508339a856f", - "sha256:e7930a0612e74fcca37019ca851b50d73b5f0c3dab7f3085a7c15d2026118315", - "sha256:e8e6dd230a158a836cda3cc521fcbedea16f22b16b8cfa8054d0c6cea5d0a531", - "sha256:eee36bf4bc11e39e3f17c171f25cdedff3d7c73b148aedc8820257ce2aa56d3b", - "sha256:f07adc282d51aaa528f3141ac1922d16d32fe89413ee59bfb8a73ed689ad3d23", - "sha256:f09816c047fdb588dddba53d321f1cb8081e38ad2a40ea6a7560a88b7a2f0ea8", - "sha256:fea4c4310061cd70ef73b39801231b9dc3dc638bb8858e38364b144fbd335a1a" + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" ], - "version": "==6.0.2rc1" + "version": "==6.0.2" }, "requests": { "hashes": [ @@ -440,11 +491,11 @@ }, "urllib3": { "hashes": [ - "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", - "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" ], "markers": "python_version >= '3.8'", - "version": "==2.2.2" + "version": "==2.2.3" }, "uvicorn": { "extras": [ @@ -459,196 +510,224 @@ }, "uvloop": { "hashes": [ - "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd", - "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec", - "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b", - "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc", - "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797", - "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5", - "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2", - "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d", - "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be", - "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd", - "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12", - "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17", - "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef", - "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24", - "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428", - "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1", - "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849", - "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593", - "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd", - "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67", - "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6", - "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3", - "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd", - "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8", - "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7", - "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533", - "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957", - "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650", - "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e", - "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7", - "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256" + "sha256:0029380131aae418f4034520d853c85880d049eb1232214fda209a40a41c806c", + "sha256:115c90a7ef29375104b153e474c7fdf1c2bbd409f0c13ecaa823ed92b2c145e7", + "sha256:124410218ddbcc5eb4c2651b8f90b2cf2cc7d9f5da77e426d57ed44eb49a8919", + "sha256:1299f155b8dbe3374d1db810cb994cf22a3fadf8c5a85032aa8f31e18745a9c6", + "sha256:19641b992c05a47169cc655b7fbe4628dd5f29cafc910ce87dbd1702609d3bb1", + "sha256:1bdb1603f6178f47fdc2ef260a352840794d1cc65d7340d7de61646b9c26000a", + "sha256:1d2c4ae86218435cd76cb2f556433281923e15c22417d4ecb2f464325ed0dde3", + "sha256:2abfc1738c3fbb5a5552ea9fb34cca5cbdf73868caf78bdacdcd6ffbab438870", + "sha256:349557a2bf1cf800ff73f95bd812124a7f35c4a7bdfa62bcffa1c5a30604023a", + "sha256:44d50ad4d7bfde8d28825bdaf851a08a519c61c1cfbc4ed630bb6e67ccf12d72", + "sha256:51f9ce02856cec8c7346875e40068b58fdf9c1f5326dbdf342c751abbcff40df", + "sha256:586c229730e74308763147195d908e7568c0769d05bafc132f4faaf655f6cffe", + "sha256:5aec94e40549d8fd1b04dc50d1b4480d4e8e1ed61066798dade0b4ecd408e7ed", + "sha256:5e12901bd67c5ba374741fc497adc44de14854895c416cd0672b2e5b676ca23c", + "sha256:61b1c1d32df0a1ed0c8dca000ed15bab59e008349787d1d21b2a9d21ac7e5c8a", + "sha256:6af42e66212598a507879518f1fa8f13a489d52285e3715d1b4c91bcc70dd0ff", + "sha256:6c0332893fa201a60c4db7d6d296b131eb91793a062cfc9845bdcdab9cc6c22a", + "sha256:6d896b0ef27d2a568b294172fe32f33102e19b4183d9cbc5bd3296c1674704f5", + "sha256:79d0b7c1c1a98282ad3384bc4cf4f199431efa3f4e5eeda6785cb902703c9691", + "sha256:7adf2f885a971c9ae9af885d1cfac0dfa015eaf0b5b9ac8d114d73027c807c88", + "sha256:7fbd38cf672c6477ccd5d034a6c25db7fdb7ef3964f82d209cde41c9a2dfe09b", + "sha256:84ddb89cacfefdc6226b87991cbc13bea193d2a0d9185d13108b0ef560dffc7c", + "sha256:8e867c5ffde9ec8880253a484a33a961e5af40e26757eda67a34798aabe471af", + "sha256:a156feb70518fd4d748209726463adf92d4dde895a137442956c66d6d3867fb8", + "sha256:b3ac2b88f32612f7c4d792b3ed9b63eed414a1e85e004881a6ff08031c4ecf6c", + "sha256:b47c276e66f2a26b58eafd0745c788e7345c9445a9e4b7799dd7065445ca91bf", + "sha256:b83e50efae3199c94c18883356b5b964d02eb5c2ddb775596c92ee0715c0fc79", + "sha256:bcddc39a94971bb5b8c76f243a8b467f7b69674bd25531b85b4d25d5917dd52f", + "sha256:c5038ebc2f436398a153926db21d235ce75b050450af6bad17faee6336f6ef0b", + "sha256:c5478798cc80ca7c3f3463d02a5f038ab058a8cd4414a7f96afe6a35401bcc99", + "sha256:c6b5947c12128459a94398c303a1404064f69649f1cc6c1262ff6fbf2be6c47a", + "sha256:c9c887a6643238b45a8334a41a616c8c2ee7d69e2c8d804feeebdd50e8861808", + "sha256:cb788e15561dab81f5c562fb2496926a1b8b47d8ff1986d9b37acfa98b37faa9", + "sha256:d363718fe643b222b4d4a05e19a529c513565672801cb4527131f95f9bd937ea", + "sha256:d692df286fe1df2659c2e26e1d4e582b02bf32847e675f7e6a770cc107ca4987", + "sha256:ea6c55bbbdbf6cb7bc3693aa52d93c5efb4ded5be903b7faf0eb08e57f8dbfd5", + "sha256:ea815a3046d31e3a88c09c13d46956f9b872a6951dd7ddee02ac8e3aa642a2de" ], - "version": "==0.19.0" + "version": "==0.21.0b1" }, "watchfiles": { "hashes": [ - "sha256:00095dd368f73f8f1c3a7982a9801190cc88a2f3582dd395b289294f8975172b", - "sha256:00ad0bcd399503a84cc688590cdffbe7a991691314dde5b57b3ed50a41319a31", - "sha256:00f39592cdd124b4ec5ed0b1edfae091567c72c7da1487ae645426d1b0ffcad1", - "sha256:030bc4e68d14bcad2294ff68c1ed87215fbd9a10d9dea74e7cfe8a17869785ab", - "sha256:052d668a167e9fc345c24203b104c313c86654dd6c0feb4b8a6dfc2462239249", - "sha256:067dea90c43bf837d41e72e546196e674f68c23702d3ef80e4e816937b0a3ffd", - "sha256:0b04a2cbc30e110303baa6d3ddce8ca3664bc3403be0f0ad513d1843a41c97d1", - "sha256:0bc3b2f93a140df6806c8467c7f51ed5e55a931b031b5c2d7ff6132292e803d6", - "sha256:0c8e0aa0e8cc2a43561e0184c0513e291ca891db13a269d8d47cb9841ced7c71", - "sha256:103622865599f8082f03af4214eaff90e2426edff5e8522c8f9e93dc17caee13", - "sha256:1235c11510ea557fe21be5d0e354bae2c655a8ee6519c94617fe63e05bca4171", - "sha256:1cc0cba54f47c660d9fa3218158b8963c517ed23bd9f45fe463f08262a4adae1", - "sha256:1d9188979a58a096b6f8090e816ccc3f255f137a009dd4bbec628e27696d67c1", - "sha256:213792c2cd3150b903e6e7884d40660e0bcec4465e00563a5fc03f30ea9c166c", - "sha256:25c817ff2a86bc3de3ed2df1703e3d24ce03479b27bb4527c57e722f8554d971", - "sha256:2627a91e8110b8de2406d8b2474427c86f5a62bf7d9ab3654f541f319ef22bcb", - "sha256:280a4afbc607cdfc9571b9904b03a478fc9f08bbeec382d648181c695648202f", - "sha256:28324d6b28bcb8d7c1041648d7b63be07a16db5510bea923fc80b91a2a6cbed6", - "sha256:28585744c931576e535860eaf3f2c0ec7deb68e3b9c5a85ca566d69d36d8dd27", - "sha256:28f393c1194b6eaadcdd8f941307fc9bbd7eb567995232c830f6aef38e8a6e88", - "sha256:2abeb79209630da981f8ebca30a2c84b4c3516a214451bfc5f106723c5f45843", - "sha256:2bdadf6b90c099ca079d468f976fd50062905d61fae183f769637cb0f68ba59a", - "sha256:2f350cbaa4bb812314af5dab0eb8d538481e2e2279472890864547f3fe2281ed", - "sha256:3218a6f908f6a276941422b035b511b6d0d8328edd89a53ae8c65be139073f84", - "sha256:3973145235a38f73c61474d56ad6199124e7488822f3a4fc97c72009751ae3b0", - "sha256:3a0d883351a34c01bd53cfa75cd0292e3f7e268bacf2f9e33af4ecede7e21d1d", - "sha256:425440e55cd735386ec7925f64d5dde392e69979d4c8459f6bb4e920210407f2", - "sha256:4b9f2a128a32a2c273d63eb1fdbf49ad64852fc38d15b34eaa3f7ca2f0d2b797", - "sha256:4cc382083afba7918e32d5ef12321421ef43d685b9a67cc452a6e6e18920890e", - "sha256:52fc9b0dbf54d43301a19b236b4a4614e610605f95e8c3f0f65c3a456ffd7d35", - "sha256:55b7cc10261c2786c41d9207193a85c1db1b725cf87936df40972aab466179b6", - "sha256:581f0a051ba7bafd03e17127735d92f4d286af941dacf94bcf823b101366249e", - "sha256:5834e1f8b71476a26df97d121c0c0ed3549d869124ed2433e02491553cb468c2", - "sha256:5e45fb0d70dda1623a7045bd00c9e036e6f1f6a85e4ef2c8ae602b1dfadf7550", - "sha256:61af9efa0733dc4ca462347becb82e8ef4945aba5135b1638bfc20fad64d4f0e", - "sha256:68fe0c4d22332d7ce53ad094622b27e67440dacefbaedd29e0794d26e247280c", - "sha256:72a44e9481afc7a5ee3291b09c419abab93b7e9c306c9ef9108cb76728ca58d2", - "sha256:7a74436c415843af2a769b36bf043b6ccbc0f8d784814ba3d42fc961cdb0a9dc", - "sha256:8597b6f9dc410bdafc8bb362dac1cbc9b4684a8310e16b1ff5eee8725d13dcd6", - "sha256:8c39987a1397a877217be1ac0fb1d8b9f662c6077b90ff3de2c05f235e6a8f96", - "sha256:8c3e3675e6e39dc59b8fe5c914a19d30029e36e9f99468dddffd432d8a7b1c93", - "sha256:8dc1fc25a1dedf2dd952909c8e5cb210791e5f2d9bc5e0e8ebc28dd42fed7562", - "sha256:8fdebb655bb1ba0122402352b0a4254812717a017d2dc49372a1d47e24073795", - "sha256:9165bcab15f2b6d90eedc5c20a7f8a03156b3773e5fb06a790b54ccecdb73385", - "sha256:94ebe84a035993bb7668f58a0ebf998174fb723a39e4ef9fce95baabb42b787f", - "sha256:9624a68b96c878c10437199d9a8b7d7e542feddda8d5ecff58fdc8e67b460848", - "sha256:96eec15e5ea7c0b6eb5bfffe990fc7c6bd833acf7e26704eb18387fb2f5fd087", - "sha256:97b94e14b88409c58cdf4a8eaf0e67dfd3ece7e9ce7140ea6ff48b0407a593ec", - "sha256:988e981aaab4f3955209e7e28c7794acdb690be1efa7f16f8ea5aba7ffdadacb", - "sha256:a8a31bfd98f846c3c284ba694c6365620b637debdd36e46e1859c897123aa232", - "sha256:a927b3034d0672f62fb2ef7ea3c9fc76d063c4b15ea852d1db2dc75fe2c09696", - "sha256:ace7d060432acde5532e26863e897ee684780337afb775107c0a90ae8dbccfd2", - "sha256:aec83c3ba24c723eac14225194b862af176d52292d271c98820199110e31141e", - "sha256:b44b70850f0073b5fcc0b31ede8b4e736860d70e2dbf55701e05d3227a154a67", - "sha256:b610fb5e27825b570554d01cec427b6620ce9bd21ff8ab775fc3a32f28bba63e", - "sha256:b810a2c7878cbdecca12feae2c2ae8af59bea016a78bc353c184fa1e09f76b68", - "sha256:bbf8a20266136507abf88b0df2328e6a9a7c7309e8daff124dda3803306a9fdb", - "sha256:bd4c06100bce70a20c4b81e599e5886cf504c9532951df65ad1133e508bf20be", - "sha256:c2444dc7cb9d8cc5ab88ebe792a8d75709d96eeef47f4c8fccb6df7c7bc5be71", - "sha256:c49b76a78c156979759d759339fb62eb0549515acfe4fd18bb151cc07366629c", - "sha256:c4a65474fd2b4c63e2c18ac67a0c6c66b82f4e73e2e4d940f837ed3d2fd9d4da", - "sha256:c5af2347d17ab0bd59366db8752d9e037982e259cacb2ba06f2c41c08af02c39", - "sha256:c668228833c5619f6618699a2c12be057711b0ea6396aeaece4ded94184304ea", - "sha256:c7b978c384e29d6c7372209cbf421d82286a807bbcdeb315427687f8371c340a", - "sha256:d048ad5d25b363ba1d19f92dcf29023988524bee6f9d952130b316c5802069cb", - "sha256:d3e1f3cf81f1f823e7874ae563457828e940d75573c8fbf0ee66818c8b6a9099", - "sha256:d47e9ef1a94cc7a536039e46738e17cce058ac1593b2eccdede8bf72e45f372a", - "sha256:da1e0a8caebf17976e2ffd00fa15f258e14749db5e014660f53114b676e68538", - "sha256:dc1b9b56f051209be458b87edb6856a449ad3f803315d87b2da4c93b43a6fe72", - "sha256:dc2e8fe41f3cac0660197d95216c42910c2b7e9c70d48e6d84e22f577d106fc1", - "sha256:dc92d2d2706d2b862ce0568b24987eba51e17e14b79a1abcd2edc39e48e743c8", - "sha256:dd64f3a4db121bc161644c9e10a9acdb836853155a108c2446db2f5ae1778c3d", - "sha256:e0f0a874231e2839abbf473256efffe577d6ee2e3bfa5b540479e892e47c172d", - "sha256:f7e1f9c5d1160d03b93fc4b68a0aeb82fe25563e12fbcdc8507f8434ab6f823c", - "sha256:fe82d13461418ca5e5a808a9e40f79c1879351fcaeddbede094028e74d836e86" + "sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a", + "sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22", + "sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a", + "sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0", + "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827", + "sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1", + "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c", + "sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e", + "sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188", + "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b", + "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5", + "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90", + "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef", + "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b", + "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15", + "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48", + "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e", + "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df", + "sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd", + "sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91", + "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d", + "sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e", + "sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4", + "sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a", + "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370", + "sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1", + "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea", + "sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04", + "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896", + "sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f", + "sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f", + "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43", + "sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735", + "sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da", + "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a", + "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61", + "sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3", + "sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c", + "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f", + "sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361", + "sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855", + "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327", + "sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5", + "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab", + "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633", + "sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777", + "sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b", + "sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be", + "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f", + "sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b", + "sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e", + "sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b", + "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366", + "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823", + "sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3", + "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1", + "sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f", + "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418", + "sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886", + "sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571", + "sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c", + "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94", + "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428", + "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234", + "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6", + "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968", + "sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9", + "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c", + "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e", + "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab", + "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec", + "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444", + "sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b", + "sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c", + "sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca", + "sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b", + "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18", + "sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318", + "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07", + "sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430", + "sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c", + "sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83", + "sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05" ], - "version": "==0.22.0" + "version": "==0.24.0" }, "websockets": { "hashes": [ - "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b", - "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6", - "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df", - "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b", - "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205", - "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892", - "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53", - "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2", - "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed", - "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c", - "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd", - "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b", - "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931", - "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30", - "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370", - "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be", - "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec", - "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf", - "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62", - "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b", - "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402", - "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f", - "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123", - "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9", - "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603", - "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45", - "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558", - "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4", - "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438", - "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137", - "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480", - "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447", - "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8", - "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04", - "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c", - "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb", - "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967", - "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b", - "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d", - "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def", - "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c", - "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92", - "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2", - "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113", - "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b", - "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28", - "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7", - "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d", - "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f", - "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468", - "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8", - "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae", - "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611", - "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d", - "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9", - "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca", - "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f", - "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2", - "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077", - "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2", - "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6", - "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374", - "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc", - "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e", - "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53", - "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399", - "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547", - "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3", - "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870", - "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5", - "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8", - "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7" + "sha256:00fd961943b6c10ee6f0b1130753e50ac5dcd906130dcd77b0003c3ab797d026", + "sha256:03d3f9ba172e0a53e37fa4e636b86cc60c3ab2cfee4935e66ed1d7acaa4625ad", + "sha256:0513c727fb8adffa6d9bf4a4463b2bade0186cbd8c3604ae5540fae18a90cb99", + "sha256:05e70fec7c54aad4d71eae8e8cab50525e899791fc389ec6f77b95312e4e9920", + "sha256:0617fd0b1d14309c7eab6ba5deae8a7179959861846cbc5cb528a7531c249448", + "sha256:06c0a667e466fcb56a0886d924b5f29a7f0886199102f0a0e1c60a02a3751cb4", + "sha256:0f52504023b1480d458adf496dc1c9e9811df4ba4752f0bc1f89ae92f4f07d0c", + "sha256:10a0dc7242215d794fb1918f69c6bb235f1f627aaf19e77f05336d147fce7c37", + "sha256:11f9976ecbc530248cf162e359a92f37b7b282de88d1d194f2167b5e7ad80ce3", + "sha256:132511bfd42e77d152c919147078460c88a795af16b50e42a0bd14f0ad71ddd2", + "sha256:139add0f98206cb74109faf3611b7783ceafc928529c62b389917a037d4cfdf4", + "sha256:14b9c006cac63772b31abbcd3e3abb6228233eec966bf062e89e7fa7ae0b7333", + "sha256:15c7d62ee071fa94a2fc52c2b472fed4af258d43f9030479d9c4a2de885fd543", + "sha256:165bedf13556f985a2aa064309baa01462aa79bf6112fbd068ae38993a0e1f1b", + "sha256:17118647c0ea14796364299e942c330d72acc4b248e07e639d34b75067b3cdd8", + "sha256:1841c9082a3ba4a05ea824cf6d99570a6a2d8849ef0db16e9c826acb28089e8f", + "sha256:1a678532018e435396e37422a95e3ab87f75028ac79570ad11f5bf23cd2a7d8c", + "sha256:1ee4cc030a4bdab482a37462dbf3ffb7e09334d01dd37d1063be1136a0d825fa", + "sha256:1f3cf6d6ec1142412d4535adabc6bd72a63f5f148c43fe559f06298bc21953c9", + "sha256:1f613289f4a94142f914aafad6c6c87903de78eae1e140fa769a7385fb232fdf", + "sha256:1fa082ea38d5de51dd409434edc27c0dcbd5fed2b09b9be982deb6f0508d25bc", + "sha256:249aab278810bee585cd0d4de2f08cfd67eed4fc75bde623be163798ed4db2eb", + "sha256:254ecf35572fca01a9f789a1d0f543898e222f7b69ecd7d5381d8d8047627bdb", + "sha256:2a02b0161c43cc9e0232711eff846569fad6ec836a7acab16b3cf97b2344c060", + "sha256:30d3a1f041360f029765d8704eae606781e673e8918e6b2c792e0775de51352f", + "sha256:3624fd8664f2577cf8de996db3250662e259bfbc870dd8ebdcf5d7c6ac0b5185", + "sha256:3f55b36d17ac50aa8a171b771e15fbe1561217510c8768af3d546f56c7576cdc", + "sha256:46af561eba6f9b0848b2c9d2427086cabadf14e0abdd9fde9d72d447df268418", + "sha256:47236c13be337ef36546004ce8c5580f4b1150d9538b27bf8a5ad8edf23ccfab", + "sha256:4a365bcb7be554e6e1f9f3ed64016e67e2fa03d7b027a33e436aecf194febb63", + "sha256:4d6ece65099411cfd9a48d13701d7438d9c34f479046b34c50ff60bb8834e43e", + "sha256:4e85f46ce287f5c52438bb3703d86162263afccf034a5ef13dbe4318e98d86e7", + "sha256:4f0426d51c8f0926a4879390f53c7f5a855e42d68df95fff6032c82c888b5f36", + "sha256:518f90e6dd089d34eaade01101fd8a990921c3ba18ebbe9b0165b46ebff947f0", + "sha256:52aed6ef21a0f1a2a5e310fb5c42d7555e9c5855476bbd7173c3aa3d8a0302f2", + "sha256:556e70e4f69be1082e6ef26dcb70efcd08d1850f5d6c5f4f2bcb4e397e68f01f", + "sha256:56a952fa2ae57a42ba7951e6b2605e08a24801a4931b5644dfc68939e041bc7f", + "sha256:59197afd478545b1f73367620407b0083303569c5f2d043afe5363676f2697c9", + "sha256:5df891c86fe68b2c38da55b7aea7095beca105933c697d719f3f45f4220a5e0e", + "sha256:63848cdb6fcc0bf09d4a155464c46c64ffdb5807ede4fb251da2c2692559ce75", + "sha256:64a11aae1de4c178fa653b07d90f2fb1a2ed31919a5ea2361a38760192e1858b", + "sha256:6724b554b70d6195ba19650fef5759ef11346f946c07dbbe390e039bcaa7cc3d", + "sha256:67494e95d6565bf395476e9d040037ff69c8b3fa356a886b21d8422ad86ae075", + "sha256:67648f5e50231b5a7f6d83b32f9c525e319f0ddc841be0de64f24928cd75a603", + "sha256:68264802399aed6fe9652e89761031acc734fc4c653137a5911c2bfa995d6d6d", + "sha256:699ba9dd6a926f82a277063603fc8d586b89f4cb128efc353b749b641fcddda7", + "sha256:6aa74a45d4cdc028561a7d6ab3272c8b3018e23723100b12e58be9dfa5a24491", + "sha256:6b41a1b3b561f1cba8321fb32987552a024a8f67f0d05f06fcf29f0090a1b956", + "sha256:71e6e5a3a3728886caee9ab8752e8113670936a193284be9d6ad2176a137f376", + "sha256:7d20516990d8ad557b5abeb48127b8b779b0b7e6771a265fa3e91767596d7d97", + "sha256:80e4ba642fc87fa532bac07e5ed7e19d56940b6af6a8c61d4429be48718a380f", + "sha256:872afa52a9f4c414d6955c365b6588bc4401272c629ff8321a55f44e3f62b553", + "sha256:8eb2b9a318542153674c6e377eb8cb9ca0fc011c04475110d3477862f15d29f0", + "sha256:9bbc525f4be3e51b89b2a700f5746c2a6907d2e2ef4513a8daafc98198b92237", + "sha256:a1a2e272d067030048e1fe41aa1ec8cfbbaabce733b3d634304fa2b19e5c897f", + "sha256:a5dc0c42ded1557cc7c3f0240b24129aefbad88af4f09346164349391dea8e58", + "sha256:acab3539a027a85d568c2573291e864333ec9d912675107d6efceb7e2be5d980", + "sha256:acbebec8cb3d4df6e2488fbf34702cbc37fc39ac7abf9449392cefb3305562e9", + "sha256:ad327ac80ba7ee61da85383ca8822ff808ab5ada0e4a030d66703cc025b021c4", + "sha256:b448a0690ef43db5ef31b3a0d9aea79043882b4632cfc3eaab20105edecf6097", + "sha256:b5a06d7f60bc2fc378a333978470dfc4e1415ee52f5f0fce4f7853eb10c1e9df", + "sha256:b74593e9acf18ea5469c3edaa6b27fa7ecf97b30e9dabd5a94c4c940637ab96e", + "sha256:b79915a1179a91f6c5f04ece1e592e2e8a6bd245a0e45d12fd56b2b59e559a32", + "sha256:b80f0c51681c517604152eb6a572f5a9378f877763231fddb883ba2f968e8817", + "sha256:b8ac5b46fd798bbbf2ac6620e0437c36a202b08e1f827832c4bf050da081b501", + "sha256:c3c493d0e5141ec055a7d6809a28ac2b88d5b878bb22df8c621ebe79a61123d0", + "sha256:c44ca9ade59b2e376612df34e837013e2b273e6c92d7ed6636d0556b6f4db93d", + "sha256:c4a6343e3b0714e80da0b0893543bf9a5b5fa71b846ae640e56e9abc6fbc4c83", + "sha256:c5870b4a11b77e4caa3937142b650fbbc0914a3e07a0cf3131f35c0587489c1c", + "sha256:ca48914cdd9f2ccd94deab5bcb5ac98025a5ddce98881e5cce762854a5de330b", + "sha256:cf2fae6d85e5dc384bf846f8243ddaa9197f3a1a70044f59399af001fd1f51d4", + "sha256:d450f5a7a35662a9b91a64aefa852f0c0308ee256122f5218a42f1d13577d71e", + "sha256:d6716c087e4aa0b9260c4e579bb82e068f84faddb9bfba9906cb87726fa2e870", + "sha256:d93572720d781331fb10d3da9ca1067817d84ad1e7c31466e9f5e59965618096", + "sha256:dbb0b697cc0655719522406c059eae233abaa3243821cfdfab1215d02ac10231", + "sha256:e33505534f3f673270dd67f81e73550b11de5b538c56fe04435d63c02c3f26b5", + "sha256:e801ca2f448850685417d723ec70298feff3ce4ff687c6f20922c7474b4746ae", + "sha256:e82db3756ccb66266504f5a3de05ac6b32f287faacff72462612120074103329", + "sha256:ef48e4137e8799998a343706531e656fdec6797b80efd029117edacb74b0a10a", + "sha256:f1d3d1f2eb79fe7b0fb02e599b2bf76a7619c79300fc55f0b5e2d382881d4f7f", + "sha256:f3fea72e4e6edb983908f0db373ae0732b275628901d909c382aae3b592589f2", + "sha256:f40de079779acbcdbb6ed4c65af9f018f8b77c5ec4e17a4b737c05c2db554491", + "sha256:f73e676a46b0fe9426612ce8caeca54c9073191a77c3e9d5c94697aef99296af", + "sha256:f9c9e258e3d5efe199ec23903f5da0eeaad58cf6fccb3547b74fd4750e5ac47a", + "sha256:fac2d146ff30d9dd2fcf917e5d147db037a5c573f0446c564f16f1f94cf87462", + "sha256:faef9ec6354fe4f9a2c0bbb52fb1ff852effc897e2a4501e25eb3a47cb0a4f89" ], - "version": "==12.0" + "version": "==13.0.1" } }, "develop": {} diff --git a/server/database.py b/server/database.py index 1c5ebad..5430a73 100644 --- a/server/database.py +++ b/server/database.py @@ -33,7 +33,7 @@ class Database(): "pragma": """ ( id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT NOT NULL, + username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, last_login DATETIME, created_on DATETIME NOT NULL DEFAULT current_timestamp diff --git a/server/main.py b/server/main.py index b6a8646..c98919a 100644 --- a/server/main.py +++ b/server/main.py @@ -1,4 +1,4 @@ -from server.routers import flights, airports, statistics, geography, importing, exporting +from server.routers import flights, airports, statistics, geography, importing, exporting, auth from fastapi import FastAPI from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles @@ -10,6 +10,7 @@ { "name": "statistics"}, { "name": "geography"}, { "name": "importing/exporting"}, + { "name": "authentication"} ] app = FastAPI(openapi_tags=tags_metadata) @@ -21,6 +22,7 @@ app.include_router(geography.router, prefix="/api") app.include_router(importing.router, prefix="/api") app.include_router(exporting.router, prefix="/api") +app.include_router(auth.router, prefix="/api") @app.get("/", include_in_schema=False) @app.get("/new", include_in_schema=False) diff --git a/server/routers/auth.py b/server/routers/auth.py new file mode 100644 index 0000000..dd14872 --- /dev/null +++ b/server/routers/auth.py @@ -0,0 +1,67 @@ +from pydantic import BaseModel +from server.database import database +from server.models import User + +import jwt +from datetime import datetime, timedelta +from typing import Annotated +from fastapi import APIRouter, Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from passlib.context import CryptContext + +router = APIRouter( + prefix="/auth", + tags=["auth"], + redirect_slashes=True +) + +# CRITICAL SAFETY WARNING: +# this variable was set here for testin purposes ONLY, +# in a production environment, this HAS to be changed +# to be entered by the user as an argument!!! +SECRET_KEY = "ad9df50bddc30ac206cd203a511285341b482d5e24f64c43579d4ade4d3b54fc" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_DAYS = 7 + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/token") + +class Token(BaseModel): + access_token: str + token_type: str + +def verify_password(password: str, password_hash: str) -> bool: + return pwd_context.verify(password, password_hash) + +def hash_password(password: str) -> str: + return pwd_context.hash(password) + +def create_access_token(data: dict): + to_encode = data.copy() + + # set expiration + expire = datetime.utcnow() + timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS) + to_encode.update({"exp": expire}) + + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + +def get_user(username: str) -> User|None: + result = database .execute_read_query(f"SELECT * FROM users WHERE username = {username};") + + if not result: + return None + + user = User.from_database(result[0]) + return User.model_validate(user) + +@router.post("/token") +async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): + user = get_user(form_data.username) + if not user or not verify_password(form_data.password, user.password_hash): + raise HTTPException(status_code=401, + headers={"WWW-Authenticate": "Bearer"}, + detail="Incorrect username or password") + + access_token = create_access_token({"sub": user.username}) + return Token(access_token=access_token, token_type="bearer") From 81f42a263447e6596572cc30289ada146aca683f Mon Sep 17 00:00:00 2001 From: Pietro Bonaldo Date: Fri, 20 Sep 2024 15:40:26 +0200 Subject: [PATCH 03/21] Create default user, simple user methods, add 'is_admin' field to users --- Pipfile | 2 +- Pipfile.lock | 14 +++------- server/{routers => auth}/auth.py | 44 ++++++++++++++++++++------------ server/auth/utils.py | 15 +++++++++++ server/database.py | 8 ++++++ server/main.py | 6 +++-- server/models.py | 2 ++ 7 files changed, 61 insertions(+), 30 deletions(-) rename server/{routers => auth}/auth.py (56%) create mode 100644 server/auth/utils.py diff --git a/Pipfile b/Pipfile index 875673f..b992428 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pydantic = "2.3.0" requests = "2.31.0" python-multipart = "0.0.9" pyjwt = "2.9.0" -passlib = {extras = ["bcrypt"], version = "1.7.4"} +bcrypt = "4.2.0" [requires] python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock index a952a8d..2194c15 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "410ce79982cfdff73e6b831c160e26675e47f3fcb51f7f01edf897ce39e8e340" + "sha256": "6d1e7914a787bbe6282f0c2fb7737733a00909f6b884057204d01fcbab2cc139" }, "pipfile-spec": 6, "requires": { @@ -62,6 +62,8 @@ "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221", "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db" ], + "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==4.2.0" }, "certifi": { @@ -242,16 +244,6 @@ "markers": "python_version >= '3.6'", "version": "==3.10" }, - "passlib": { - "extras": [ - "bcrypt" - ], - "hashes": [ - "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", - "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04" - ], - "version": "==1.7.4" - }, "pydantic": { "hashes": [ "sha256:1607cc106602284cd4a00882986570472f193fde9cb1259bceeaedb26aa79a6d", diff --git a/server/routers/auth.py b/server/auth/auth.py similarity index 56% rename from server/routers/auth.py rename to server/auth/auth.py index dd14872..a9409ac 100644 --- a/server/routers/auth.py +++ b/server/auth/auth.py @@ -1,16 +1,15 @@ -from pydantic import BaseModel -from server.database import database from server.models import User +from server.database import database +from server.auth.utils import hash_password, verify_password, oauth2_scheme import jwt +from pydantic import BaseModel from datetime import datetime, timedelta from typing import Annotated from fastapi import APIRouter, Depends, HTTPException -from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm -from passlib.context import CryptContext +from fastapi.security import OAuth2PasswordRequestForm router = APIRouter( - prefix="/auth", tags=["auth"], redirect_slashes=True ) @@ -23,18 +22,10 @@ ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_DAYS = 7 -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/token") - class Token(BaseModel): access_token: str token_type: str -def verify_password(password: str, password_hash: str) -> bool: - return pwd_context.verify(password, password_hash) - -def hash_password(password: str) -> str: - return pwd_context.hash(password) def create_access_token(data: dict): to_encode = data.copy() @@ -46,8 +37,9 @@ def create_access_token(data: dict): encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt + def get_user(username: str) -> User|None: - result = database .execute_read_query(f"SELECT * FROM users WHERE username = {username};") + result = database.execute_read_query(f"SELECT * FROM users WHERE username = '{username}';") if not result: return None @@ -55,13 +47,33 @@ def get_user(username: str) -> User|None: user = User.from_database(result[0]) return User.model_validate(user) -@router.post("/token") -async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): +def update_last_login(username: str) -> None: + database.execute_query(f"""UPDATE users + SET last_login = current_timestamp + WHERE username = '{username}';""") + +@router.post("/token", status_code=200) +async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token: user = get_user(form_data.username) if not user or not verify_password(form_data.password, user.password_hash): raise HTTPException(status_code=401, headers={"WWW-Authenticate": "Bearer"}, detail="Incorrect username or password") + update_last_login(user.username) + access_token = create_access_token({"sub": user.username}) return Token(access_token=access_token, token_type="bearer") + +@router.post("/user", status_code=201) +async def create_user(token: Annotated[str, Depends(oauth2_scheme)], username: str, password: str): + password_hash = hash_password(password) + database.execute_query(f"INSERT INTO users (username, password_hash) VALUES (?, ?)", + [username, password_hash]) + +@router.patch("/user", status_code=200) +async def update_password(token: Annotated[str, Depends(oauth2_scheme)], username: str, new_password: str): + password_hash = hash_password(new_password) + database.execute_query(f"""UPDATE users + SET password_hash = '{password_hash}' + WHERE username = '{username}';""") diff --git a/server/auth/utils.py b/server/auth/utils.py new file mode 100644 index 0000000..605b420 --- /dev/null +++ b/server/auth/utils.py @@ -0,0 +1,15 @@ +from fastapi.security import OAuth2PasswordBearer +import bcrypt + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token") + +def verify_password(password: str, password_hash: str) -> bool: + password_bytes = password.encode('utf-8') + password_hash_bytes = password_hash.encode('utf-8') + return bcrypt.checkpw(password_bytes, password_hash_bytes) + +def hash_password(password: str) -> str: + pwd_bytes = password.encode('utf-8') + salt = bcrypt.gensalt() + password_hash = bcrypt.hashpw(pwd_bytes, salt).decode('utf-8') + return password_hash diff --git a/server/database.py b/server/database.py index 5430a73..b3db4cc 100644 --- a/server/database.py +++ b/server/database.py @@ -35,6 +35,7 @@ class Database(): id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, + is_admin BIT NOT NULL DEFAULT 0, last_login DATETIME, created_on DATETIME NOT NULL DEFAULT current_timestamp )""", @@ -98,6 +99,13 @@ def initialize_tables(self): table_pragma = self.tables[table]["pragma"] self.execute_query(f"CREATE TABLE {table} {table_pragma};") + # create default user + from server.auth.utils import hash_password + default_username = "admin" + default_password = hash_password("admin") + self.execute_query("INSERT INTO users (username, password_hash) VALUES (?, ?);", + [default_username, default_password]) + self.execute_query(""" CREATE TABLE airports ( icao TEXT, diff --git a/server/main.py b/server/main.py index c98919a..ed15da1 100644 --- a/server/main.py +++ b/server/main.py @@ -1,4 +1,5 @@ -from server.routers import flights, airports, statistics, geography, importing, exporting, auth +from server.routers import flights, airports, statistics, geography, importing, exporting +from server.auth import auth from fastapi import FastAPI from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles @@ -22,7 +23,8 @@ app.include_router(geography.router, prefix="/api") app.include_router(importing.router, prefix="/api") app.include_router(exporting.router, prefix="/api") -app.include_router(auth.router, prefix="/api") + +app.include_router(auth.router, prefix="/auth") @app.get("/", include_in_schema=False) @app.get("/new", include_in_schema=False) diff --git a/server/models.py b/server/models.py index d180f0d..4f927db 100644 --- a/server/models.py +++ b/server/models.py @@ -59,8 +59,10 @@ def empty(self) -> bool: return True class User(CustomModel): + id: int username: str password_hash: str + is_admin: bool last_login: datetime.datetime|None created_on: datetime.datetime From f3740490206eeae878333c9334939afac53a1efa Mon Sep 17 00:00:00 2001 From: Pietro Bonaldo Date: Fri, 20 Sep 2024 16:21:59 +0200 Subject: [PATCH 04/21] Get current user functionality, improve user update method --- server/auth/auth.py | 71 +++++++++++++++++++++++++++++++++++++++------ server/database.py | 2 +- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/server/auth/auth.py b/server/auth/auth.py index a9409ac..4e2ce67 100644 --- a/server/auth/auth.py +++ b/server/auth/auth.py @@ -1,4 +1,4 @@ -from server.models import User +from server.models import CustomModel, User from server.database import database from server.auth.utils import hash_password, verify_password, oauth2_scheme @@ -26,7 +26,6 @@ class Token(BaseModel): access_token: str token_type: str - def create_access_token(data: dict): to_encode = data.copy() @@ -37,7 +36,6 @@ def create_access_token(data: dict): encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt - def get_user(username: str) -> User|None: result = database.execute_read_query(f"SELECT * FROM users WHERE username = '{username}';") @@ -65,15 +63,70 @@ async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> T access_token = create_access_token({"sub": user.username}) return Token(access_token=access_token, token_type="bearer") +@router.get("/user") +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> User: + credentials_exception = HTTPException( + status_code=401, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username = payload.get("sub") + + if username == None: + raise credentials_exception + except jwt.InvalidTokenError: + raise credentials_exception + + user = get_user(username) + + if user == None: + raise credentials_exception + + return user + @router.post("/user", status_code=201) -async def create_user(token: Annotated[str, Depends(oauth2_scheme)], username: str, password: str): +async def create_user(user: Annotated[User, Depends(get_current_user)], username: str, password: str): + if not user.is_admin: + raise HTTPException(status_code=401, detail="Only admins change create new users") + password_hash = hash_password(password) database.execute_query(f"INSERT INTO users (username, password_hash) VALUES (?, ?)", [username, password_hash]) +class UserPatch(CustomModel): + username: str|None = None + password: str|None = None + is_admin: str|None = None + @router.patch("/user", status_code=200) -async def update_password(token: Annotated[str, Depends(oauth2_scheme)], username: str, new_password: str): - password_hash = hash_password(new_password) - database.execute_query(f"""UPDATE users - SET password_hash = '{password_hash}' - WHERE username = '{username}';""") +async def update_user(user: Annotated[User, Depends(get_current_user)], username: str, new_user: UserPatch): + if user.username != username and not user.is_admin: + raise HTTPException(status_code=401, detail="Only admins can edit other users") + if new_user.is_admin and (user.username == username or not user.is_admin): + raise HTTPException(status_code=401, detail="Only admins can change the admin status of other users") + + query = "UPDATE users SET " + + values = [] + for attr in UserPatch.get_attributes(): + value = getattr(new_user, attr) + + if not value: + continue + + if attr == "password": + value = hash_password(value) + attr = "password_hash" + + query += f"{attr}=?," + values.append(value) + + if query[-1] == ',': + query = query[:-1] + + query += f" WHERE username = '{username}';" + print(query) + print(values) + database.execute_query(query, values) diff --git a/server/database.py b/server/database.py index b3db4cc..b351ba7 100644 --- a/server/database.py +++ b/server/database.py @@ -103,7 +103,7 @@ def initialize_tables(self): from server.auth.utils import hash_password default_username = "admin" default_password = hash_password("admin") - self.execute_query("INSERT INTO users (username, password_hash) VALUES (?, ?);", + self.execute_query("INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, 1);", [default_username, default_password]) self.execute_query(""" From 8a85948c4af752ff46d64878c96c9e08d88dfe35 Mon Sep 17 00:00:00 2001 From: Pietro Bonaldo Date: Sun, 6 Oct 2024 22:05:26 +0200 Subject: [PATCH 05/21] Restrict jetlog to authenticated users, Login page --- client/App.tsx | 26 +++++++++------- client/api.ts | 34 +++++++++++++++++---- client/components/SingleFlight.tsx | 4 +-- client/components/Stats.tsx | 6 ++-- client/components/WorldMap.tsx | 19 ++++++++---- client/index.html | 4 +++ client/index.js | 3 ++ client/pages/AllFlights.tsx | 4 +-- client/pages/Login.tsx | 43 ++++++++++++++++++++++++++ client/pages/Settings.tsx | 6 ++-- client/settingsManager.ts | 47 ----------------------------- client/storage/configStorage.ts | 48 ++++++++++++++++++++++++++++++ client/storage/tokenStorage.ts | 29 ++++++++++++++++++ server/auth/auth.py | 24 +++++++-------- server/auth/utils.py | 2 +- server/main.py | 19 +++++++----- 16 files changed, 217 insertions(+), 101 deletions(-) create mode 100644 client/pages/Login.tsx delete mode 100644 client/settingsManager.ts create mode 100644 client/storage/configStorage.ts create mode 100644 client/storage/tokenStorage.ts diff --git a/client/App.tsx b/client/App.tsx index 7647c2c..74b073f 100644 --- a/client/App.tsx +++ b/client/App.tsx @@ -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' @@ -12,18 +13,23 @@ import Navbar from './components/Navbar'; export function App() { return ( - -
- } /> - } /> - } /> - } /> - } /> + } /> + + +
+ +
+ }> + } /> + } /> + } /> + } /> + } /> +
-
-
); } diff --git a/client/api.ts b/client/api.ts index 16ab2b1..993bef8 100644 --- a/client/api.ts +++ b/client/api.ts @@ -1,7 +1,11 @@ +import { useNavigate } from 'react-router-dom'; + 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; @@ -10,11 +14,33 @@ 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) { + window.location.href = "/login"; + } + else { + alert("Bad response: " + err.response.data.detail); + } } else if (err.request) { alert("Bad request: " + err.request); @@ -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(); diff --git a/client/components/SingleFlight.tsx b/client/components/SingleFlight.tsx index ee354bd..4574483 100644 --- a/client/components/SingleFlight.tsx +++ b/client/components/SingleFlight.tsx @@ -1,7 +1,7 @@ import React, {useEffect, useState} from 'react'; import {useNavigate} from 'react-router-dom'; -import { SettingsManager } from '../settingsManager'; +import ConfigStorage from '../storage/configStorage'; import { Button, Heading, Input, Select, Subheading, TextArea } from '../components/Elements' import { Airport, Flight } from '../models'; import API from '../api'; @@ -26,7 +26,7 @@ export default function SingleFlight({ flightID }) { const [flightPatch, setFlightPatch] = useState({}); const [editMode, setEditMode] = useState(false); const navigate = useNavigate(); - const metricUnits = SettingsManager.getSetting("metricUnits"); + const metricUnits = ConfigStorage.getSetting("metricUnits"); useEffect(() => { API.get(`/flights?id=${flightID}&metric=${metricUnits}`) diff --git a/client/components/Stats.tsx b/client/components/Stats.tsx index 6833f92..9620dcd 100644 --- a/client/components/Stats.tsx +++ b/client/components/Stats.tsx @@ -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}) { @@ -16,7 +16,7 @@ function StatBox({stat, description}) { export function ShortStats() { const [statistics, setStatistics] = useState() - const metricUnits = SettingsManager.getSetting("metricUnits"); + const metricUnits = ConfigStorage.getSetting("metricUnits"); // runs before render useMemo(() => { @@ -77,7 +77,7 @@ function StatFrequency({ object, measure }) { export function AllStats({ filters }) { const [statistics, setStatistics] = useState() - const metricUnits = SettingsManager.getSetting("metricUnits"); + const metricUnits = ConfigStorage.getSetting("metricUnits"); useEffect(() => { API.get(`/statistics?metric=${metricUnits}`, filters) diff --git a/client/components/WorldMap.tsx b/client/components/WorldMap.tsx index b15e4b9..6b1649a 100644 --- a/client/components/WorldMap.tsx +++ b/client/components/WorldMap.tsx @@ -2,15 +2,18 @@ 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() const [markers, setMarkers] = useState([]) const [lines, setLines] = useState([]) useEffect(() => { + API.get("/geography/world") + .then((data) => setWorld(data)); + API.get("/geography/markers") .then((data) => setMarkers(data)); @@ -18,11 +21,15 @@ export default function WorldMap() { .then((data) => setLines(data)); }, []); + if (world === undefined) { + return; + } + return ( <> - + {({ geographies }) => geographies.map((geo) => ( ( diff --git a/client/index.js b/client/index.js index a3f4e72..7d17473 100644 --- a/client/index.js +++ b/client/index.js @@ -1,6 +1,9 @@ import { createRoot } from 'react-dom/client'; import { App } from './App'; +import TokenStorage from './storage/tokenStorage'; +TokenStorage.loadStoredToken(); + const container = document.getElementById("app"); const root = createRoot(container); root.render(); diff --git a/client/pages/AllFlights.tsx b/client/pages/AllFlights.tsx index 2cdd211..a89fea4 100644 --- a/client/pages/AllFlights.tsx +++ b/client/pages/AllFlights.tsx @@ -5,7 +5,7 @@ import { Heading, Label, Input, Select, Dialog, Whisper } from '../components/El import SingleFlight from '../components/SingleFlight'; import { Flight } from '../models' import API from '../api' -import { SettingsManager } from '../settingsManager'; +import ConfigStorage from '../storage/configStorage'; interface FlightsFilters { limit?: number; @@ -107,7 +107,7 @@ function TableHeading({ text }) { function FlightsTable({ filters }: { filters: FlightsFilters }) { const [flights, setFlights] = useState(); const navigate = useNavigate(); - const metricUnits = SettingsManager.getSetting("metricUnits"); + const metricUnits = ConfigStorage.getSetting("metricUnits"); useEffect(() => { API.get(`/flights?metric=${metricUnits}`, filters) diff --git a/client/pages/Login.tsx b/client/pages/Login.tsx new file mode 100644 index 0000000..a8bd8ca --- /dev/null +++ b/client/pages/Login.tsx @@ -0,0 +1,43 @@ +import React, {useState} from 'react'; +import { useNavigate } from 'react-router-dom'; + +import API from '../api'; +import TokenStorage from '../storage/tokenStorage'; +import {Heading, Checkbox, Input, Button} from '../components/Elements' + +export default function Login() { + const navigate = useNavigate(); + const [remember, setRemember] = useState(false) + + const handleSubmit = (event) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + + API.post("/auth/token", formData) + .then((res) => { + const token = res.access_token; + TokenStorage.storeToken(token, remember); + + navigate("/"); + }); + } + + return ( +
+
+ +
+ + + +

Remember me

+ setRemember(e.target.checked)}/> + +
+ +
+
+ ); +} diff --git a/client/pages/Settings.tsx b/client/pages/Settings.tsx index f17d237..1e94ea0 100644 --- a/client/pages/Settings.tsx +++ b/client/pages/Settings.tsx @@ -3,10 +3,10 @@ import { useNavigate } from 'react-router-dom'; import API from '../api'; import {Heading, Label, Input, Checkbox, Subheading, Button} from '../components/Elements' -import {SettingsInterface, SettingsManager} from '../settingsManager'; +import ConfigStorage, {ConfigInterface} from '../storage/configStorage'; export default function Settings() { - const [options, setOptions] = useState(SettingsManager.getAllSettings()) + const [options, setOptions] = useState(ConfigStorage.getAllSettings()) const navigate = useNavigate(); const handleImportSubmit = (event) => { @@ -38,7 +38,7 @@ export default function Settings() { const value = event.target.checked.toString(); setOptions({...options, [key]: value}) - SettingsManager.setSetting(key, value); + ConfigStorage.setSetting(key, value); } return ( diff --git a/client/settingsManager.ts b/client/settingsManager.ts deleted file mode 100644 index 74d37a0..0000000 --- a/client/settingsManager.ts +++ /dev/null @@ -1,47 +0,0 @@ -// cannot store boolean in localStorage -export interface SettingsInterface { - frequencyBasedMarker: string; - frequencyBasedLine: string; - //militaryClock: string; - metricUnits: string; -} -const SettingsKeys = ["frequencyBasedMarker", "frequencyBasedLine", /*"militaryClock",*/ "metricUnits"]; -type Setting = typeof SettingsKeys[number]; - -const defaultSettings: SettingsInterface = { - frequencyBasedMarker: "false", - frequencyBasedLine: "false", - //militaryClock: "true", - metricUnits: "true" -} - -class SettingsManagerClass { - constructor() { - for(let key of SettingsKeys) { - const current = localStorage.getItem(key); - - if(current === null) { - localStorage.setItem(key, defaultSettings[key]); - } - } - } - - getSetting(setting: Setting): string|null { - return localStorage.getItem(setting); - } - - getAllSettings(): SettingsInterface { - var settings: SettingsInterface = defaultSettings; - - for(let key of SettingsKeys) { - settings[key] = this.getSetting(key); - } - - return settings; - } - - setSetting(setting: Setting, value: string): void { - localStorage.setItem(setting, value) - } -} -export const SettingsManager = new SettingsManagerClass(); diff --git a/client/storage/configStorage.ts b/client/storage/configStorage.ts new file mode 100644 index 0000000..266c3a2 --- /dev/null +++ b/client/storage/configStorage.ts @@ -0,0 +1,48 @@ +// cannot store boolean in localStorage +export interface ConfigInterface { + frequencyBasedMarker: string; + frequencyBasedLine: string; + //militaryClock: string; + metricUnits: string; +} +const ConfigKeys = ["frequencyBasedMarker", "frequencyBasedLine", /*"militaryClock",*/ "metricUnits"]; +type Config = typeof ConfigKeys[number]; + +const defaultConfig: ConfigInterface = { + frequencyBasedMarker: "false", + frequencyBasedLine: "false", + //militaryClock: "true", + metricUnits: "true" +} + +class ConfigStorageClass { + constructor() { + for(let key of ConfigKeys) { + const current = localStorage.getItem(key); + + if(current === null) { + localStorage.setItem(key, defaultConfig[key]); + } + } + } + + getSetting(setting: Config): string|null { + return localStorage.getItem(setting); + } + + getAllSettings(): ConfigInterface{ + var settings: ConfigInterface = defaultConfig; + + for(let key of ConfigKeys) { + settings[key] = this.getSetting(key); + } + + return settings; + } + + setSetting(setting: Config, value: string): void { + localStorage.setItem(setting, value) + } +} +const ConfigStorage = new ConfigStorageClass(); +export default ConfigStorage; diff --git a/client/storage/tokenStorage.ts b/client/storage/tokenStorage.ts new file mode 100644 index 0000000..f45eae1 --- /dev/null +++ b/client/storage/tokenStorage.ts @@ -0,0 +1,29 @@ +class TokenStorageClass { + private tokenKey = "token"; + + getToken(): string|null { + const token = sessionStorage.getItem(this.tokenKey); + return token; + } + + loadStoredToken(): void { + const token = localStorage.getItem(this.tokenKey); + if (token !== null) { + this.storeToken(token); + } + } + + storeToken(token: string, remember: boolean = false): void { + sessionStorage.setItem(this.tokenKey, token); + if (remember) { + localStorage.setItem(this.tokenKey, token); + } + } + + clearToken(): void { + sessionStorage.removeItem(this.tokenKey); + localStorage.removeItem(this.tokenKey); + } +} +const TokenStorage = new TokenStorageClass(); +export default TokenStorage; diff --git a/server/auth/auth.py b/server/auth/auth.py index 4e2ce67..2de37f7 100644 --- a/server/auth/auth.py +++ b/server/auth/auth.py @@ -10,7 +10,8 @@ from fastapi.security import OAuth2PasswordRequestForm router = APIRouter( - tags=["auth"], + prefix="/auth", + tags=["authentication"], redirect_slashes=True ) @@ -64,12 +65,11 @@ async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> T return Token(access_token=access_token, token_type="bearer") @router.get("/user") -async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> User: +async def get_current_user(token: str = Depends(oauth2_scheme)) -> User: credentials_exception = HTTPException( - status_code=401, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) + status_code=401, + headers={"WWW-Authenticate": "Bearer"}, + detail="Invalid token") try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username = payload.get("sub") @@ -89,7 +89,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> Use @router.post("/user", status_code=201) async def create_user(user: Annotated[User, Depends(get_current_user)], username: str, password: str): if not user.is_admin: - raise HTTPException(status_code=401, detail="Only admins change create new users") + raise HTTPException(status_code=403, detail="Only admins change create new users") password_hash = hash_password(password) database.execute_query(f"INSERT INTO users (username, password_hash) VALUES (?, ?)", @@ -98,15 +98,15 @@ async def create_user(user: Annotated[User, Depends(get_current_user)], username class UserPatch(CustomModel): username: str|None = None password: str|None = None - is_admin: str|None = None + is_admin: bool|None = None @router.patch("/user", status_code=200) async def update_user(user: Annotated[User, Depends(get_current_user)], username: str, new_user: UserPatch): if user.username != username and not user.is_admin: - raise HTTPException(status_code=401, detail="Only admins can edit other users") + raise HTTPException(status_code=403, detail="Only admins can edit other users") if new_user.is_admin and (user.username == username or not user.is_admin): - raise HTTPException(status_code=401, detail="Only admins can change the admin status of other users") - + raise HTTPException(status_code=403, detail="Only admins can change the admin status of other users") + query = "UPDATE users SET " values = [] @@ -127,6 +127,4 @@ async def update_user(user: Annotated[User, Depends(get_current_user)], username query = query[:-1] query += f" WHERE username = '{username}';" - print(query) - print(values) database.execute_query(query, values) diff --git a/server/auth/utils.py b/server/auth/utils.py index 605b420..f7ace9d 100644 --- a/server/auth/utils.py +++ b/server/auth/utils.py @@ -1,7 +1,7 @@ from fastapi.security import OAuth2PasswordBearer import bcrypt -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/token", auto_error=False) def verify_password(password: str, password_hash: str) -> bool: password_bytes = password.encode('utf-8') diff --git a/server/main.py b/server/main.py index ed15da1..ce96772 100644 --- a/server/main.py +++ b/server/main.py @@ -1,6 +1,6 @@ from server.routers import flights, airports, statistics, geography, importing, exporting from server.auth import auth -from fastapi import FastAPI +from fastapi import FastAPI, Depends from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from pathlib import Path @@ -17,20 +17,23 @@ app = FastAPI(openapi_tags=tags_metadata) build_path = Path(__file__).parent.parent / 'dist' -app.include_router(flights.router, prefix="/api") -app.include_router(airports.router, prefix="/api") -app.include_router(statistics.router, prefix="/api") -app.include_router(geography.router, prefix="/api") -app.include_router(importing.router, prefix="/api") -app.include_router(exporting.router, prefix="/api") +auth_dependency = [Depends(auth.get_current_user)] -app.include_router(auth.router, prefix="/auth") +app.include_router(flights.router, prefix="/api", dependencies=auth_dependency) +app.include_router(airports.router, prefix="/api", dependencies=auth_dependency) +app.include_router(statistics.router, prefix="/api", dependencies=auth_dependency) +app.include_router(geography.router, prefix="/api", dependencies=auth_dependency) +app.include_router(importing.router, prefix="/api", dependencies=auth_dependency) +app.include_router(exporting.router, prefix="/api", dependencies=auth_dependency) + +app.include_router(auth.router, prefix="/api") @app.get("/", include_in_schema=False) @app.get("/new", include_in_schema=False) @app.get("/flights", include_in_schema=False) @app.get("/statistics", include_in_schema=False) @app.get("/settings", include_in_schema=False) +@app.get("/login", include_in_schema=False) async def root(): with open(build_path / 'index.html', "r") as file: html = file.read() From 396e3b8e4733e9a4f13af52e52ca018d519015ab Mon Sep 17 00:00:00 2001 From: Pietro Bonaldo Date: Mon, 7 Oct 2024 17:01:58 +0200 Subject: [PATCH 06/21] Add user_id field to flight model --- client/models.ts | 1 + server/database.py | 1 + server/models.py | 11 ++++++----- server/routers/exporting.py | 6 +++--- server/routers/flights.py | 22 ++++++++++++++-------- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/client/models.ts b/client/models.ts index 76bda48..3b44ac3 100644 --- a/client/models.ts +++ b/client/models.ts @@ -1,5 +1,6 @@ export class Flight { id: number; + userId: number; date: string; origin: Airport; destination: Airport; diff --git a/server/database.py b/server/database.py index 91512e5..9cab546 100644 --- a/server/database.py +++ b/server/database.py @@ -13,6 +13,7 @@ class Database(): "pragma": """ ( id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL DEFAULT 1, date TEXT NOT NULL, origin TEXT NOT NULL, destination TEXT NOT NULL, diff --git a/server/models.py b/server/models.py index b5fc1d9..aa26412 100644 --- a/server/models.py +++ b/server/models.py @@ -37,13 +37,13 @@ def from_database(cls, db: tuple, explicit: dict|None = None): return instance @classmethod - def get_attributes(cls, with_id: bool = True) -> list[str]: + def get_attributes(cls, ignore: list = []) -> list[str]: attributes = list(cls.__fields__.keys()) - if with_id: - return attributes + for ignored_attr in ignore: + attributes.remove(ignored_attr) - return attributes[1:] + return attributes @classmethod def validate_single_field(cls, key, value): @@ -118,6 +118,7 @@ def icao_must_exist(cls, v) -> str|None: class FlightModel(CustomModel): # all optional to accomodate patch id: int|None = None + user_id: int|None = None date: datetime.date|None = None origin: AirportModel|str|None = None destination: AirportModel|str|None = None @@ -160,7 +161,7 @@ def time_must_be_hh_mm(cls, v) -> str|None: def get_values(self) -> list: values = [] - for attr in FlightModel.get_attributes(False): + for attr in FlightModel.get_attributes(ignore=["id"]): value = getattr(self, attr) if type(value) == AirportModel: diff --git a/server/routers/exporting.py b/server/routers/exporting.py index b0e6c76..70d2c52 100644 --- a/server/routers/exporting.py +++ b/server/routers/exporting.py @@ -18,7 +18,7 @@ def cleanup(file_path: str): def stringify_airport(airport: AirportModel) -> str: code = airport.iata if airport.iata else airport.icao - return f"{code} - {airport.city}/{airport.country}" + return f"{code} - {airport.municipality}/{airport.country}" @router.post("/csv", status_code=200) async def export_to_CSV() -> FileResponse: @@ -26,7 +26,7 @@ async def export_to_CSV() -> FileResponse: assert type(flights) == list # make linter happy file = open("/tmp/jetlog.csv", "a") - columns = FlightModel.get_attributes(with_id=False) + columns = FlightModel.get_attributes(ignore=["id"]) file.write(','.join(columns) + '\n') @@ -56,7 +56,7 @@ async def export_to_iCal() -> FileResponse: assert type(flight.destination) == AirportModel file.write("BEGIN:VEVENT\n") - file.write(f"SUMMARY:Flight from {flight.origin.city} to {flight.destination.city}\n") + file.write(f"SUMMARY:Flight from {flight.origin.municipality} to {flight.destination.municipality}\n") file.write(f"DESCRIPTION:Origin: {stringify_airport(flight.origin)}\\n" + f"Destination: {stringify_airport(flight.destination)}" + (f"\\n\\nNotes: {flight.notes}" if flight.notes else "") + diff --git a/server/routers/flights.py b/server/routers/flights.py index c29e3f0..99d0396 100644 --- a/server/routers/flights.py +++ b/server/routers/flights.py @@ -1,6 +1,8 @@ from server.database import database -from server.models import AirportModel, FlightModel -from fastapi import APIRouter, HTTPException +from server.auth.auth import get_current_user +from server.models import AirportModel, FlightModel, User + +from fastapi import APIRouter, Depends, HTTPException from enum import Enum import datetime import math @@ -49,7 +51,7 @@ def spherical_distance(origin: AirportModel, destination: AirportModel) -> int: return round(distance); @router.post("", status_code=201) -async def add_flight(flight: FlightModel) -> int: +async def add_flight(flight: FlightModel, user: User = Depends(get_current_user)) -> int: if not (flight.date and flight.origin and flight.destination): raise HTTPException(status_code=404, detail="Insufficient flight data. Date, Origin, and Destination are required") @@ -91,7 +93,7 @@ async def add_flight(flight: FlightModel) -> int: flight.duration = round(delta_minutes) - columns = FlightModel.get_attributes(False) + columns = FlightModel.get_attributes(ignore=["id"]) query = "INSERT INTO flights (" for attr in columns: @@ -102,6 +104,7 @@ async def add_flight(flight: FlightModel) -> int: query += ") RETURNING id;" values = flight.get_values() + values[0] = user.id; return database.execute_query(query, values) @@ -112,7 +115,7 @@ async def update_flight(id: int, new_flight: FlightModel) -> int: query = "UPDATE flights SET " - for attr in FlightModel.get_attributes(False): + for attr in FlightModel.get_attributes(ignore=["id", "user_id"]): value = getattr(new_flight, attr) if value: query += f"{attr}=?," if value else "" @@ -143,11 +146,12 @@ async def get_flights(id: int|None = None, order: Order = Order.DESCENDING, sort: Sort = Sort.DATE, start: datetime.date|None = None, - end: datetime.date|None = None) -> list[FlightModel]|FlightModel: + end: datetime.date|None = None, + ) -> list[FlightModel]|FlightModel: id_filter = f"WHERE f.id = {str(id)}" if id else "" - date_filter_start = "WHERE" if not id and (start or end) else "AND" if start or end else "" + date_filter_start = "WHERE" if id and (start or end) else "AND" if start or end else "" date_filter = "" date_filter += f"JULIANDAY(date) > JULIANDAY('{start}')" if start else "" @@ -171,7 +175,9 @@ async def get_flights(id: int|None = None, res = database.execute_read_query(query); # get rid of origin, destination ICAOs for proper conversion - res = [ flight_db[:2] + flight_db[4:] for flight_db in res ] + # after this, each flight_db is in the format: + # [id, user_id, date, departure_time, ..., AirportModel, AirportModel] + res = [ flight_db[:3] + flight_db[5:] for flight_db in res ] flights = [] From 22513c18663fec217fd124502f1a61e0bd6fa0ad Mon Sep 17 00:00:00 2001 From: Pietro Bonaldo Date: Mon, 7 Oct 2024 17:23:09 +0200 Subject: [PATCH 07/21] (refactor) Use different Flight Model for patches --- server/models.py | 50 ++++++++++++++++++------------------- server/routers/exporting.py | 2 +- server/routers/flights.py | 23 +++++++++++++---- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/server/models.py b/server/models.py index aa26412..7f90201 100644 --- a/server/models.py +++ b/server/models.py @@ -43,7 +43,28 @@ def get_attributes(cls, ignore: list = []) -> list[str]: for ignored_attr in ignore: attributes.remove(ignored_attr) - return attributes + return attributes + + def get_values(self, ignore: list = [], explicit: dict = {}) -> list: + values = [] + + for attr in self.get_attributes(ignore): + if attr in explicit: + values.append(explicit[attr]) + continue + + value = getattr(self, attr) + + if type(value) == AirportModel: + value = value.icao + elif type(value) == datetime.date: + value = value.isoformat() + elif type(value) == SeatType or type(value) == ClassType: + value = value.value + + values.append(value) + + return values @classmethod def validate_single_field(cls, key, value): @@ -112,16 +133,12 @@ def icao_must_exist(cls, v) -> str|None: return v -# note: for airports, the database type -# is string (icao code), while the type -# returned by the API is AirportModel class FlightModel(CustomModel): - # all optional to accomodate patch id: int|None = None user_id: int|None = None - date: datetime.date|None = None - origin: AirportModel|str|None = None - destination: AirportModel|str|None = None + date: datetime.date + origin: AirportModel|str # API uses AirportModel/str, database uses str + destination: AirportModel|str departure_time: str|None = None arrival_time: str|None = None arrival_date: datetime.date|None = None @@ -158,23 +175,6 @@ def time_must_be_hh_mm(cls, v) -> str|None: return v - def get_values(self) -> list: - values = [] - - for attr in FlightModel.get_attributes(ignore=["id"]): - value = getattr(self, attr) - - if type(value) == AirportModel: - value = value.icao - elif type(value) == datetime.date: - value = value.isoformat() - elif type(value) == SeatType or type(value) == ClassType: - value = value.value - - values.append(value) - - return values - class StatisticsModel(CustomModel): total_flights: int total_duration: int diff --git a/server/routers/exporting.py b/server/routers/exporting.py index 70d2c52..b05a310 100644 --- a/server/routers/exporting.py +++ b/server/routers/exporting.py @@ -31,7 +31,7 @@ async def export_to_CSV() -> FileResponse: file.write(','.join(columns) + '\n') for flight in flights: - values = [ str(val) if val != None else '' for val in flight.get_values() ] + values = [ str(val) if val != None else '' for val in flight.get_values(ignore=["id"]) ] row = ','.join(values) file.write(row + '\n') diff --git a/server/routers/flights.py b/server/routers/flights.py index 99d0396..595be2b 100644 --- a/server/routers/flights.py +++ b/server/routers/flights.py @@ -1,6 +1,6 @@ from server.database import database from server.auth.auth import get_current_user -from server.models import AirportModel, FlightModel, User +from server.models import AirportModel, ClassType, CustomModel, FlightModel, SeatType, User from fastapi import APIRouter, Depends, HTTPException from enum import Enum @@ -103,19 +103,32 @@ async def add_flight(flight: FlightModel, user: User = Depends(get_current_user) query = query[:-1] query += ") RETURNING id;" - values = flight.get_values() - values[0] = user.id; + values = flight.get_values(ignore=["id"], explicit={"user_id": user.id}) return database.execute_query(query, values) +class FlightPatchModel(CustomModel): + date: datetime.date|None = None + origin: AirportModel|str|None = None + destination: AirportModel|str|None = None + departure_time: str|None = None + arrival_time: str|None = None + arrival_date: datetime.date|None = None + seat: SeatType|None = None + ticket_class: ClassType|None = None + duration: int|None = None + distance: int|None = None + airplane: str|None = None + flight_number: str|None = None + notes: str|None = None @router.patch("", status_code=200) -async def update_flight(id: int, new_flight: FlightModel) -> int: +async def update_flight(id: int, new_flight: FlightPatchModel) -> int: if new_flight.empty(): return id query = "UPDATE flights SET " - for attr in FlightModel.get_attributes(ignore=["id", "user_id"]): + for attr in FlightPatchModel.get_attributes(): value = getattr(new_flight, attr) if value: query += f"{attr}=?," if value else "" From aef5a08233ef7a536a1aa8c8691460f147b3c8ee Mon Sep 17 00:00:00 2001 From: Pietro Bonaldo Date: Mon, 7 Oct 2024 18:34:29 +0200 Subject: [PATCH 08/21] Make flights only accessible to associated user --- server/routers/flights.py | 34 ++++++++++++++++++++++++---------- server/routers/geography.py | 24 +++++++++++++++--------- server/routers/statistics.py | 35 ++++++++++++++++++++++------------- 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/server/routers/flights.py b/server/routers/flights.py index 595be2b..6e7683b 100644 --- a/server/routers/flights.py +++ b/server/routers/flights.py @@ -24,6 +24,13 @@ class Sort(str, Enum): DURATION = "duration" DISTANCE = "distance" +async def check_flight_authorization(id: int, user: User) -> None: + res = database.execute_read_query(f"SELECT user_id FROM flights WHERE id = {str(id)};") + flight_user_id = res[0][0] + + if flight_user_id != user.id: + raise HTTPException(status_code=403, detail="You are not authorized to access this flight") + # https://en.wikipedia.org/wiki/Haversine_formula def spherical_distance(origin: AirportModel, destination: AirportModel) -> int: if not origin.latitude or not origin.longitude or not destination.latitude or not destination.longitude: @@ -122,7 +129,11 @@ class FlightPatchModel(CustomModel): flight_number: str|None = None notes: str|None = None @router.patch("", status_code=200) -async def update_flight(id: int, new_flight: FlightPatchModel) -> int: +async def update_flight(id: int, + new_flight: FlightPatchModel, + user: User = Depends(get_current_user)) -> int: + await check_flight_authorization(id, user) + if new_flight.empty(): return id @@ -143,7 +154,9 @@ async def update_flight(id: int, new_flight: FlightPatchModel) -> int: return database.execute_query(query, values) @router.delete("", status_code=200) -async def delete_flight(id: int) -> int: +async def delete_flight(id: int, user: User = Depends(get_current_user)) -> int: + await check_flight_authorization(id, user) + return database.execute_query( """ DELETE FROM flights WHERE id = ? RETURNING id; @@ -160,13 +173,13 @@ async def get_flights(id: int|None = None, sort: Sort = Sort.DATE, start: datetime.date|None = None, end: datetime.date|None = None, - ) -> list[FlightModel]|FlightModel: + user: User = Depends(get_current_user)) -> list[FlightModel]|FlightModel: - id_filter = f"WHERE f.id = {str(id)}" if id else "" + user_filter = f"WHERE f.user_id = {str(user.id)}" - date_filter_start = "WHERE" if id and (start or end) else "AND" if start or end else "" + id_filter = f"AND f.id = {str(id)}" if id else "" - date_filter = "" + date_filter = "AND" if start or end else "" date_filter += f"JULIANDAY(date) > JULIANDAY('{start}')" if start else "" date_filter += " AND " if start and end else "" date_filter += f"JULIANDAY(date) < JULIANDAY('{end}')" if end else "" @@ -176,11 +189,12 @@ async def get_flights(id: int|None = None, f.*, o.*, d.* - FROM flights f - JOIN airports o ON LOWER(f.origin) = LOWER(o.icao) - JOIN airports d ON LOWER(f.destination) = LOWER(d.icao) + FROM flights f + JOIN airports o ON UPPER(f.origin) = o.icao + JOIN airports d ON UPPER(f.destination) = d.icao + {user_filter} {id_filter} - {date_filter_start} {date_filter} + {date_filter} ORDER BY f.{sort.value} {order.value} LIMIT {limit} OFFSET {offset};""" diff --git a/server/routers/geography.py b/server/routers/geography.py index c036dea..65bd816 100644 --- a/server/routers/geography.py +++ b/server/routers/geography.py @@ -1,9 +1,13 @@ from server.database import database +from server.auth.auth import get_current_user + from models import CustomModel -from fastapi import APIRouter +from fastapi import APIRouter, Depends from pathlib import Path import json +from server.models import User + router = APIRouter( prefix="/geography", tags=["geography"], @@ -37,12 +41,13 @@ async def get_world_geojson() -> object: return json.loads(geojson_content) @router.get("/markers", status_code=200) -async def get_airport_markers() -> list[Coord]: - query = """ +async def get_airport_markers(user: User = Depends(get_current_user)) -> list[Coord]: + query = f""" SELECT o.latitude, o.longitude, d.latitude, d.longitude FROM flights f - JOIN airports o ON LOWER(f.origin) = LOWER(o.icao) - JOIN airports d ON LOWER(f.destination) = LOWER(d.icao)""" + JOIN airports o ON UPPER(f.origin) = o.icao + JOIN airports d ON UPPER(f.destination) = d.icao + WHERE user_id = {str(user.id)};""" res = database.execute_read_query(query); @@ -80,12 +85,13 @@ async def get_airport_markers() -> list[Coord]: return coordinates @router.get("/lines", status_code=200) -async def get_flight_trajectories() -> list[Trajectory]: - query = """ +async def get_flight_trajectories(user: User = Depends(get_current_user)) -> list[Trajectory]: + query = f""" SELECT o.latitude, o.longitude, d.latitude, d.longitude FROM flights f - JOIN airports o ON LOWER(f.origin) = LOWER(o.icao) - JOIN airports d ON LOWER(f.destination) = LOWER(d.icao)""" + JOIN airports o ON UPPER(f.origin) = o.icao + JOIN airports d ON UPPER(f.destination) = d.icao + WHERE user_id = {str(user.id)};""" res = database.execute_read_query(query); diff --git a/server/routers/statistics.py b/server/routers/statistics.py index 63beb56..58f8aee 100644 --- a/server/routers/statistics.py +++ b/server/routers/statistics.py @@ -1,6 +1,9 @@ from server.database import database -from server.models import StatisticsModel -from fastapi import APIRouter +from server.models import StatisticsModel, User +from server.auth.auth import get_current_user + + +from fastapi import APIRouter, Depends import datetime router = APIRouter( @@ -12,12 +15,18 @@ @router.get("", status_code=200) async def get_statistics(metric: bool = True, start: datetime.date|None = None, - end: datetime.date|None = None): - date_filter = "WHERE " if start or end else "" + end: datetime.date|None = None, + user: User = Depends(get_current_user)): + + user_filter = f"WHERE user_id = {str(user.id)}" + + date_filter = "AND" if start or end else "" date_filter += f"JULIANDAY(f.date) > JULIANDAY('{start}')" if start else "" date_filter += " AND " if start and end else "" date_filter += f"JULIANDAY(f.date) < JULIANDAY('{end}')" if end else "" + filters = f"{user_filter} {date_filter}" + # get simple numerical stats res = database.execute_read_query(f""" SELECT COUNT(*) AS total_flights, @@ -25,23 +34,23 @@ async def get_statistics(metric: bool = True, COALESCE(SUM(distance), 0) AS total_distance, ( SELECT COUNT(DISTINCT ap) FROM ( - SELECT origin AS ap FROM flights f {date_filter} + SELECT origin AS ap FROM flights f {filters} UNION ALL - SELECT destination as ap FROM flights f {date_filter} + SELECT destination as ap FROM flights f {filters} ) ) AS total_unique_airports, COALESCE(( SELECT JULIANDAY(date) - FROM flights f {date_filter} + FROM flights f {filters} ORDER BY date DESC LIMIT 1 ) - ( SELECT JULIANDAY(date) - FROM flights f {date_filter} + FROM flights f {filters} ORDER BY date ASC LIMIT 1 ), 0) AS days_range - FROM flights f {date_filter}; + FROM flights f {filters}; """) statistics_db = res[0] @@ -55,8 +64,8 @@ async def get_statistics(metric: bool = True, a.country FROM airports a JOIN flights f - ON ( LOWER(a.icao) = LOWER(f.origin) OR LOWER(a.icao) = LOWER(f.destination) ) - {date_filter} + ON ( a.icao = UPPER(f.origin) OR a.icao = UPPER(f.destination) ) + {filters} GROUP BY a.icao ORDER BY visits DESC LIMIT 5; @@ -71,7 +80,7 @@ async def get_statistics(metric: bool = True, res = database.execute_read_query(f""" SELECT seat, COUNT(*) AS freq FROM flights f - {date_filter} + {filters} GROUP BY seat ORDER BY freq DESC; """) @@ -82,7 +91,7 @@ async def get_statistics(metric: bool = True, res = database.execute_read_query(f""" SELECT ticket_class, COUNT(*) AS freq FROM flights f - {date_filter} + {filters} GROUP BY ticket_class ORDER BY freq DESC; """) From 35271715f7aeb91d3cf80637dd7f775b544cb4a4 Mon Sep 17 00:00:00 2001 From: Pietro Bonaldo Date: Mon, 7 Oct 2024 18:49:28 +0200 Subject: [PATCH 09/21] Disable all wrapping in flights table --- client/pages/AllFlights.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pages/AllFlights.tsx b/client/pages/AllFlights.tsx index a89fea4..6a5a49b 100644 --- a/client/pages/AllFlights.tsx +++ b/client/pages/AllFlights.tsx @@ -90,7 +90,7 @@ export default function AllFlights() { function TableCell({ text }) { return ( - + {text} ); @@ -98,7 +98,7 @@ function TableCell({ text }) { function TableHeading({ text }) { return ( - + {text} ); From 8a0e7a413b0dc15599f71cd8c3d49907c2817a43 Mon Sep 17 00:00:00 2001 From: Pietro Bonaldo Date: Tue, 15 Oct 2024 18:43:08 +0200 Subject: [PATCH 10/21] UI to edit own user, login page improvements --- client/api.ts | 6 ++-- client/components/Elements.tsx | 7 +++-- client/models.ts | 8 +++++ client/pages/AllFlights.tsx | 1 - client/pages/Login.tsx | 18 ++++++++++-- client/pages/Settings.tsx | 54 ++++++++++++++++++++++++++++++++-- client/pages/Statistics.tsx | 1 - 7 files changed, 82 insertions(+), 13 deletions(-) diff --git a/client/api.ts b/client/api.ts index 993bef8..3e2dcfb 100644 --- a/client/api.ts +++ b/client/api.ts @@ -1,5 +1,3 @@ -import { useNavigate } from 'react-router-dom'; - import axios, {Axios} from 'axios'; import TokenStorage from './storage/tokenStorage'; @@ -36,7 +34,9 @@ class APIClass { private handleError(err: any) { if (err.response) { if (err.response.status === 401) { - window.location.href = "/login"; + if (window.location.pathname !== "/login") { + window.location.href = "/login"; + } } else { alert("Bad response: " + err.response.data.detail); diff --git a/client/components/Elements.tsx b/client/components/Elements.tsx index 66b9c05..1100b8f 100644 --- a/client/components/Elements.tsx +++ b/client/components/Elements.tsx @@ -71,7 +71,7 @@ export function Button({ text, }; return ( - diff --git a/client/pages/Settings.tsx b/client/pages/Settings.tsx index 1e94ea0..4688b0c 100644 --- a/client/pages/Settings.tsx +++ b/client/pages/Settings.tsx @@ -1,14 +1,24 @@ -import React, {useState} from 'react'; +import React, {useState, useEffect} from 'react'; import { useNavigate } from 'react-router-dom'; import API from '../api'; -import {Heading, Label, Input, Checkbox, Subheading, Button} from '../components/Elements' +import {Heading, Label, Input, Checkbox, Subheading, Button, Dialog} from '../components/Elements' import ConfigStorage, {ConfigInterface} from '../storage/configStorage'; +import {User} from '../models'; +import TokenStorage from '../storage/tokenStorage'; export default function Settings() { const [options, setOptions] = useState(ConfigStorage.getAllSettings()) + const [user, setUser] = useState(); const navigate = useNavigate(); + useEffect(() => { + API.get("/auth/user") + .then((data) => { + setUser(data); + }); + }, []); + const handleImportSubmit = (event) => { event.preventDefault(); const formData = new FormData(event.currentTarget); @@ -41,6 +51,18 @@ export default function Settings() { ConfigStorage.setSetting(key, value); } + const editUser = (event) => { + let userPatchData = Object.fromEntries(new FormData(event.currentTarget)); + userPatchData = Object.fromEntries(Object.entries(userPatchData).filter(([_, v]) => v != "")); + + API.patch(`/auth/user?username=${user?.username}`, userPatchData); + } + + const logout = () => { + TokenStorage.clearToken(); + window.location.href = "/login"; + } + return ( <> @@ -93,6 +115,34 @@ export default function Settings() { onChange={handleOptionChange} /> + +
+ + { user === undefined ? +

Loading...

+ : + <> +

Username: {user.username}

+

Admin: {user.isAdmin.toString()}

+

Last login: {user.lastLogin}

+

Created on: {user.createdOn}

+ + +
); diff --git a/client/pages/Statistics.tsx b/client/pages/Statistics.tsx index d8bc744..821ad0f 100644 --- a/client/pages/Statistics.tsx +++ b/client/pages/Statistics.tsx @@ -11,7 +11,6 @@ export default function Statistics() { const [filters, setFilters] = useState(); const handleSubmit = (event) => { - event.preventDefault(); const formData = new FormData(event.currentTarget); var filters = {} From 03a815d46f952f91e4a0b37668f4dcda64165a24 Mon Sep 17 00:00:00 2001 From: Pietro Bonaldo Date: Wed, 16 Oct 2024 14:56:08 +0200 Subject: [PATCH 11/21] Restructure users API endpoints --- server/auth/auth.py | 40 +++++++++++++++++++++++++++++++--------- server/database.py | 2 +- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/server/auth/auth.py b/server/auth/auth.py index 2de37f7..d23812d 100644 --- a/server/auth/auth.py +++ b/server/auth/auth.py @@ -38,7 +38,7 @@ def create_access_token(data: dict): return encoded_jwt def get_user(username: str) -> User|None: - result = database.execute_read_query(f"SELECT * FROM users WHERE username = '{username}';") + result = database.execute_read_query(f"SELECT * FROM users WHERE username = ?;", [username]) if not result: return None @@ -49,7 +49,7 @@ def get_user(username: str) -> User|None: def update_last_login(username: str) -> None: database.execute_query(f"""UPDATE users SET last_login = current_timestamp - WHERE username = '{username}';""") + WHERE username = ?;""", [username]) @router.post("/token", status_code=200) async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token: @@ -64,7 +64,7 @@ async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> T access_token = create_access_token({"sub": user.username}) return Token(access_token=access_token, token_type="bearer") -@router.get("/user") +@router.get("/users/me") async def get_current_user(token: str = Depends(oauth2_scheme)) -> User: credentials_exception = HTTPException( status_code=401, @@ -86,11 +86,31 @@ async def get_current_user(token: str = Depends(oauth2_scheme)) -> User: return user -@router.post("/user", status_code=201) -async def create_user(user: Annotated[User, Depends(get_current_user)], username: str, password: str): +@router.get("/users/{username}") +async def get_specific_user(username: str, user: User = Depends(get_current_user)) -> User: + if user.username != username and not user.is_admin: + raise HTTPException(status_code=403, detail="Only admins can get other users' details") + + found_user = get_user(username) + if found_user is None: + raise HTTPException(status_code=404, detail=f"User '{username}' not found") + + return found_user + +@router.get("/users") +async def get_all_usernames(user: User = Depends(get_current_user)) -> list[str]: + res = database.execute_read_query("SELECT username FROM users;") + usernames = [username[0] for username in res] + return usernames + +@router.post("/users", status_code=201) +async def create_user(username: str, password: str, user: User = Depends(get_current_user)): if not user.is_admin: raise HTTPException(status_code=403, detail="Only admins change create new users") + if len(username) < 3: + raise HTTPException(status_code=400, detail="Username should be at least 3 characters long") + password_hash = hash_password(password) database.execute_query(f"INSERT INTO users (username, password_hash) VALUES (?, ?)", [username, password_hash]) @@ -100,12 +120,14 @@ class UserPatch(CustomModel): password: str|None = None is_admin: bool|None = None -@router.patch("/user", status_code=200) -async def update_user(user: Annotated[User, Depends(get_current_user)], username: str, new_user: UserPatch): +@router.patch("/users/{username}", status_code=200) +async def update_user(username: str, new_user: UserPatch, user: User = Depends(get_current_user)): if user.username != username and not user.is_admin: raise HTTPException(status_code=403, detail="Only admins can edit other users") - if new_user.is_admin and (user.username == username or not user.is_admin): - raise HTTPException(status_code=403, detail="Only admins can change the admin status of other users") + if new_user.is_admin and not user.is_admin: + raise HTTPException(status_code=403, detail="Only admins can users admin") + if new_user.is_admin and username == user.username: + raise HTTPException(status_code=403, detail="You may only change the admin status of other users") query = "UPDATE users SET " diff --git a/server/database.py b/server/database.py index 9cab546..eb6ac1a 100644 --- a/server/database.py +++ b/server/database.py @@ -34,7 +34,7 @@ class Database(): "pragma": """ ( id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT NOT NULL UNIQUE, + username TEXT NOT NULL UNIQUE COLLATE NOCASE CHECK(LENGTH(username) > 2), password_hash TEXT NOT NULL, is_admin BIT NOT NULL DEFAULT 0, last_login DATETIME, From 5eb7b6ef99b2bf17fbc0595484d5a22e8650f52b Mon Sep 17 00:00:00 2001 From: Pietro Bonaldo Date: Wed, 16 Oct 2024 15:00:54 +0200 Subject: [PATCH 12/21] Use new user endpoints in frontend --- client/pages/Settings.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pages/Settings.tsx b/client/pages/Settings.tsx index 4688b0c..5239908 100644 --- a/client/pages/Settings.tsx +++ b/client/pages/Settings.tsx @@ -13,7 +13,7 @@ export default function Settings() { const navigate = useNavigate(); useEffect(() => { - API.get("/auth/user") + API.get("/auth/users/me") .then((data) => { setUser(data); }); @@ -55,7 +55,7 @@ export default function Settings() { let userPatchData = Object.fromEntries(new FormData(event.currentTarget)); userPatchData = Object.fromEntries(Object.entries(userPatchData).filter(([_, v]) => v != "")); - API.patch(`/auth/user?username=${user?.username}`, userPatchData); + API.patch(`/auth/users/${user?.username}`, userPatchData); } const logout = () => { From 0f26e6a493b2ebae72ed30d232889166cb9c7e27 Mon Sep 17 00:00:00 2001 From: Pietro Bonaldo Date: Wed, 16 Oct 2024 20:02:38 +0200 Subject: [PATCH 13/21] UI user management, minor changes and bug fixes --- client/components/Elements.tsx | 15 +-- client/pages/Settings.tsx | 173 +++++++++++++++++++++++++++------ server/auth/auth.py | 32 +++--- 3 files changed, 172 insertions(+), 48 deletions(-) diff --git a/client/components/Elements.tsx b/client/components/Elements.tsx index 1100b8f..39d5e34 100644 --- a/client/components/Elements.tsx +++ b/client/components/Elements.tsx @@ -1,4 +1,4 @@ -import React, {ChangeEvent} from 'react'; +import React, {ChangeEvent, useState} from 'react'; interface HeadingProps { text: string; @@ -189,17 +189,20 @@ export function Select({name = undefined, interface DialogProps { title: string; + buttonLevel?: "default"|"success"|"danger"; formBody: any; // ? onSubmit: React.FormEventHandler; } -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(); } @@ -211,9 +214,9 @@ export function Dialog({ title, formBody, onSubmit }: DialogProps) { return ( <> -