Start move to FastAPI
This commit is contained in:
@@ -1,15 +1,23 @@
|
||||
from flask import Blueprint, current_app, request, jsonify
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from models import FlightModel
|
||||
|
||||
from mongoengine import DoesNotExist, ValidationError
|
||||
|
||||
from flask_jwt_extended import get_jwt_identity, jwt_required
|
||||
|
||||
from database.models import User, Flight, AuthLevel
|
||||
from database.utils import get_flight_list
|
||||
from routes.utils import auth_level_required
|
||||
|
||||
flights_api = Blueprint('flights_api', __name__)
|
||||
router = APIRouter()
|
||||
|
||||
logger = logging.getLogger("flights")
|
||||
|
||||
|
||||
@flights_api.route('/flights', methods=['GET'])
|
||||
@router.get('/flights')
|
||||
@jwt_required()
|
||||
def get_flights():
|
||||
"""
|
||||
@@ -20,13 +28,14 @@ def get_flights():
|
||||
try:
|
||||
user = User.objects.get(username=get_jwt_identity())
|
||||
except DoesNotExist:
|
||||
current_app.logger.warning("User %s not found", get_jwt_identity())
|
||||
logger.warning("User %s not found", get_jwt_identity())
|
||||
return {"msg": "user not found"}, 401
|
||||
flights = Flight.objects(user=user.id).to_json()
|
||||
|
||||
flights = get_flight_list(filters=[[{"field": "user", "operator": "eq", "value": user.id}]]).to_json()
|
||||
return flights, 200
|
||||
|
||||
|
||||
@flights_api.route('/flights/all', methods=['GET'])
|
||||
@router.get('/flights/all')
|
||||
@jwt_required()
|
||||
@auth_level_required(AuthLevel.ADMIN)
|
||||
def get_all_flights():
|
||||
@@ -35,13 +44,14 @@ def get_all_flights():
|
||||
|
||||
:return: List of flights
|
||||
"""
|
||||
flights = Flight.objects.to_json()
|
||||
logger.debug("Get all flights - user: %s", get_jwt_identity())
|
||||
flights = get_flight_list().to_json()
|
||||
return flights, 200
|
||||
|
||||
|
||||
@flights_api.route('/flights/<flight_id>', methods=['GET'])
|
||||
@router.get('/flights/{flight_id}', response_model=FlightModel)
|
||||
@jwt_required()
|
||||
def get_flight(flight_id):
|
||||
def get_flight(flight_id: str):
|
||||
"""
|
||||
Get all details of a given flight
|
||||
|
||||
@@ -51,19 +61,20 @@ def get_flight(flight_id):
|
||||
try:
|
||||
user = User.objects.get(username=get_jwt_identity())
|
||||
except DoesNotExist:
|
||||
current_app.logger.warning("User %s not found", get_jwt_identity())
|
||||
return {"msg": "user not found"}, 401
|
||||
logger.warning("User %s not found", get_jwt_identity())
|
||||
raise HTTPException(401, "User not found")
|
||||
|
||||
flight = Flight.objects(id=flight_id).to_json()
|
||||
if flight.user != user.id and AuthLevel(user.level) != AuthLevel.ADMIN:
|
||||
current_app.logger.warning("Attempted access to unauthorized flight by %s", user.username)
|
||||
return {"msg": "Unauthorized access"}, 403
|
||||
return flight, 200
|
||||
logger.info("Attempted access to unauthorized flight by %s", user.username)
|
||||
raise HTTPException(403, "Unauthorized access")
|
||||
|
||||
return flight
|
||||
|
||||
|
||||
@flights_api.route('/flights', methods=['POST'])
|
||||
@router.post('/flights')
|
||||
@jwt_required()
|
||||
def add_flight():
|
||||
def add_flight(flight_body: FlightModel):
|
||||
"""
|
||||
Add a flight logbook entry
|
||||
|
||||
@@ -72,64 +83,64 @@ def add_flight():
|
||||
try:
|
||||
user = User.objects.get(username=get_jwt_identity())
|
||||
except DoesNotExist:
|
||||
current_app.logger.warning("User %s not found", get_jwt_identity())
|
||||
return {"msg": "user not found"}, 401
|
||||
logger.warning("User %s not found", get_jwt_identity())
|
||||
raise HTTPException(401, "User not found")
|
||||
|
||||
body = request.get_json()
|
||||
try:
|
||||
flight = Flight(user=user, **body).save()
|
||||
except ValidationError:
|
||||
return jsonify({"msg": "Invalid request"})
|
||||
id = flight.id
|
||||
return jsonify({'id': str(id)}), 201
|
||||
flight = Flight(user=user.id, **flight_body.model_dump()).save()
|
||||
except ValidationError as e:
|
||||
logger.info("Invalid flight body: %s", e)
|
||||
raise HTTPException(400, "Invalid request")
|
||||
|
||||
return {"id": flight.id}
|
||||
|
||||
|
||||
@flights_api.route('/flights/<flight_id>', methods=['PUT'])
|
||||
@router.put('/flights/{flight_id}', status_code=201, response_model=FlightModel)
|
||||
@jwt_required()
|
||||
def update_flight(flight_id):
|
||||
def update_flight(flight_id: str, flight_body: FlightModel):
|
||||
"""
|
||||
Update the given flight with new information
|
||||
|
||||
:param flight_id: ID of flight to update
|
||||
:return: Error messages if user not found or access unauthorized, else 200
|
||||
:param flight_body: New flight information to update with
|
||||
:return: Updated flight
|
||||
"""
|
||||
try:
|
||||
user = User.objects.get(username=get_jwt_identity())
|
||||
except DoesNotExist:
|
||||
current_app.logger.warning("User %s not found", get_jwt_identity())
|
||||
return {"msg": "user not found"}, 401
|
||||
logger.warning("User %s not found", get_jwt_identity())
|
||||
raise HTTPException(status_code=401, detail="user not found")
|
||||
|
||||
flight = Flight.objects(id=flight_id)
|
||||
|
||||
if flight.user != user and AuthLevel(user.level) != AuthLevel.ADMIN:
|
||||
current_app.logger.warning("Attempted access to unauthorized flight by %s", user.username)
|
||||
return {"msg": "Unauthorized access"}, 403
|
||||
logger.info("Attempted access to unauthorized flight by %s", user.username)
|
||||
raise HTTPException(403, "Unauthorized access")
|
||||
|
||||
body = request.get_json()
|
||||
flight.update(**body)
|
||||
flight.update(**flight_body.model_dump())
|
||||
|
||||
return '', 200
|
||||
return flight_body
|
||||
|
||||
|
||||
@flights_api.route('/flights/<flight_id>', methods=['DELETE'])
|
||||
def delete_flight(flight_id):
|
||||
@router.delete('/flights/{flight_id}', status_code=200)
|
||||
def delete_flight(flight_id: str):
|
||||
"""
|
||||
Delete the given flight
|
||||
|
||||
:param flight_id: ID of flight to delete
|
||||
:return: Error messages if user not found or access unauthorized, else 200
|
||||
:return: 200
|
||||
"""
|
||||
try:
|
||||
user = User.objects.get(username=get_jwt_identity())
|
||||
except DoesNotExist:
|
||||
current_app.logger.warning("User %s not found", get_jwt_identity())
|
||||
return {"msg": "user not found"}, 401
|
||||
logger.warning("User %s not found", get_jwt_identity())
|
||||
raise HTTPException(401, "user not found")
|
||||
|
||||
flight = Flight.objects(id=flight_id)
|
||||
|
||||
if flight.user != user and AuthLevel(user.level) != AuthLevel.ADMIN:
|
||||
current_app.logger.warning("Attempted access to unauthorized flight by %s", user.username)
|
||||
return {"msg": "Unauthorized access"}, 403
|
||||
logger.info("Attempted access to unauthorized flight by %s", user.username)
|
||||
raise HTTPException(403, "Unauthorized access")
|
||||
|
||||
flight.delete()
|
||||
|
||||
|
@@ -1,73 +1,74 @@
|
||||
import bcrypt
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from flask_jwt_extended import create_access_token, get_jwt, get_jwt_identity, unset_jwt_cookies, jwt_required, \
|
||||
JWTManager
|
||||
from mongoengine import DoesNotExist, ValidationError
|
||||
|
||||
from database.models import AuthLevel, User, Flight
|
||||
from models import UserModel
|
||||
from routes.utils import auth_level_required
|
||||
|
||||
users_api = Blueprint('users_api', __name__)
|
||||
router = APIRouter()
|
||||
|
||||
logger = logging.getLogger("users")
|
||||
|
||||
|
||||
@users_api.route('/users', methods=["POST"])
|
||||
@router.post('/users', status_code=201)
|
||||
@jwt_required()
|
||||
@auth_level_required(AuthLevel.ADMIN)
|
||||
def add_user():
|
||||
def add_user(body: UserModel):
|
||||
"""
|
||||
Add user to database.
|
||||
|
||||
:return: Failure message if user already exists, otherwise ID of newly created user
|
||||
"""
|
||||
body = request.get_json()
|
||||
try:
|
||||
username = body["username"]
|
||||
password = body["password"]
|
||||
except KeyError:
|
||||
return jsonify({"msg": "Missing username or password"})
|
||||
try:
|
||||
auth_level = AuthLevel(body["auth_level"])
|
||||
except KeyError:
|
||||
auth_level = AuthLevel.USER
|
||||
|
||||
auth_level = body.level if body.level is not None else AuthLevel.USER
|
||||
|
||||
try:
|
||||
existing_user = User.objects.get(username=username)
|
||||
current_app.logger.info("User %s already exists at auth level %s", existing_user.username, existing_user.level)
|
||||
return jsonify({"msg": "Username already exists"})
|
||||
existing_user = User.objects.get(username=body.username)
|
||||
logger.debug("User %s already exists at auth level %s", existing_user.username, existing_user.level)
|
||||
return {"msg": "Username already exists"}
|
||||
|
||||
except DoesNotExist:
|
||||
current_app.logger.info("Creating user %s with auth level %s", username, auth_level)
|
||||
logger.info("Creating user %s with auth level %s", body.username, auth_level)
|
||||
|
||||
hashed_password = bcrypt.hashpw(body.password.encode('utf-8'), bcrypt.gensalt())
|
||||
user = User(username=body.username, password=hashed_password, level=auth_level)
|
||||
|
||||
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
|
||||
user = User(username=username, password=hashed_password, level=auth_level.value)
|
||||
try:
|
||||
user.save()
|
||||
except ValidationError:
|
||||
return jsonify({"msg": "Invalid request"})
|
||||
raise HTTPException(400, "Invalid request")
|
||||
|
||||
return jsonify({"id": str(user.id)}), 201
|
||||
return {"id": str(user.id)}
|
||||
|
||||
|
||||
@users_api.route('/users/<user_id>', methods=['DELETE'])
|
||||
@router.delete('/users/{user_id}', status_code=200)
|
||||
@jwt_required()
|
||||
@auth_level_required(AuthLevel.ADMIN)
|
||||
def remove_user(user_id):
|
||||
def remove_user(user_id: str):
|
||||
"""
|
||||
Delete given user from database
|
||||
Delete given user from database along with all flights associated with said user
|
||||
|
||||
:param user_id: ID of user to delete
|
||||
:return: 200 if success, 401 if user does not exist
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
# Delete user from database
|
||||
User.objects.get(id=user_id).delete()
|
||||
except DoesNotExist:
|
||||
current_app.logger.info("Attempt to delete nonexistent user %s by %s", user_id, get_jwt_identity())
|
||||
return {"msg": "User does not exist"}, 401
|
||||
logger.info("Attempt to delete nonexistent user %s by %s", user_id, get_jwt_identity())
|
||||
raise HTTPException(401, "User does not exist")
|
||||
|
||||
# Delete all flights associated with the user
|
||||
Flight.objects(user=user_id).delete()
|
||||
return '', 200
|
||||
|
||||
|
||||
@users_api.route('/users', methods=["GET"])
|
||||
@router.get('/users', status_code=200, response_model=list[UserModel])
|
||||
@jwt_required()
|
||||
@auth_level_required(AuthLevel.ADMIN)
|
||||
def get_users():
|
||||
@@ -77,115 +78,111 @@ def get_users():
|
||||
:return: List of users in the database
|
||||
"""
|
||||
users = User.objects.to_json()
|
||||
return users, 200
|
||||
return users
|
||||
|
||||
|
||||
@users_api.route('/login', methods=["POST"])
|
||||
def create_token():
|
||||
@router.post('/login', status_code=200)
|
||||
def create_token(body: UserModel):
|
||||
"""
|
||||
Log in as given user and return JWT for API access
|
||||
Log in as given user - create associated JWT for API access
|
||||
|
||||
:return: 401 if username or password invalid, else JWT
|
||||
:return: JWT for given user
|
||||
"""
|
||||
body = request.get_json()
|
||||
try:
|
||||
username = body["username"]
|
||||
password = body["password"]
|
||||
except KeyError:
|
||||
return jsonify({"msg": "Missing username or password"})
|
||||
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
user = User.objects.get(username=body.username)
|
||||
except DoesNotExist:
|
||||
return jsonify({"msg": "Invalid username or password"}), 401
|
||||
raise HTTPException(401, "Invalid username or password")
|
||||
else:
|
||||
if bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-8')):
|
||||
access_token = create_access_token(identity=username)
|
||||
current_app.logger.info("%s successfully logged in", username)
|
||||
response = {"access_token": access_token}
|
||||
return jsonify(response), 200
|
||||
current_app.logger.info("Failed login attempt from %s", request.remote_addr)
|
||||
return jsonify({"msg": "Invalid username or password"}), 401
|
||||
if bcrypt.checkpw(body.password.encode('utf-8'), user.password.encode('utf-8')):
|
||||
access_token = create_access_token(identity=body.username)
|
||||
logger.info("%s successfully logged in", body.username)
|
||||
return {"access_token": access_token}
|
||||
|
||||
logger.info("Failed login attempt for user %s", body.username)
|
||||
raise HTTPException(401, "Invalid username or password")
|
||||
|
||||
|
||||
@users_api.route('/logout', methods=["POST"])
|
||||
@router.post('/logout', status_code=200)
|
||||
def logout():
|
||||
"""
|
||||
Log out given user. Note that JWTs cannot be natively revoked so this must also be handled by the frontend
|
||||
|
||||
:return: Message with JWT removed from headers
|
||||
"""
|
||||
response = jsonify({"msg": "logout successful"})
|
||||
unset_jwt_cookies(response)
|
||||
response = {"msg": "logout successful"}
|
||||
# unset_jwt_cookies(response)
|
||||
return response
|
||||
|
||||
|
||||
@users_api.route('/profile/<user_id>', methods=["GET"])
|
||||
@router.get('/profile/{user_id}', status_code=200)
|
||||
@jwt_required()
|
||||
@auth_level_required(AuthLevel.ADMIN)
|
||||
def get_user_profile(user_id):
|
||||
def get_user_profile(user_id: str):
|
||||
"""
|
||||
Get profile of the given user
|
||||
|
||||
:param user_id: ID of the requested user
|
||||
:return: 401 is user does not exist, else username and auth level
|
||||
:return: Username and auth level of the requested user
|
||||
"""
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
except DoesNotExist:
|
||||
current_app.logger.warning("User %s not found", get_jwt_identity())
|
||||
return {"msg": "User not found"}, 401
|
||||
return jsonify({"username": user.username, "auth_level:": str(user.level)}), 200
|
||||
logger.warning("User %s not found", get_jwt_identity())
|
||||
raise HTTPException(401, "User not found")
|
||||
|
||||
return {"username": user.username, "auth_level:": str(user.level)}
|
||||
|
||||
|
||||
@users_api.route('/profile/<user_id>', methods=["PUT"])
|
||||
@router.put('/profile/{user_id}', status_code=200)
|
||||
@jwt_required()
|
||||
@auth_level_required(AuthLevel.ADMIN)
|
||||
def update_user_profile(user_id):
|
||||
def update_user_profile(user_id: str, body: UserModel):
|
||||
"""
|
||||
Update the profile of the given user
|
||||
:param user_id: ID of the user to update
|
||||
:param body: New user information to insert
|
||||
:return: Error messages if request is invalid, else 200
|
||||
"""
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
except DoesNotExist:
|
||||
current_app.logger.warning("User %s not found", get_jwt_identity())
|
||||
return jsonify({"msg": "User not found"}), 401
|
||||
logger.warning("User %s not found", get_jwt_identity())
|
||||
raise HTTPException(401, "User not found")
|
||||
|
||||
body = request.get_json()
|
||||
return update_profile(user.id, body["username"], body["password"], body["auth_level"])
|
||||
return update_profile(user.id, body.username, body.password, body.level)
|
||||
|
||||
|
||||
@users_api.route('/profile', methods=["GET"])
|
||||
@router.get('/profile', status_code=200)
|
||||
@jwt_required()
|
||||
def get_profile():
|
||||
"""
|
||||
Return basic user information for the currently logged-in user
|
||||
|
||||
:return: 401 if user not found, else username and auth level
|
||||
:return: Username and auth level of current user
|
||||
"""
|
||||
try:
|
||||
user = User.objects.get(username=get_jwt_identity())
|
||||
except DoesNotExist:
|
||||
current_app.logger.warning("User %s not found", get_jwt_identity())
|
||||
return jsonify({"msg": "User not found"}), 401
|
||||
return jsonify({"username": user.username, "auth_level:": str(user.level)}), 200
|
||||
logger.warning("User %s not found", get_jwt_identity())
|
||||
raise HTTPException(401, "User not found")
|
||||
|
||||
return {"username": user.username, "auth_level:": str(user.level)}
|
||||
|
||||
|
||||
@users_api.route('/profile', methods=["PUT"])
|
||||
@router.put('/profile')
|
||||
@jwt_required()
|
||||
def update_profile():
|
||||
def update_profile(body: UserModel):
|
||||
"""
|
||||
Update the profile of the currently logged-in user
|
||||
|
||||
:return: Error messages if request is invalid, else 200
|
||||
:param body: New information to insert
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
user = User.objects.get(username=get_jwt_identity())
|
||||
except DoesNotExist:
|
||||
current_app.logger.warning("User %s not found", get_jwt_identity())
|
||||
return {"msg": "user not found"}, 401
|
||||
body = request.get_json()
|
||||
logger.warning("User %s not found", get_jwt_identity())
|
||||
raise HTTPException(401, "User not found")
|
||||
|
||||
return update_profile(user.id, body["username"], body["password"], body["auth_level"])
|
||||
|
@@ -1,8 +1,4 @@
|
||||
import os
|
||||
|
||||
import bcrypt
|
||||
from flask import current_app
|
||||
|
||||
from flask_jwt_extended import get_jwt_identity
|
||||
|
||||
from database.models import AuthLevel, User
|
||||
@@ -29,29 +25,3 @@ def auth_level_required(level: AuthLevel):
|
||||
return auth_wrapper
|
||||
|
||||
return auth_inner
|
||||
|
||||
|
||||
def create_admin_user():
|
||||
"""
|
||||
Create default admin user if no admin users are present in the database
|
||||
|
||||
:return: None
|
||||
"""
|
||||
if User.objects(level=AuthLevel.ADMIN.value).count() == 0:
|
||||
current_app.logger.info("No admin users exist. Creating default admin user...")
|
||||
try:
|
||||
admin_username = os.environ["TAILFIN_ADMIN_USERNAME"]
|
||||
current_app.logger.info("Setting admin username to 'TAILFIN_ADMIN_USERNAME': %s", admin_username)
|
||||
except KeyError:
|
||||
admin_username = "admin"
|
||||
current_app.logger.info("'TAILFIN_ADMIN_USERNAME' not set, using default username 'admin'")
|
||||
try:
|
||||
admin_password = os.environ["TAILFIN_ADMIN_PASSWORD"]
|
||||
current_app.logger.info("Setting admin password to 'TAILFIN_ADMIN_PASSWORD'")
|
||||
except KeyError:
|
||||
admin_password = "admin"
|
||||
current_app.logger.warning("'TAILFIN_ADMIN_PASSWORD' not set, using default password 'admin'\n"
|
||||
"Change this as soon as possible")
|
||||
hashed_password = bcrypt.hashpw(admin_password.encode('utf-8'), bcrypt.gensalt())
|
||||
User(username=admin_username, password=hashed_password, level=AuthLevel.ADMIN.value).save()
|
||||
current_app.logger.info("Default admin user created with username %s", User.objects.get(level=AuthLevel.ADMIN).username)
|
||||
|
Reference in New Issue
Block a user