diff --git a/Dockerfile.dev b/Dockerfile.dev index 9e2edce..3f2512d 100755 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -6,6 +6,8 @@ ENV PYTHONUNBUFFERED=1 ENV MODE="DEVELOPMENT" +ENV FLASK_APP=app.py + RUN mkdir -p /app COPY requirements.txt /app diff --git a/Dockerfile.local b/Dockerfile.local index 95bcdfe..2c904bc 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -8,6 +8,8 @@ ENV TZ=America/New_York ENV MODE="LOCAL" +ENV FLASK_APP=app.py + RUN mkdir -p /app COPY requirements.txt /app diff --git a/Dockerfile.prod b/Dockerfile.prod index 260fc2a..ba97ec0 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -17,7 +17,8 @@ COPY . . # Tell Docker that the container listens on port 80 EXPOSE 80 -# Run the application using Gunicorn -# This command runs the Flask app. 'app:app' means "in the file named app.py, run the variable named app". -# Adjust if your main file or Flask app variable is named differently. -CMD ["gunicorn", "--bind", "0.0.0.0:80", "app:app"] \ No newline at end of file +# Set Flask app for CLI commands +ENV FLASK_APP=app.py + +# Run database migrations and then the application +CMD flask db upgrade && gunicorn --bind 0.0.0.0:80 app:app diff --git a/app.py b/app.py index 38abb03..1a990cf 100755 --- a/app.py +++ b/app.py @@ -6,7 +6,7 @@ HOST = '0.0.0.0' if __name__ == '__main__': app.run( - debug=True, + debug=app.config.get('DEBUG', False), host=HOST, port=PORT, use_reloader=True diff --git a/app/__init__.py b/app/__init__.py index c50c1b8..7753b33 100755 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,20 +1,56 @@ # coding=utf-8 +import logging +import sys from flask import Flask, jsonify from flask_bcrypt import Bcrypt from flask_cors import CORS from flask_marshmallow import Marshmallow from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate from flask_session import Session from flask_login import LoginManager from sqlalchemy.orm import sessionmaker from werkzeug.routing import BaseConverter from flask_mail import Mail from config import load_config -import re +import re +from sqlalchemy import text ApplicationConfig = load_config() +# Configure logging +def setup_logging(): + """Configure structured logging for the application.""" + log_level = logging.DEBUG if ApplicationConfig.CURRENT_SETTINGS != 'PRODUCTION' else logging.INFO + + # Create formatter + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + # Configure root logger + root_logger = logging.getLogger() + root_logger.setLevel(log_level) + + # Remove existing handlers to avoid duplicates + root_logger.handlers.clear() + + # Console handler + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(log_level) + console_handler.setFormatter(formatter) + root_logger.addHandler(console_handler) + + # Reduce noise from third-party libraries + logging.getLogger('werkzeug').setLevel(logging.WARNING) + logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING) + + return logging.getLogger('eamco_office_api') + +logger = setup_logging() + app = Flask(__name__, static_url_path='', static_folder='static', @@ -55,6 +91,7 @@ app.config['SECRET_KEY'] = ApplicationConfig.SECRET_KEY session.configure(bind=ApplicationConfig.SQLALCHEMY_DATABASE_URI) db = SQLAlchemy(app) +migrate = Migrate(app, db) bcrypt = Bcrypt(app) app.config['SESSION_SQLALCHEMY'] = db server_session = Session(app) @@ -93,15 +130,8 @@ def load_user_from_request(request): # If no valid key is found in header or args, return None return None -# api_main = { -# "origins": [ApplicationConfig.ORIGIN_URL], -# "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"], -# "allow_headers": ['Authorization', 'application/json', 'authorization', 'Content-Type', -# 'Access-Control-Allow-Headers', 'Origin,Accept', -# 'X-Requested-With', 'Content-Type', 'Access-Control-Request-cMethod', -# 'Access-Control-Request-Headers'] -# } -cors = CORS(app, + +cors = CORS(app, supports_credentials=True, resources={r"/*": {"origins": ApplicationConfig.CORS_ALLOWED_ORIGINS} }) @@ -217,8 +247,32 @@ from .service import service as service_blueprint app.register_blueprint(service_blueprint, url_prefix='/service') +def check_db_connection(): + """ + Test database connectivity. + """ + try: + db.session.execute(text("SELECT 1")) + return True + except Exception: + return False + with app.app_context(): - db.configure_mappers() - db.create_all() db.session.commit() + + # Startup logging + logger.info("🚀 eamco_office_api STARTING") + mode = ApplicationConfig.CURRENT_SETTINGS.upper() + if mode in ['DEVELOPMENT', 'DEV']: + logger.info("🤖🤖🤖🤖🤖 Mode: Development 🤖🤖🤖🤖🤖") + elif mode in ['PRODUCTION', 'PROD']: + logger.info("💀💀💀💀💀💀💀💀💀💀 ⚠️ WARNING PRODUCTION 💀💀💀💀💀💀💀💀💀💀") + logger.info(f"DB: {ApplicationConfig.SQLALCHEMY_DATABASE_URI[:30]}...") + logger.info(f"CORS: {len(ApplicationConfig.CORS_ALLOWED_ORIGINS)} origins configured") + + # Test database connection + if check_db_connection(): + logger.info("DB Connection: ✅ OK") + else: + logger.info("DB Connection: ❌ FAILED") diff --git a/app/admin/views.py b/app/admin/views.py index 1deb862..42626bd 100755 --- a/app/admin/views.py +++ b/app/admin/views.py @@ -1,3 +1,4 @@ +import logging from flask import request, jsonify from flask_login import current_user, logout_user, login_user, login_required from app.admin import admin @@ -7,12 +8,17 @@ from app.classes.pricing import ( Pricing_Oil_Oil, Pricing_Oil_Oil_schema) from app.classes.admin import Admin_Company, Admin_Company_schema, Call +from app.common.decorators import admin_required + +logger = logging.getLogger(__name__) @admin.route("/oil/create", methods=["POST"]) +@admin_required def create_oil_price(): """ Changes the price for oil deliveries """ + logger.info("POST /admin/oil/create - Creating new oil price") now = datetime.utcnow() price_from_supplier = request.json["price_from_supplier"] price_for_customer = request.json["price_for_customer"] @@ -50,10 +56,12 @@ def create_oil_price(): @admin.route("/oil/get", methods=["GET"]) +@admin_required def get_oil_price(): """ gets oil prices """ + logger.info("GET /admin/oil/get - Fetching current oil prices") get_oil_prices = (db.session .query(Pricing_Oil_Oil) .order_by(Pricing_Oil_Oil.date.desc()) @@ -63,7 +71,9 @@ def get_oil_price(): @admin.route("/company/", methods=["GET"]) +@admin_required def get_company(company_id): + logger.info(f"GET /admin/company/{company_id} - Fetching company data") get_data_company = (db.session .query(Admin_Company) .first()) @@ -72,11 +82,12 @@ def get_company(company_id): return jsonify(company_schema.dump(get_data_company)) @admin.route("/voip_routing", methods=["GET"]) - +@admin_required def get_voip_routing(): """ Gets the current VOIP routing (latest Call record's current_phone) """ + logger.info("GET /admin/voip_routing - Fetching current VoIP routing") latest_call = (db.session .query(Call) .order_by(Call.created_at.desc()) diff --git a/app/auth/views.py b/app/auth/views.py index 9ebfb16..4e19f27 100755 --- a/app/auth/views.py +++ b/app/auth/views.py @@ -1,3 +1,4 @@ +import logging from flask import request, jsonify from flask_login import current_user, logout_user, login_required from app.auth import auth @@ -5,9 +6,12 @@ from app import db, bcrypt from datetime import datetime from uuid import uuid4 from app.classes.auth import Auth_User -from app.classes.employee import Employee_Employee +from app.classes.employee import Employee_Employee +from app.schemas import LoginSchema, RegisterSchema, ChangePasswordSchema, validate_request import re +logger = logging.getLogger(__name__) + @auth.route("/whoami", methods=["GET"]) def check_session(): """ @@ -25,7 +29,7 @@ def check_session(): user = db.session.query(Auth_User).filter(Auth_User.api_key == api_key).first() if not user: - print("no user found with that api key") + logger.warning("Authentication failed: no user found with provided API key") return jsonify({"ok": False, "error": "Invalid token"}), 401 # Now, build the complete response with both user and employee data. @@ -73,9 +77,11 @@ def logout(): @auth.route("/login", methods=["POST"]) +@validate_request(LoginSchema) def login(): - username = request.json["username"] - password = request.json["password"] + data = request.validated_data + username = data["username"] + password = data["password"] user = db.session.query(Auth_User).filter_by(username=username).first() @@ -103,15 +109,17 @@ def login(): }), 200 @auth.route("/register", methods=["POST"]) +@validate_request(RegisterSchema) def register_user(): """ Main post function to register a user """ + data = request.validated_data now = datetime.utcnow() - username = request.json["username"] - email = request.json["email"] - password = request.json["password"] + username = data["username"] + email = data["email"] + password = data["password"] part_one_code = uuid4().hex part_two_code = uuid4().hex @@ -172,6 +180,7 @@ def register_user(): @auth.route('/change-password', methods=['POST']) +@validate_request(ChangePasswordSchema) def change_password(): auth_header = request.headers.get('Authorization') if not auth_header: @@ -184,8 +193,9 @@ def change_password(): if not user: return jsonify({"error": "Invalid token"}), 401 - new_password = request.json["new_password"] - new_password_confirm = request.json["password_confirm"] + data = request.validated_data + new_password = data["new_password"] + new_password_confirm = data["password_confirm"] if str(new_password) != str(new_password_confirm): return jsonify({"error": "Error: Incorrect Passwords"}), 200 @@ -214,7 +224,7 @@ def admin_change_password(): if not user: return jsonify({"error": "Invalid token"}), 401 - if user.admin_role != 0: + if user.admin_role == 0: return jsonify({"error": "Admin access required"}), 403 employee_id = request.json.get("employee_id") diff --git a/app/common/decorators.py b/app/common/decorators.py index 5cf0c50..a9193c3 100755 --- a/app/common/decorators.py +++ b/app/common/decorators.py @@ -1,5 +1,5 @@ from flask_login import current_user -from flask import abort +from flask import abort, jsonify from functools import wraps @@ -14,3 +14,12 @@ def login_required(f): return f(*args, **kwargs) return decorated_function + + +def admin_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not current_user.is_authenticated or not current_user.admin_role: + return jsonify({"error": "Admin access required"}), 403 + return f(*args, **kwargs) + return decorated_function diff --git a/app/constants.py b/app/constants.py new file mode 100644 index 0000000..587a211 --- /dev/null +++ b/app/constants.py @@ -0,0 +1,46 @@ +""" +EAMCO Office API Constants + +This file contains all status code constants used throughout the application +to eliminate magic numbers and improve code maintainability. +""" + +class DeliveryStatus: + """Delivery status codes""" + WAITING = 0 + CANCELLED = 1 + OUT_FOR_DELIVERY = 2 + TOMORROW = 3 + PARTIAL_DELIVERY = 4 + ISSUE = 5 + UNKNOWN = 6 + PENDING_PAYMENT = 9 + FINALIZED = 10 + DELIVERED = 11 # New: Replaces previous use of 1 for delivered + +class PaymentStatus: + """Payment status codes""" + UNPAID = 0 + PRE_AUTHORIZED = 1 + PROCESSING = 2 + PAID = 3 + FAILED = 4 + +class AutoStatus: + """Automatic delivery status codes""" + DEFAULT = 0 + WILL_CALL = 1 + READY_FOR_FINALIZATION = 3 + +class TransactionStatus: + """Transaction status codes""" + APPROVED = 0 + DECLINED = 1 + +class CustomerAutomaticStatus: + """Customer automatic delivery status""" + WILL_CALL = 0 + AUTOMATIC = 1 + +# Additional constants can be added here as needed +# For example: ServiceStatus, UserRoles, etc. \ No newline at end of file diff --git a/app/customer/views.py b/app/customer/views.py index d189bba..0ac2627 100755 --- a/app/customer/views.py +++ b/app/customer/views.py @@ -1,8 +1,12 @@ +import logging from flask import request, jsonify from flask_login import login_required from geopy.geocoders import Nominatim from app.customer import customer from app import db +from app.common.decorators import login_required as common_login_required + +logger = logging.getLogger(__name__) from datetime import datetime from app.classes.cards import Card_Card from app.classes.customer import \ @@ -16,6 +20,7 @@ from app.classes.service import Service_Parts from app.classes.admin import Admin_Company from app.classes.auto import Auto_Delivery,Auto_Delivery_schema from app.classes.stats_customer import Stats_Customer +from app.schemas import CreateCustomerSchema, UpdateCustomerSchema, validate_request import string import random @@ -33,8 +38,9 @@ def generate_random_number_string(length): @customer.route("/all", methods=["GET"]) - +@common_login_required def all_customers_around(): + logger.info("GET /customer/all - Fetching all customers") customer_list = db.session \ .query(Customer_Customer) \ .all() @@ -43,12 +49,12 @@ def all_customers_around(): @customer.route("/all/", methods=["GET"]) - +@common_login_required def all_customers(page): """ pagination all customers """ - + logger.info(f"GET /customer/all/{page} - Fetching customers page {page}") per_page_amount = 100 if page is None: offset_limit = 0 @@ -67,9 +73,11 @@ def all_customers(page): @customer.route("/", methods=["GET"]) +@common_login_required def get_a_customer(customer_id): """ """ + logger.info(f"GET /customer/{customer_id} - Fetching customer") get_customer = (db.session .query(Customer_Customer) .filter(Customer_Customer.id == customer_id) @@ -79,10 +87,12 @@ def get_a_customer(customer_id): @customer.route("/description/", methods=["GET"]) +@common_login_required def get_a_customer_description(customer_id): """ - + """ + logger.info(f"GET /customer/description/{customer_id} - Fetching customer description") get_customer_description = (db.session .query(Customer_Description) .filter(Customer_Description.customer_id == customer_id) @@ -112,10 +122,12 @@ def get_a_customer_description(customer_id): @customer.route("/tank/", methods=["GET"]) +@common_login_required def get_a_customer_tank(customer_id): """ - + """ + logger.info(f"GET /customer/tank/{customer_id} - Fetching customer tank info") get_customer_tank = (db.session .query(Customer_Tank_Inspection) .filter(Customer_Tank_Inspection.customer_id == customer_id) @@ -142,56 +154,50 @@ def get_a_customer_tank(customer_id): @customer.route("/create", methods=["POST"]) - +@validate_request(CreateCustomerSchema) +@common_login_required def create_customer(): """ + Create a new customer with validated input data. """ + logger.info("POST /customer/create - Creating new customer") + # Get validated data from request + data = request.validated_data + now = datetime.utcnow() get_company = (db.session .query(Admin_Company) .filter(Admin_Company.id == 1) .first()) - get_company = (db.session - .query(Admin_Company) - .filter(Admin_Company.id == 1) - .first()) - - random_string = generate_random_number_string(6) - + made_account_number = str(get_company.account_prefix) + '-' + str(random_string) see_if_exists = (db.session.query(Customer_Customer).filter(Customer_Customer.account_number == made_account_number).first()) if see_if_exists is not None: - random_string = generate_random_number_string(10) - made_account_number = str(get_company.account_prefix) + '-' + str(random_string) see_if_exists = (db.session.query(Customer_Customer).filter(Customer_Customer.account_number == made_account_number).first()) if see_if_exists is not None: - random_string = generate_random_number_string(10) - made_account_number = str(get_company.account_prefix) + '-' + str(random_string) - see_if_exists = (db.session.query(Customer_Customer).filter(Customer_Customer.account_number == made_account_number).first()) - response_customer_last_name = request.json["customer_last_name"] - response_customer_first_name = request.json["customer_first_name"] - response_customer_town = request.json["customer_town"] - response_customer_state = request.json["customer_state"] - response_customer_zip = request.json["customer_zip"] - response_customer_email = request.json["customer_email"] - response_customer_home_type = request.json["customer_home_type"] - customer_phone_number = request.json["customer_phone_number"] - customer_address = request.json["customer_address"] - customer_apt = request.json["customer_apt"] - customer_description_msg = request.json["customer_description"] - + # Use validated data instead of direct request.json access + response_customer_last_name = data["customer_last_name"] + response_customer_first_name = data["customer_first_name"] + response_customer_town = data["customer_town"] + response_customer_state = data["customer_state"] + response_customer_zip = str(data["customer_zip"]) + response_customer_email = data.get("customer_email") + response_customer_home_type = data["customer_home_type"] + customer_phone_number = data.get("customer_phone_number") + customer_address = data["customer_address"] + customer_apt = data.get("customer_apt") + customer_description_msg = data.get("customer_description") int_customer_home_type = int(response_customer_home_type) - response_customer_zip = str(response_customer_zip) response_customer_state = int(response_customer_state) @@ -204,51 +210,18 @@ def create_customer(): else: the_state = 'MA' - # if response_customer_town == 0: - # the_town = 'Auburn' - # elif response_customer_town == 1: - # the_town = 'Charlton' - # elif response_customer_town == 2: - # the_town = 'Cherry Valley' - # elif response_customer_town == 3: - # the_town = 'Dudley' - # elif response_customer_town == 4: - # the_town = 'Grafton' - # elif response_customer_town == 5: - # the_town = 'Leicester' - # elif response_customer_town == 6: - # the_town = 'Millbury' - # elif response_customer_town == 7: - # the_town = 'N Oxford' - # elif response_customer_town == 8: - # the_town = 'Oxford' - # elif response_customer_town == 9: - # the_town = 'Rochdale' - # elif response_customer_town == 10: - # the_town = 'Shrewsbury' - # elif response_customer_town == 11: - # the_town = 'Southbridge' - # elif response_customer_town == 12: - # the_town = 'Spencer' - # elif response_customer_town == 13: - # the_town = 'Sturbridge' - # elif response_customer_town == 14: - # the_town = 'Webster' - # elif response_customer_town == 15: - # the_town = 'Worcester' - # else: - # the_town = 'NA' + geolocator = Nominatim(user_agent="auburnoil") address_string = customer_address + ' ' + response_customer_town+ ' ' + the_state try: location = geolocator.geocode(address_string) - user_lat =location.latitude + user_lat = location.latitude user_long = location.longitude cor_ad = True - except: - user_lat =None + except Exception: + user_lat = None user_long = None cor_ad = False @@ -323,70 +296,95 @@ def create_customer(): @customer.route("/edit/", methods=["PUT"]) @login_required +@validate_request(UpdateCustomerSchema) def edit_customer(customer_id): """ """ + logger.info(f"PUT /customer/edit/{customer_id} - Editing customer") get_customer = (db.session .query(Customer_Customer) .filter(Customer_Customer.id == customer_id) .first()) + + if not get_customer: + return jsonify({"error": "Customer not found"}), 404 + get_customer_description = (db.session .query(Customer_Description) .filter(Customer_Description.customer_id == customer_id) .first()) - response_customer_last_name = request.json["customer_last_name"] - response_customer_first_name = request.json["customer_first_name"] - response_customer_town = request.json["customer_town"] - response_customer_state = request.json["customer_state"] - response_customer_zip = request.json["customer_zip"] - response_customer_phone_number = request.json["customer_phone_number"] - response_customer_email = request.json["customer_email"] - response_customer_home_type = request.json["customer_home_type"] - response_customer_address = request.json["customer_address"] - response_customer_apt = request.json["customer_apt"] - response_customer_description = request.json["customer_description"] - response_customer_fill_location = request.json["customer_fill_location"] + data = request.validated_data + response_customer_last_name = data.get("customer_last_name") + response_customer_first_name = data.get("customer_first_name") + response_customer_town = data.get("customer_town") + response_customer_state = data.get("customer_state") + response_customer_zip = data.get("customer_zip") + response_customer_phone_number = data.get("customer_phone_number") + response_customer_email = data.get("customer_email") + response_customer_home_type = data.get("customer_home_type") + response_customer_address = data.get("customer_address") + response_customer_apt = data.get("customer_apt") + response_customer_description = data.get("customer_description") + response_customer_fill_location = data.get("customer_fill_location") + # Update description if provided if get_customer_description is not None: - get_customer_description.description = response_customer_description - get_customer_description.fill_location = response_customer_fill_location + if response_customer_description is not None: + get_customer_description.description = response_customer_description + if response_customer_fill_location is not None: + get_customer_description.fill_location = response_customer_fill_location db.session.add(get_customer_description) - if response_customer_state == 0: - the_state = 'MA' - elif response_customer_state == 1: - the_state = 'RI' - elif response_customer_state == 1: - the_state = 'NH' - else: - the_state = 'MA' + # Only update fields that were provided in the request + if response_customer_last_name is not None: + get_customer.customer_last_name = response_customer_last_name + if response_customer_first_name is not None: + get_customer.customer_first_name = response_customer_first_name + if response_customer_town is not None: + get_customer.customer_town = response_customer_town + if response_customer_state is not None: + get_customer.customer_state = response_customer_state + if response_customer_zip is not None: + get_customer.customer_zip = response_customer_zip + if response_customer_phone_number is not None: + get_customer.customer_phone_number = response_customer_phone_number + if response_customer_email is not None: + get_customer.customer_email = response_customer_email + if response_customer_home_type is not None: + get_customer.customer_home_type = response_customer_home_type + if response_customer_apt is not None: + get_customer.customer_apt = response_customer_apt - geolocator = Nominatim(user_agent="auburnoil") - address_string = response_customer_address + ' ' + response_customer_town+ ' ' + the_state - try: - location = geolocator.geocode(address_string, timeout=10) - get_customer.customer_latitude = location.latitude - get_customer.customer_longitude = location.longitude - cor_ad = True - except: - get_customer.customer_latitude = None - get_customer.customer_longitude = None - cor_ad = False - + # Re-geocode if address fields changed + if response_customer_address is not None or response_customer_town is not None or response_customer_state is not None: + get_customer.customer_address = response_customer_address if response_customer_address is not None else get_customer.customer_address - get_customer.customer_address = response_customer_address - get_customer.customer_home_type = response_customer_home_type - get_customer.customer_phone_number = response_customer_phone_number - get_customer.customer_last_name = response_customer_last_name - get_customer.customer_first_name = response_customer_first_name - get_customer.customer_town = response_customer_town - get_customer.customer_state = response_customer_state - get_customer.customer_zip = response_customer_zip - get_customer.customer_email = response_customer_email - get_customer.customer_apt = response_customer_apt - get_customer.correct_address = cor_ad + state_code = response_customer_state if response_customer_state is not None else get_customer.customer_state + if state_code == 0: + the_state = 'MA' + elif state_code == 1: + the_state = 'RI' + elif state_code == 2: + the_state = 'NH' + else: + the_state = 'MA' + + town = response_customer_town if response_customer_town is not None else get_customer.customer_town + address = get_customer.customer_address + + geolocator = Nominatim(user_agent="auburnoil") + address_string = address + ' ' + town + ' ' + the_state + try: + location = geolocator.geocode(address_string, timeout=10) + get_customer.customer_latitude = location.latitude + get_customer.customer_longitude = location.longitude + get_customer.correct_address = True + except Exception: + get_customer.customer_latitude = None + get_customer.customer_longitude = None + get_customer.correct_address = False db.session.add(get_customer) @@ -407,6 +405,7 @@ def edit_customer(customer_id): def delete_customer(customer_id): """ """ + logger.info(f"DELETE /customer/delete/{customer_id} - Deleting customer") get_customer = (db.session .query(Customer_Customer) .filter(Customer_Customer.id == customer_id) @@ -436,6 +435,7 @@ def delete_customer(customer_id): def customer_count(): """ """ + logger.info("GET /customer/count - Getting customer count") get_customer = (db.session .query(Customer_Customer) .count()) @@ -451,6 +451,7 @@ def customer_count(): def customer_automatic_status(customer_id): """ """ + logger.info(f"GET /customer/automatic/status/{customer_id} - Checking auto delivery status") get_customer = (db.session .query(Customer_Customer) .filter(Customer_Customer.id == customer_id) @@ -475,7 +476,7 @@ def get_all_automatic_deliveries(): """ Get all automatic deliveries for the table. """ - + logger.info("GET /customer/automatic/deliveries - Fetching all auto deliveries") try: deliveries = Auto_Delivery.query.all() schema = Auto_Delivery_schema(many=True) @@ -491,6 +492,7 @@ def get_all_automatic_deliveries(): def customer_automatic_assignment(customer_id): """ """ + logger.info(f"GET /customer/automatic/assign/{customer_id} - Toggling auto delivery assignment") get_customer = (db.session .query(Customer_Customer) .filter(Customer_Customer.id == customer_id) @@ -578,6 +580,7 @@ def edit_customer_tank(customer_id): """ Safely edits or creates tank and description details for a customer. """ + logger.info(f"PUT /customer/edit/tank/{customer_id} - Editing customer tank info") get_customer = db.session.query(Customer_Customer).filter(Customer_Customer.id == customer_id).one_or_none() if not get_customer: return jsonify({"ok": False, "error": "Customer not found"}), 404 diff --git a/app/delivery/views.py b/app/delivery/views.py index 79a96a6..8509b0d 100755 --- a/app/delivery/views.py +++ b/app/delivery/views.py @@ -1,8 +1,12 @@ +import logging from flask import request, jsonify from flask_login import current_user from datetime import date, datetime, timedelta from app.delivery import delivery from app import db +from app.common.decorators import login_required as common_login_required + +logger = logging.getLogger(__name__) from sqlalchemy import or_ from app.classes.customer import (Customer_Customer) from app.classes.delivery import (Delivery_Delivery, @@ -20,6 +24,7 @@ from app.classes.auto import Tickets_Auto_Delivery, Tickets_Auto_Delivery_schema # This endpoint is fine, but I've added some comments for clarity. @delivery.route("/updatestatus", methods=["GET"]) +@common_login_required def move_deliveries(): """ Batch updates delivery statuses based on their expected delivery date relative to today. @@ -28,6 +33,7 @@ def move_deliveries(): - Future deliveries -> "Waiting" (0) - Past-due deliveries -> "Pending" (9) """ + logger.info("GET /delivery/updatestatus - Batch updating delivery statuses") counter = 0 today = date.today() tomorrow = today + timedelta(days=1) @@ -84,10 +90,12 @@ def move_deliveries(): @delivery.route("/", methods=["GET"]) +@common_login_required def get_a_delivery(delivery_id): """ Get a single delivery's details. """ + logger.info(f"GET /delivery/{delivery_id} - Fetching delivery") get_delivery = db.session.query(Delivery_Delivery).filter(Delivery_Delivery.id == delivery_id).first() if not get_delivery: return jsonify({"ok": False, "error": "Delivery not found"}), 404 @@ -101,8 +109,9 @@ def get_a_delivery(delivery_id): @delivery.route("/past1/", methods=["GET"]) +@common_login_required def get_customer_past_delivery1(customer_id): - + logger.info(f"GET /delivery/past1/{customer_id} - Fetching customer past deliveries (first 5)") get_customer_past_delivery = (db.session .query(Delivery_Delivery) .filter(Delivery_Delivery.customer_id == customer_id) @@ -114,8 +123,9 @@ def get_customer_past_delivery1(customer_id): @delivery.route("/past2/", methods=["GET"]) +@common_login_required def get_customer_past_delivery2(customer_id): - + logger.info(f"GET /delivery/past2/{customer_id} - Fetching customer past deliveries (next 5)") get_customer_past_delivery = (db.session .query(Delivery_Delivery) .filter(Delivery_Delivery.customer_id == customer_id) @@ -127,7 +137,9 @@ def get_customer_past_delivery2(customer_id): return jsonify(delivery_schema.dump(get_customer_past_delivery)) @delivery.route("/auto/", methods=["GET"]) +@common_login_required def get_customer_auto_delivery(customer_id): + logger.info(f"GET /delivery/auto/{customer_id} - Fetching customer auto deliveries") get_customer_past_delivery = (db.session .query(Tickets_Auto_Delivery) .filter(Tickets_Auto_Delivery.customer_id == customer_id) @@ -140,6 +152,7 @@ def get_customer_auto_delivery(customer_id): @delivery.route("/order/", methods=["GET"]) +@common_login_required def get_a_specific_delivery(delivery_id): """ Get a single delivery by its ID. @@ -155,6 +168,7 @@ def get_a_specific_delivery(delivery_id): @delivery.route("/cash//", methods=["PUT"]) +@common_login_required def update_a_delivery_payment(delivery_id, type_of_payment): """ This update a delivery for example if user updates to a fill @@ -175,6 +189,7 @@ def update_a_delivery_payment(delivery_id, type_of_payment): @delivery.route("/all/", methods=["GET"]) +@common_login_required def get_deliveries_all(page): """ This will get deliveries not done @@ -200,6 +215,7 @@ def get_deliveries_all(page): @delivery.route("/customer//", methods=["GET"]) +@common_login_required def get_deliveries_from_customer(customer_id, page): """ This will get deliveries not done @@ -225,6 +241,7 @@ def get_deliveries_from_customer(customer_id, page): @delivery.route("/all/order/", methods=["GET"]) +@common_login_required def get_deliveries_not_delivered(page): """ @@ -250,6 +267,7 @@ def get_deliveries_not_delivered(page): @delivery.route("/waiting/", methods=["GET"]) +@common_login_required def get_deliveries_waiting(page): """ This will get deliveries not done @@ -278,6 +296,7 @@ def get_deliveries_waiting(page): @delivery.route("/pending/", methods=["GET"]) +@common_login_required def get_deliveries_pending(page): """ """ @@ -299,6 +318,7 @@ def get_deliveries_pending(page): return jsonify(customer_schema.dump(deliveries)) @delivery.route("/outfordelivery/", methods=["GET"]) +@common_login_required def get_deliveries_outfordelivery(page): """ """ @@ -322,6 +342,7 @@ def get_deliveries_outfordelivery(page): @delivery.route("/tommorrow/", methods=["GET"]) +@common_login_required def get_deliveries_tommorrow(page): """ This will get deliveries not done @@ -348,6 +369,7 @@ def get_deliveries_tommorrow(page): @delivery.route("/finalized/", methods=["GET"]) +@common_login_required def get_deliveries_finalized(page): """ This will get deliveries not done @@ -372,6 +394,7 @@ def get_deliveries_finalized(page): @delivery.route("/cancelled/", methods=["GET"]) +@common_login_required def get_deliveries_cancelled(page): """ This will get deliveries not done @@ -396,6 +419,7 @@ def get_deliveries_cancelled(page): @delivery.route("/partialdelivery/", methods=["GET"]) +@common_login_required def get_deliveries_partial(page): """ This will get deliveries not done @@ -420,6 +444,7 @@ def get_deliveries_partial(page): @delivery.route("/issue/", methods=["GET"]) +@common_login_required def get_deliveries_issue(page): """ This will get deliveries not done @@ -444,6 +469,7 @@ def get_deliveries_issue(page): @delivery.route("/time/today", methods=["GET"]) +@common_login_required def get_deliveries_today(): """ This will get today's deliveries @@ -460,10 +486,12 @@ def get_deliveries_today(): @delivery.route("/edit/", methods=["POST"]) +@common_login_required def edit_a_delivery(delivery_id): """ This will edit a delivery using a delivery id. """ + logger.info(f"POST /delivery/edit/{delivery_id} - Editing delivery") data = request.json get_delivery = db.session.query(Delivery_Delivery).filter(Delivery_Delivery.id == delivery_id).first() @@ -561,10 +589,12 @@ def edit_a_delivery(delivery_id): @delivery.route("/create/", methods=["POST"]) +@common_login_required def create_a_delivery(user_id): """ This will create a delivery using a customer id """ + logger.info(f"POST /delivery/create/{user_id} - Creating delivery for customer") get_customer = db.session\ .query(Customer_Customer)\ .filter(Customer_Customer.id == user_id)\ @@ -600,7 +630,7 @@ def create_a_delivery(user_id): promo_id = request.json["promo_id"] else: promo_id = None - except: + except (KeyError, TypeError): promo_id = None if promo_id is not None: get_promo_data = (db.session @@ -651,7 +681,7 @@ def create_a_delivery(user_id): card_payment_id = request.json["credit_card_id"] else: card_payment_id = None - except: + except (KeyError, TypeError): card_payment_id = None if card_payment_id is not None: @@ -777,10 +807,12 @@ def create_a_delivery(user_id): @delivery.route("/cancel/", methods=["POST"]) +@common_login_required def cancel_a_delivery(delivery_id): """ This will cancel a delivery """ + logger.info(f"POST /delivery/cancel/{delivery_id} - Cancelling delivery") get_delivery = db.session\ .query(Delivery_Delivery)\ .filter(Delivery_Delivery.id == delivery_id)\ @@ -794,10 +826,12 @@ def cancel_a_delivery(delivery_id): @delivery.route("/delivered/", methods=["POST"]) +@common_login_required def mark_as_delivered(delivery_id): """ This will mark the delivery as delivered """ + logger.info(f"POST /delivery/delivered/{delivery_id} - Marking delivery as delivered") # how many gallons delivered gallons_put_into_tank = request.json["gallons_put_into_tank"] # was the tank full or not @@ -819,6 +853,7 @@ def mark_as_delivered(delivery_id): @delivery.route("/partial/", methods=["POST"]) +@common_login_required def partial_delivery(delivery_id): """ This will mark the delivery as delivered @@ -842,6 +877,7 @@ def partial_delivery(delivery_id): @delivery.route("/note/technician/", methods=["PUT"]) +@common_login_required def delivery_note_driver(delivery_id): """ @@ -876,10 +912,12 @@ def delivery_note_driver(delivery_id): @delivery.route("/delete/", methods=["DELETE"]) +@common_login_required def delete_call(delivery_id): """ delete a delivery call """ + logger.info(f"DELETE /delivery/delete/{delivery_id} - Deleting delivery") get_call_to_delete = (db.session .query(Delivery_Delivery) .filter(Delivery_Delivery.id == delivery_id) @@ -893,6 +931,7 @@ def delete_call(delivery_id): @delivery.route("/total/", methods=["GET"]) +@common_login_required def calculate_total(delivery_id): """ This will get deliveries not done diff --git a/app/delivery_data/views.py b/app/delivery_data/views.py index 938d9d6..fd472b9 100755 --- a/app/delivery_data/views.py +++ b/app/delivery_data/views.py @@ -1,3 +1,4 @@ +import logging from flask import request, jsonify from datetime import datetime from decimal import Decimal @@ -11,6 +12,8 @@ from app.classes.stats_employee import Stats_Employee_Oil from app.classes.auto import Auto_Delivery from app.classes.stats_customer import Stats_Customer +logger = logging.getLogger(__name__) + @delivery_data.route("/finalize/", methods=["PUT"]) def office_finalize_delivery(delivery_id): """ @@ -20,6 +23,7 @@ def office_finalize_delivery(delivery_id): """ Finalizes a delivery from office """ + logger.info(f"PUT /deliverydata/finalize/{delivery_id} - Finalizing delivery from office") now = datetime.utcnow() get_delivery = db.session \ .query(Delivery_Delivery) \ diff --git a/app/delivery_status/views.py b/app/delivery_status/views.py index 4e669d2..ccafcb0 100755 --- a/app/delivery_status/views.py +++ b/app/delivery_status/views.py @@ -1,3 +1,4 @@ +import logging from flask import jsonify from datetime import date, timedelta from app.delivery_status import deliverystatus @@ -13,6 +14,8 @@ from app.classes.transactions import Transaction from datetime import date, timedelta, datetime from zoneinfo import ZoneInfo +logger = logging.getLogger(__name__) + # --- NEW EFFICIENT ENDPOINT --- @deliverystatus.route("/stats/sidebar-counts", methods=["GET"]) @@ -21,6 +24,7 @@ def get_sidebar_counts(): Efficiently gets all counts needed for the navigation sidebar in a single request. This combines logic from all the individual /count/* endpoints. """ + logger.info("GET /deliverystatus/stats/sidebar-counts - Fetching sidebar counts") try: eastern = ZoneInfo("America/New_York") now_local = datetime.now(eastern).replace(tzinfo=None) # naive local time @@ -73,6 +77,7 @@ def get_tomorrow_totals(): """ Get total gallons by town for tomorrow's deliveries, including grand total. """ + logger.info("GET /deliverystatus/tomorrow-totals - Fetching tomorrow delivery totals") try: deliveries = db.session.query( Delivery_Delivery.customer_town, @@ -101,6 +106,7 @@ def get_today_totals(): """ Get total gallons by town for today's deliveries, including grand total. """ + logger.info("GET /deliverystatus/today-totals - Fetching today delivery totals") try: deliveries = db.session.query( Delivery_Delivery.customer_town, @@ -129,6 +135,7 @@ def get_waiting_totals(): """ Get total gallons by town for waiting deliveries, including grand total. """ + logger.info("GET /deliverystatus/waiting-totals - Fetching waiting delivery totals") try: deliveries = db.session.query( Delivery_Delivery.customer_town, diff --git a/app/employees/views.py b/app/employees/views.py index 113c30f..65ca66b 100755 --- a/app/employees/views.py +++ b/app/employees/views.py @@ -1,3 +1,4 @@ +import logging from flask import request, jsonify from sqlalchemy import or_ @@ -9,9 +10,12 @@ from app.classes.employee import Employee_Employee, Employee_Employee_schema from app.classes.auth import Auth_User from app.classes.stats_employee import Stats_Employee_Oil, Stats_Employee_Office +logger = logging.getLogger(__name__) + @employees.route("/", methods=["GET"]) @login_required def get_specific_employee(userid): + logger.info(f"GET /employees/{userid} - Fetching employee by user ID") employee = db.session \ .query(Employee_Employee) \ .filter(Employee_Employee.user_id == userid) \ @@ -31,6 +35,7 @@ def get_specific_employee(userid): @employees.route("/byid/", methods=["GET"]) @login_required def get_employee_by_id(employee_id): + logger.info(f"GET /employees/byid/{employee_id} - Fetching employee by ID") employee = db.session \ .query(Employee_Employee) \ .filter(Employee_Employee.id == employee_id) \ @@ -42,6 +47,7 @@ def get_employee_by_id(employee_id): @employees.route("/userid/", methods=["GET"]) @login_required def get_specific_employee_user_id(userid): + logger.info(f"GET /employees/userid/{userid} - Fetching employee by user ID") employee = db.session \ .query(Employee_Employee) \ .filter(Employee_Employee.user_id == userid) \ @@ -56,6 +62,7 @@ def all_employees_paginated(page): """ pagination all employees """ + logger.info(f"GET /employees/all/{page} - Fetching employees page {page}") per_page_amount = 50 if page is None: offset_limit = 0 @@ -75,6 +82,7 @@ def all_employees_paginated(page): @employees.route("/all", methods=["GET"]) @login_required def all_employees(): + logger.info("GET /employees/all - Fetching all employees") employee_list = db.session \ .query(Employee_Employee) \ .all() @@ -85,6 +93,7 @@ def all_employees(): @employees.route("/drivers", methods=["GET"]) @login_required def all_employees_drivers(): + logger.info("GET /employees/drivers - Fetching all drivers") employee_list = db.session \ .query(Employee_Employee) \ .filter(or_(Employee_Employee.employee_type == 4, @@ -98,6 +107,7 @@ def all_employees_drivers(): @employees.route("/techs", methods=["GET"]) @login_required def all_employees_techs(): + logger.info("GET /employees/techs - Fetching all technicians") employee_list = db.session \ .query(Employee_Employee) \ .filter(or_(Employee_Employee.employee_type == 0, @@ -116,6 +126,7 @@ def employee_create(): """ This will create an employee """ + logger.info("POST /employees/create - Creating new employee") e_last_name = request.json["employee_last_name"] e_first_name = request.json["employee_first_name"] e_town = request.json["employee_town"] @@ -180,6 +191,7 @@ def employee_edit(employee_id): """ This will update an employee """ + logger.info(f"POST /employees/edit/{employee_id} - Editing employee") e_last_name = request.json["employee_last_name"] e_first_name = request.json["employee_first_name"] e_town = request.json["employee_town"] diff --git a/app/info/views.py b/app/info/views.py index 37bfbcd..41846ea 100755 --- a/app/info/views.py +++ b/app/info/views.py @@ -1,3 +1,4 @@ +import logging from flask import jsonify from decimal import Decimal from app.info import info @@ -6,10 +7,15 @@ from app.classes.pricing import Pricing_Oil_Oil, Pricing_Oil_Oil_schema from app.classes.admin import Admin_Company from app.classes.delivery import Delivery_Delivery from app.classes.service import Service_Service +from flask_login import login_required + +logger = logging.getLogger(__name__) @info.route("/price/oil/tiers", methods=["GET"]) +@login_required def get_pricing_tiers(): + logger.info("GET /info/price/oil/tiers - Fetching oil pricing tiers") get_price_query = (db.session .query(Pricing_Oil_Oil) .order_by(Pricing_Oil_Oil.date.desc()) @@ -34,7 +40,9 @@ def get_pricing_tiers(): return jsonify(pricing_totals) @info.route("/price/oil", methods=["GET"]) +@login_required def get_oil_price_today(): + logger.info("GET /info/price/oil - Fetching current oil prices") get_price_query = (db.session .query(Pricing_Oil_Oil) .order_by(Pricing_Oil_Oil.date.desc()) @@ -50,7 +58,9 @@ def get_oil_price_today(): @info.route("/price/oil/table", methods=["GET"]) +@login_required def get_pricing(): + logger.info("GET /info/price/oil/table - Fetching oil pricing table") get_price_query = (db.session .query(Pricing_Oil_Oil) .order_by(Pricing_Oil_Oil.date.desc()) @@ -63,7 +73,9 @@ def get_pricing(): @info.route("/company", methods=["GET"]) +@login_required def get_company(): + logger.info("GET /info/company - Fetching company information") get_data_company = (db.session .query(Admin_Company) .first()) diff --git a/app/main/views.py b/app/main/views.py index b2d2291..e44d13d 100755 --- a/app/main/views.py +++ b/app/main/views.py @@ -1,14 +1,19 @@ +import logging from flask import jsonify, Response, url_for from app import app +logger = logging.getLogger(__name__) + @app.route("/favicon.ico") def favicon(): + logger.info("GET /favicon.ico - Serving favicon") return url_for('static', filename='data:,') @app.route('/robots.txt') @app.route('/sitemap.xml') def static_from_root(): + logger.info("GET /robots.txt or /sitemap.xml - Serving robots/sitemap") def disallow(string): return 'Disallow: {0}'.format(string) return Response("User-agent: *\n{0}\n".format("\n".join([ disallow('/bin/*'), @@ -19,5 +24,5 @@ def static_from_root(): @app.route('/index', methods=['GET']) @app.route('/', methods=['GET']) def index(): + logger.info("GET / or /index - API health check") return jsonify({"success": "Api is online"}), 200 - diff --git a/app/money/views.py b/app/money/views.py index 3991ce7..2b8f929 100644 --- a/app/money/views.py +++ b/app/money/views.py @@ -1,3 +1,4 @@ +import logging from flask import jsonify from app.money import money from app import db @@ -6,6 +7,8 @@ from datetime import date from app.classes.money import Money_delivery, Money_delivery_schema from app.classes.delivery import Delivery_Delivery, Delivery_Delivery_schema +logger = logging.getLogger(__name__) + def get_monday_date(date_object): """Gets the date of the Monday for the given date.""" @@ -25,6 +28,7 @@ def get_monday_date(date_object): @money.route("/profit/week", methods=["GET"]) def total_profit_week(): + logger.info("GET /money/profit/week - Calculating weekly profit") # Get today's date total_profit = 0 total_deliveries = 0 @@ -52,6 +56,7 @@ def total_profit_week(): @money.route("/profit/year", methods=["GET"]) def total_profit_year(): + logger.info("GET /money/profit/year - Calculating yearly profit") # Get today's date total_profit = 0 @@ -74,10 +79,11 @@ def total_profit_year(): def get_money_delivery(delivery_id): """ """ + logger.info(f"GET /money/{delivery_id} - Fetching delivery profit data") profit = (db.session .query(Money_delivery) .filter(Money_delivery.delivery_id == delivery_id) .first()) money_schema = Money_delivery_schema(many=False) - return jsonify(money_schema.dump(profit)) \ No newline at end of file + return jsonify(money_schema.dump(profit)) diff --git a/app/payment/views.py b/app/payment/views.py index be59b4f..756d214 100755 --- a/app/payment/views.py +++ b/app/payment/views.py @@ -1,3 +1,4 @@ +import logging from flask import jsonify, request from app.payment import payment from app import db @@ -6,6 +7,9 @@ from app.classes.cards import Card_Card, Card_Card_schema from app.classes.transactions import Transaction from app.classes.delivery import Delivery_Delivery from app.classes.service import Service_Service, Service_Service_schema +from flask_login import login_required + +logger = logging.getLogger(__name__) @@ -52,10 +56,12 @@ def set_card_main(user_id, card_id): @payment.route("/cards/", methods=["GET"]) +@login_required def get_user_cards(user_id): """ gets all cards of a user """ + logger.info(f"GET /payment/cards/{user_id} - Fetching user cards") get_u_cards = (db.session .query(Card_Card) .filter(Card_Card.user_id == user_id) @@ -66,11 +72,12 @@ def get_user_cards(user_id): @payment.route("/cards/onfile/", methods=["GET"]) +@login_required def get_user_cards_count(user_id): """ gets all cards of a user """ - + logger.info(f"GET /payment/cards/onfile/{user_id} - Getting card count") get_u_cards = (db.session .query(Card_Card) .filter(Card_Card.user_id == user_id) @@ -83,6 +90,7 @@ def get_user_cards_count(user_id): @payment.route("/card/", methods=["GET"]) +@login_required def get_user_specific_card(card_id): """ gets a specific card of a user @@ -99,6 +107,7 @@ def get_user_specific_card(card_id): @payment.route("/card/main//", methods=["PUT"]) +@login_required def set_main_card(user_id, card_id): """ updates a card of a user @@ -130,11 +139,12 @@ def set_main_card(user_id, card_id): @payment.route("/card/remove/", methods=["DELETE"]) +@login_required def remove_user_card(card_id): """ - removes a card + removes a card """ - + logger.info(f"DELETE /payment/card/remove/{card_id} - Removing card") get_card = (db.session .query(Card_Card) .filter(Card_Card.id == card_id) @@ -151,6 +161,7 @@ def remove_user_card(card_id): # ... (your existing imports: jsonify, request, db, Customer_Customer, Card_Card) ... @payment.route("/card/create/", methods=["POST"]) +@login_required def create_user_card(user_id): """ Adds a card for a user to the local database. This is its only job. @@ -196,17 +207,18 @@ def create_user_card(user_id): set_card_main(user_id=get_customer.id, card_id=create_new_card.id) db.session.commit() - print(f"SUCCESS: Card saved locally for user {user_id} with new ID {create_new_card.id}") + logger.info(f"Card saved locally for user {user_id} with ID {create_new_card.id}") except Exception as e: db.session.rollback() - print(f"DATABASE ERROR: Could not save card for user {user_id}. Error: {e}") + logger.error(f"Database error saving card for user {user_id}: {e}") return jsonify({"ok": False, "error": "Failed to save card information."}), 500 # Return a success response with the card_id return jsonify({"ok": True, "card_id": create_new_card.id}), 200 @payment.route("/card/update_payment_profile/", methods=["PUT"]) +@login_required def update_card_payment_profile(card_id): """ Updates the auth_net_payment_profile_id for a card @@ -230,6 +242,7 @@ def update_card_payment_profile(card_id): @payment.route("/card/edit/", methods=["PUT"]) +@login_required def update_user_card(card_id): """ edits a card @@ -277,6 +290,7 @@ def update_user_card(card_id): @payment.route("/transactions/authorize/", methods=["GET"]) +@login_required def get_authorize_transactions(page): """ Gets transactions with transaction_type = 0 (charge), for the authorize page @@ -320,6 +334,7 @@ def get_authorize_transactions(page): @payment.route("/authorize/cleanup/", methods=["POST"]) +@login_required def cleanup_authorize_profile(customer_id): """ Clean up Authorize.Net profile data in local database when API check fails. @@ -349,6 +364,7 @@ def cleanup_authorize_profile(customer_id): @payment.route("/authorize/", methods=["PUT"]) +@login_required def update_delivery_payment_authorize(delivery_id): """ Update a delivery's payment_type to 11 (CC - Authorize API) after successful preauthorization @@ -370,6 +386,7 @@ def update_delivery_payment_authorize(delivery_id): @payment.route("/transaction/delivery/", methods=["GET"]) +@login_required def get_transaction_by_delivery(delivery_id): """ Get a single transaction by delivery_id for Authorize.net payments @@ -403,6 +420,7 @@ def get_transaction_by_delivery(delivery_id): @payment.route("/transactions/customer//", methods=["GET"]) +@login_required def get_customer_transactions(customer_id, page): """ Gets transactions for a specific customer @@ -445,6 +463,7 @@ def get_customer_transactions(customer_id, page): @payment.route("/service/payment//", methods=["PUT"]) +@login_required def process_service_payment_tiger(service_id, payment_type): service = db.session.query(Service_Service).filter(Service_Service.id == service_id).first() if not service: @@ -464,6 +483,7 @@ def process_service_payment_tiger(service_id, payment_type): @payment.route("/authorize/service/", methods=["PUT"]) +@login_required def update_service_payment_authorize(service_id): service = db.session.query(Service_Service).filter(Service_Service.id == service_id).first() if not service: @@ -487,6 +507,7 @@ def update_service_payment_authorize(service_id): @payment.route("/capture/service/", methods=["PUT"]) +@login_required def update_service_payment_capture(service_id): service = db.session.query(Service_Service).filter(Service_Service.id == service_id).first() if not service: @@ -511,6 +532,7 @@ def update_service_payment_capture(service_id): @payment.route("/transactions/service/", methods=["GET"]) +@login_required def get_service_transactions(service_id): """ Gets all transactions for a specific service ID @@ -544,5 +566,5 @@ def get_service_transactions(service_id): return jsonify(transactions_data), 200 except Exception as e: - print(f"Error fetching transactions for service {service_id}: {str(e)}") + logger.error(f"Error fetching transactions for service {service_id}: {e}") return jsonify({"ok": False, "error": str(e)}), 500 diff --git a/app/promo/views.py b/app/promo/views.py index 6a3f23b..9fd7b7f 100644 --- a/app/promo/views.py +++ b/app/promo/views.py @@ -1,3 +1,4 @@ +import logging from flask import request, jsonify import decimal from datetime import datetime @@ -11,6 +12,8 @@ from app.classes.delivery import (Delivery_Delivery, Delivery_Notes_Driver, ) +logger = logging.getLogger(__name__) + def convert_to_decimal(text): try: number = float(text) @@ -23,9 +26,10 @@ def convert_to_decimal(text): def get_promo(promo_id): """ """ - get_promo_data = (db.session - .query(Promo_Promo) - .filter(Promo_Promo.id == promo_id) + logger.info(f"GET /promo/{promo_id} - Fetching promo") + get_promo_data = (db.session + .query(Promo_Promo) + .filter(Promo_Promo.id == promo_id) .first()) query_schema = Promo_Promo_schema(many=False) return jsonify(query_schema.dump(get_promo_data)) @@ -35,6 +39,7 @@ def get_promo(promo_id): def get_promo_price(delivery_id): """ """ + logger.info(f"GET /promo/promoprice/{delivery_id} - Calculating promo price") get_delivery = (db.session .query(Delivery_Delivery) .filter(Delivery_Delivery.id == delivery_id) @@ -53,8 +58,9 @@ def get_promo_price(delivery_id): def get_all_promo(): """ """ - get_promo_data = (db.session - .query(Promo_Promo) + logger.info("GET /promo/all - Fetching all promos") + get_promo_data = (db.session + .query(Promo_Promo) .all()) query_schema = Promo_Promo_schema(many=True) return jsonify(query_schema.dump(get_promo_data)) @@ -64,11 +70,12 @@ def get_all_promo(): def delete_a_promo(promo_id): """ """ - get_promo_data = (db.session - .query(Promo_Promo) - .filter(Promo_Promo.id == promo_id) + logger.info(f"DELETE /promo/delete/{promo_id} - Deleting promo") + get_promo_data = (db.session + .query(Promo_Promo) + .filter(Promo_Promo.id == promo_id) .first()) - + db.session.delete(get_promo_data) db.session.commit() @@ -81,6 +88,7 @@ def delete_a_promo(promo_id): def create_promo(): """ """ + logger.info("POST /promo/create - Creating new promo") date_created = datetime.utcnow() name_of_promotion = request.json["name_of_promotion"] money_off_delivery = request.json["money_off_delivery"] @@ -94,13 +102,13 @@ def create_promo(): name_of_promotion = name_of_promotion, money_off_delivery = amount_off, description = description, - date_created = date_created, + date_created = date_created, text_on_ticket=text_on_ticket ) db.session.add(new_promo) db.session.commit() - + return jsonify({ "ok": True, 'promo_id':new_promo.id, @@ -112,9 +120,10 @@ def edit_promo(promo_id): """ """ - get_promo_data = (db.session - .query(Promo_Promo) - .filter(Promo_Promo.id == promo_id) + logger.info(f"PUT /promo/edit/{promo_id} - Editing promo") + get_promo_data = (db.session + .query(Promo_Promo) + .filter(Promo_Promo.id == promo_id) .first()) text_on_ticket = request.json["text_on_ticket"] name_of_promotion = request.json["name_of_promotion"] @@ -144,13 +153,14 @@ def turn_on_promo(promo_id): """ """ - get_promo_data = (db.session - .query(Promo_Promo) - .filter(Promo_Promo.id == promo_id) + logger.info(f"PATCH /promo/on/{promo_id} - Activating promo") + get_promo_data = (db.session + .query(Promo_Promo) + .filter(Promo_Promo.id == promo_id) .first()) get_promo_data.active = True - + db.session.add(get_promo_data) db.session.commit() @@ -165,13 +175,14 @@ def turn_off_promo(promo_id): """ """ - get_promo_data = (db.session - .query(Promo_Promo) - .filter(Promo_Promo.id == promo_id) + logger.info(f"PATCH /promo/off/{promo_id} - Deactivating promo") + get_promo_data = (db.session + .query(Promo_Promo) + .filter(Promo_Promo.id == promo_id) .first()) get_promo_data.active = False - + db.session.add(get_promo_data) db.session.commit() @@ -179,4 +190,4 @@ def turn_off_promo(promo_id): return jsonify({ "ok": True, 'promo_id':get_promo_data.id, - }), 200 \ No newline at end of file + }), 200 diff --git a/app/query/views.py b/app/query/views.py index 81a6e6b..5604595 100755 --- a/app/query/views.py +++ b/app/query/views.py @@ -1,34 +1,42 @@ +import logging from flask import jsonify from app.query import query from app import db from app.classes.admin import Admin_Company from app.classes.query import (Query_StateList, - Query_DeliveryStatusList, - Query_DeliveryStatusList_Schema, - Query_StateList_Schema, - Query_CustomerTypeList, + Query_DeliveryStatusList, + Query_DeliveryStatusList_Schema, + Query_StateList_Schema, + Query_CustomerTypeList, Query_CustomerTypeList_Schema, - Query_EmployeeTypeList, + Query_EmployeeTypeList, Query_EmployeeTypeList_Schema) +from flask_login import login_required + +logger = logging.getLogger(__name__) @query.route("/company/", methods=["GET"]) +@login_required def get_company(company_id): """ This will get the company from env variable """ + logger.info(f"GET /query/company/{company_id} - Fetching company data") - query_data = (db.session - .query(Admin_Company) - .filter(Admin_Company.id == company_id) + query_data = (db.session + .query(Admin_Company) + .filter(Admin_Company.id == company_id) .first()) delivery_schema = Query_DeliveryStatusList_Schema(many=False) return jsonify(delivery_schema.dump(query_data)) @query.route("/states", methods=["GET"]) +@login_required def get_state_list(): """ This will get states """ + logger.info("GET /query/states - Fetching state list") query_data = db.session \ .query(Query_StateList) \ @@ -39,10 +47,12 @@ def get_state_list(): @query.route("/customertype", methods=["GET"]) +@login_required def get_customer_type_list(): """ This will get types of customers """ + logger.info("GET /query/customertype - Fetching customer type list") query_data = db.session \ .query(Query_CustomerTypeList) \ @@ -54,10 +64,12 @@ def get_customer_type_list(): @query.route("/employeetype", methods=["GET"]) +@login_required def get_employee_type_list(): """ This will get types of service """ + logger.info("GET /query/employeetype - Fetching employee type list") query_data = db.session \ .query(Query_EmployeeTypeList) \ @@ -67,16 +79,15 @@ def get_employee_type_list(): @query.route("/deliverystatus", methods=["GET"]) +@login_required def get_delivery_status_list(): """ This will get delivery status """ + logger.info("GET /query/deliverystatus - Fetching delivery status list") query_data = db.session \ .query(Query_DeliveryStatusList) \ .all() delivery_schema = Query_DeliveryStatusList_Schema(many=True) return jsonify(delivery_schema.dump(query_data)) - - - diff --git a/app/reports/views.py b/app/reports/views.py index fa725de..889b0ca 100755 --- a/app/reports/views.py +++ b/app/reports/views.py @@ -1,3 +1,4 @@ +import logging from flask import jsonify from sqlalchemy.sql import func from app.reports import reports @@ -7,14 +8,17 @@ from app.classes.customer import Customer_Customer from app.classes.delivery import Delivery_Delivery +logger = logging.getLogger(__name__) + @reports.route("/oil/total", methods=["GET"]) def oil_total_gallons(): + logger.info("GET /reports/oil/total - Calculating total oil delivered") total_oil = db.session\ .query(func.sum(Delivery_Delivery.gallons_delivered))\ .group_by(Delivery_Delivery.id)\ .all() - + return jsonify({"ok": True, "oil": total_oil }), 200 @reports.route("/customers/list", methods=["GET"]) @@ -24,6 +28,7 @@ def customer_list(): Returns account number, first name, last name, address, town, and phone number. Ordered by last name from A to Z. """ + logger.info("GET /reports/customers/list - Fetching customer list for reports") customers = db.session.query(Customer_Customer).order_by(Customer_Customer.customer_last_name.asc()).all() customer_data = [ diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..9b401ab --- /dev/null +++ b/app/schemas/__init__.py @@ -0,0 +1,4 @@ +# Validation schemas for API requests +from .customer import CreateCustomerSchema, UpdateCustomerSchema, UpdateDescriptionSchema +from .auth import LoginSchema, RegisterSchema, ChangePasswordSchema +from .utils import validate_request, validate_json diff --git a/app/schemas/auth.py b/app/schemas/auth.py new file mode 100644 index 0000000..a08401a --- /dev/null +++ b/app/schemas/auth.py @@ -0,0 +1,56 @@ +from marshmallow import Schema, fields, validate, EXCLUDE + + +class LoginSchema(Schema): + """Validation schema for user login""" + class Meta: + unknown = EXCLUDE + + username = fields.Str( + required=True, + validate=validate.Length(min=3, max=100), + error_messages={"required": "Username is required"} + ) + password = fields.Str( + required=True, + validate=validate.Length(min=6, max=200), + error_messages={"required": "Password is required"} + ) + + +class RegisterSchema(Schema): + """Validation schema for user registration""" + class Meta: + unknown = EXCLUDE + + username = fields.Str( + required=True, + validate=validate.Length(min=3, max=100), + error_messages={"required": "Username is required"} + ) + password = fields.Str( + required=True, + validate=validate.Length(min=6, max=200), + error_messages={"required": "Password is required"} + ) + email = fields.Email( + required=True, + error_messages={"required": "Email is required"} + ) + + +class ChangePasswordSchema(Schema): + """Validation schema for password change""" + class Meta: + unknown = EXCLUDE + + new_password = fields.Str( + required=True, + validate=validate.Length(min=6, max=200), + error_messages={"required": "New password is required"} + ) + password_confirm = fields.Str( + required=True, + validate=validate.Length(min=6, max=200), + error_messages={"required": "Password confirmation is required"} + ) diff --git a/app/schemas/customer.py b/app/schemas/customer.py new file mode 100644 index 0000000..ce9665e --- /dev/null +++ b/app/schemas/customer.py @@ -0,0 +1,126 @@ +from marshmallow import Schema, fields, validate, EXCLUDE + + +class CreateCustomerSchema(Schema): + """Validation schema for creating a new customer""" + class Meta: + unknown = EXCLUDE + + customer_last_name = fields.Str( + required=True, + validate=validate.Length(min=1, max=250), + error_messages={"required": "Last name is required"} + ) + customer_first_name = fields.Str( + required=True, + validate=validate.Length(min=1, max=250), + error_messages={"required": "First name is required"} + ) + customer_town = fields.Str( + required=True, + validate=validate.Length(min=1, max=140), + error_messages={"required": "Town is required"} + ) + customer_state = fields.Int( + required=True, + validate=validate.Range(min=0, max=50), + error_messages={"required": "State is required"} + ) + customer_zip = fields.Str( + required=True, + validate=validate.Length(min=5, max=10), + error_messages={"required": "Zip code is required"} + ) + customer_email = fields.Email( + allow_none=True, + load_default=None + ) + customer_home_type = fields.Int( + required=True, + validate=validate.Range(min=0, max=10), + error_messages={"required": "Home type is required"} + ) + customer_phone_number = fields.Str( + allow_none=True, + validate=validate.Length(max=25), + load_default=None + ) + customer_address = fields.Str( + required=True, + validate=validate.Length(min=1, max=1000), + error_messages={"required": "Address is required"} + ) + customer_apt = fields.Str( + allow_none=True, + validate=validate.Length(max=140), + load_default=None + ) + customer_description = fields.Str( + allow_none=True, + validate=validate.Length(max=2000), + load_default=None + ) + + +class UpdateCustomerSchema(Schema): + """Validation schema for updating an existing customer""" + class Meta: + unknown = EXCLUDE + + customer_last_name = fields.Str( + validate=validate.Length(min=1, max=250) + ) + customer_first_name = fields.Str( + validate=validate.Length(min=1, max=250) + ) + customer_town = fields.Str( + validate=validate.Length(min=1, max=140) + ) + customer_state = fields.Int( + validate=validate.Range(min=0, max=50) + ) + customer_zip = fields.Str( + validate=validate.Length(min=5, max=10) + ) + customer_email = fields.Email( + allow_none=True + ) + customer_home_type = fields.Int( + validate=validate.Range(min=0, max=10) + ) + customer_phone_number = fields.Str( + allow_none=True, + validate=validate.Length(max=25) + ) + customer_address = fields.Str( + validate=validate.Length(min=1, max=1000) + ) + customer_apt = fields.Str( + allow_none=True, + validate=validate.Length(max=140) + ) + customer_automatic = fields.Int( + validate=validate.Range(min=0, max=10) + ) + customer_description = fields.Str( + allow_none=True, + validate=validate.Length(max=2000) + ) + customer_fill_location = fields.Int( + allow_none=True, + validate=validate.Range(min=0, max=10) + ) + + +class UpdateDescriptionSchema(Schema): + """Validation schema for updating customer description""" + class Meta: + unknown = EXCLUDE + + description = fields.Str( + allow_none=True, + validate=validate.Length(max=2000) + ) + fill_location = fields.Int( + allow_none=True + ) diff --git a/app/schemas/utils.py b/app/schemas/utils.py new file mode 100644 index 0000000..a793fda --- /dev/null +++ b/app/schemas/utils.py @@ -0,0 +1,56 @@ +from flask import jsonify, request +from functools import wraps +from marshmallow import ValidationError + + +def validate_request(schema_class): + """ + Decorator to validate incoming JSON request data against a marshmallow schema. + + Usage: + @customer.route("/create", methods=["POST"]) + @validate_request(CreateCustomerSchema) + def create_customer(): + data = request.validated_data # Access validated data + ... + """ + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + # Check if request has JSON data + if not request.is_json: + return jsonify({"error": "Request must be JSON"}), 400 + + json_data = request.get_json() + if json_data is None: + return jsonify({"error": "Invalid JSON data"}), 400 + + # Validate the data + schema = schema_class() + try: + validated_data = schema.load(json_data) + # Attach validated data to request object for easy access + request.validated_data = validated_data + except ValidationError as err: + return jsonify({"error": "Validation failed", "details": err.messages}), 400 + + return f(*args, **kwargs) + return decorated_function + return decorator + + +def validate_json(schema_class, data): + """ + Validate data against a schema and return (validated_data, errors). + + Usage: + data, errors = validate_json(CreateCustomerSchema, request.get_json()) + if errors: + return jsonify({"error": "Validation failed", "details": errors}), 400 + """ + schema = schema_class() + try: + validated_data = schema.load(data or {}) + return validated_data, None + except ValidationError as err: + return None, err.messages diff --git a/app/search/views.py b/app/search/views.py index ed2cb94..7210a04 100755 --- a/app/search/views.py +++ b/app/search/views.py @@ -1,3 +1,4 @@ +import logging from flask import request, jsonify from app.search import search @@ -5,16 +6,27 @@ from app import db from sqlalchemy import or_ from app.classes.customer import Customer_Customer, Customer_Customer_schema from app.classes.delivery import Delivery_Delivery, Delivery_Delivery_schema +from flask_login import login_required + +logger = logging.getLogger(__name__) + + +def escape_like(value: str) -> str: + """Escape special LIKE characters to prevent LIKE injection.""" + if value is None: + return "" + return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") @search.route("/customer", methods=["GET"]) +@login_required def search_customers(): """ """ - keyword = request.args.get('q') - search = "%{}%".format(keyword) + logger.info(f"GET /search/customer - Searching customers with keyword: {keyword}") + search = "%{}%".format(escape_like(keyword)) search_type = (search[1]) search = search.replace("!", "") search = search.replace("#", "") @@ -66,12 +78,14 @@ def search_customers(): @search.route("/delivery", methods=["GET"]) +@login_required def search_delivery(): """ pagination all customers """ keyword = request.args.get('q') - search = "%{}%".format(keyword) + logger.info(f"GET /search/delivery - Searching deliveries with keyword: {keyword}") + search = "%{}%".format(escape_like(keyword)) search_type = (search[1]) delivery_ticket = (db.session diff --git a/app/service/views.py b/app/service/views.py index 70f929f..f644b67 100644 --- a/app/service/views.py +++ b/app/service/views.py @@ -1,3 +1,4 @@ +import logging from flask import request, jsonify from app.service import service from app import db @@ -8,9 +9,14 @@ from app.classes.service import (Service_Service, Service_Plans, Service_Plans_schema ) from app.classes.auto import Auto_Delivery +from flask_login import login_required + +logger = logging.getLogger(__name__) @service.route("/all", methods=["GET"]) +@login_required def get_all_service_calls(): + logger.info("GET /service/all - Fetching all service calls for calendar") try: all_services = Service_Service.query.all() color_map = { @@ -49,18 +55,19 @@ def get_all_service_calls(): return jsonify(calendar_events), 200 except Exception as e: - # Add error logging to see what's happening - print(f"An error occurred in /service/all: {e}") + logger.error(f"Error in /service/all: {e}") return jsonify(error=str(e)), 500 # --- THIS IS THE FIX --- # The logic from /all has been copied here to ensure a consistent data structure. @service.route("/upcoming", methods=["GET"]) +@login_required def get_upcoming_service_calls(): """ Fetches a list of all future service calls from today onwards. """ + logger.info("GET /service/upcoming - Fetching upcoming service calls") now = datetime.now() upcoming_services = ( Service_Service.query @@ -78,6 +85,7 @@ def get_upcoming_service_calls(): @service.route("/past", methods=["GET"]) +@login_required def get_past_service_calls(): """ Fetches a list of all past service calls before today. @@ -97,6 +105,7 @@ def get_past_service_calls(): @service.route("/today", methods=["GET"]) +@login_required def get_today_service_calls(): """ Fetches a list of all service calls for today. @@ -119,6 +128,7 @@ def get_today_service_calls(): @service.route("/upcoming/count", methods=["GET"]) +@login_required def get_upcoming_service_calls_count(): now = datetime.now() try: @@ -128,6 +138,7 @@ def get_upcoming_service_calls_count(): return jsonify({"error": str(e)}), 500 @service.route("/for-customer/", methods=["GET"]) +@login_required def get_service_calls_for_customer(customer_id): service_records = (Service_Service.query.filter_by(customer_id=customer_id).order_by(Service_Service.scheduled_date.desc()).all()) service_schema = Service_Service_schema(many=True) @@ -135,6 +146,7 @@ def get_service_calls_for_customer(customer_id): return jsonify(result), 200 @service.route("/create", methods=["POST"]) +@login_required def create_service_call(): data = request.get_json() if not data: return jsonify({"error": "No data provided"}), 400 @@ -155,6 +167,7 @@ def create_service_call(): return jsonify({ "ok": True, "id": new_service_call.id }), 201 @service.route("/update-cost/", methods=["PUT"]) +@login_required def update_service_cost(id): """ Dedicated endpoint to update only the service cost for a service call. @@ -196,10 +209,11 @@ def update_service_cost(id): except Exception as e: db.session.rollback() - print(f"Error updating service cost for service {id}: {str(e)}") + logger.error(f"Error updating service cost for service {id}: {e}") return jsonify({"error": str(e)}), 500 @service.route("/update/", methods=["PUT"]) +@login_required def update_service_call(id): service_record = Service_Service.query.get_or_404(id) data = request.get_json() @@ -222,6 +236,7 @@ def update_service_call(id): # Service Plans CRUD endpoints @service.route("/plans/active", methods=["GET"]) +@login_required def get_active_service_plans(): """ Get all active service plans (contract_plan > 0) @@ -245,6 +260,7 @@ def get_active_service_plans(): @service.route("/plans/customer/", methods=["GET"]) +@login_required def get_customer_service_plan(customer_id): """ Get service plan for a specific customer @@ -261,6 +277,7 @@ def get_customer_service_plan(customer_id): @service.route("/plans/create", methods=["POST"]) +@login_required def create_service_plan(): """ Create a new service plan for a customer @@ -287,6 +304,7 @@ def create_service_plan(): @service.route("/plans/update/", methods=["PUT"]) +@login_required def update_service_plan(customer_id): """ Update existing service plan for a customer @@ -317,6 +335,7 @@ def update_service_plan(customer_id): @service.route("/plans/delete/", methods=["DELETE"]) +@login_required def delete_service_plan(customer_id): """ Delete service plan for a customer @@ -334,12 +353,14 @@ def delete_service_plan(customer_id): return jsonify({"error": str(e)}), 500 @service.route("/", methods=["GET"]) +@login_required def get_service_by_id(id): service_record = Service_Service.query.get_or_404(id) service_schema = Service_Service_schema() return jsonify({"ok": True, "service": service_schema.dump(service_record)}), 200 @service.route("/delete/", methods=["DELETE"]) +@login_required def delete_service_call(id): service_record = Service_Service.query.get_or_404(id) try: @@ -351,6 +372,7 @@ def delete_service_call(id): return jsonify({"error": str(e)}), 500 @service.route("/parts/customer/", methods=["GET"]) +@login_required def get_service_parts(customer_id): parts = Service_Parts.query.filter_by(customer_id=customer_id).first() if parts: @@ -363,6 +385,7 @@ def get_service_parts(customer_id): }), 200 @service.route("/parts/update/", methods=["POST"]) +@login_required def update_service_parts(customer_id): try: data = request.get_json() @@ -397,6 +420,7 @@ def update_service_parts(customer_id): @service.route("/payment//", methods=["PUT"]) +@login_required def process_service_payment(service_id, payment_type): service = db.session.query(Service_Service).filter(Service_Service.id == service_id).first() if not service: diff --git a/app/social/views.py b/app/social/views.py index 45bc67f..e12dca8 100644 --- a/app/social/views.py +++ b/app/social/views.py @@ -1,14 +1,20 @@ +import logging from flask import jsonify, request import datetime from app.social import social from app import db -from app.classes.customer_social import (Customer_Customer_Social_schema, +from app.classes.customer_social import (Customer_Customer_Social_schema, Customer_Customer_Social) +from flask_login import login_required + +logger = logging.getLogger(__name__) @social.route("/posts//", methods=["GET"]) +@login_required def get_customer_posts(customer_id, page): + logger.info(f"GET /social/posts/{customer_id}/{page} - Fetching customer posts page {page}") per_page_amount = 50 if page is None: offset_limit = 0 @@ -16,7 +22,7 @@ def get_customer_posts(customer_id, page): offset_limit = 0 else: offset_limit = (per_page_amount * page) - per_page_amount - + customer_posts = (db.session .query(Customer_Customer_Social) .filter(Customer_Customer_Social.customer_id == customer_id) @@ -28,8 +34,10 @@ def get_customer_posts(customer_id, page): @social.route("/create/", methods=["POST"]) +@login_required def create_post(customer_id): - + logger.info(f"POST /social/create/{customer_id} - Creating new social post") + comment = request.json["comment"] poster_employee_id = request.json["poster_employee_id"] @@ -47,7 +55,9 @@ def create_post(customer_id): @social.route("/posts/", methods=["PATCH"]) +@login_required def edit_post(post_id): + logger.info(f"PATCH /social/posts/{post_id} - Editing social post") customer_post = (db.session .query(Customer_Customer_Social) @@ -55,7 +65,7 @@ def edit_post(post_id): .first()) comment = request.json["comment"] customer_post.comment = comment - + db.session.add(customer_post) db.session.commit() @@ -64,7 +74,9 @@ def edit_post(post_id): @social.route("/delete/", methods=["DELETE"]) +@login_required def delete_post(post_id): + logger.info(f"DELETE /social/delete/{post_id} - Deleting social post") customer_post = (db.session .query(Customer_Customer_Social) diff --git a/app/stats/views.py b/app/stats/views.py index 3176da3..49ebbd0 100755 --- a/app/stats/views.py +++ b/app/stats/views.py @@ -1,3 +1,4 @@ +import logging from flask import jsonify from datetime import date from app.stats import stats @@ -7,6 +8,8 @@ from app.classes.delivery import Delivery_Delivery from app.classes.stats_company import Stats_Company, Stats_Company_schema from app.classes.stats_customer import Stats_Customer, Stats_Customer_schema +logger = logging.getLogger(__name__) + def get_monday_date(date_object): """Gets the date of the Monday for the given date.""" @@ -27,6 +30,7 @@ def get_monday_date(date_object): @stats.route("/calls/add", methods=["PUT"]) def total_calls_post(): + logger.info("PUT /stats/calls/add - Incrementing call count") total_calls_today = (db.session .query(Stats_Company) .filter(Stats_Company.expected_delivery_date == date.today()) @@ -38,17 +42,18 @@ def total_calls_post(): db.session.add(total_calls_today) db.session.commit() - + return jsonify({"ok": True,}), 200 @stats.route("/calls/count/today", methods=["GET"]) def total_calls_today(): + logger.info("GET /stats/calls/count/today - Getting today's call count") total_calls_today = (db.session .query(Stats_Company) .filter(Stats_Company.expected_delivery_date == date.today()) .count()) - + return jsonify({"ok": True, 'data': total_calls_today, }), 200 @@ -56,17 +61,18 @@ def total_calls_today(): @stats.route("/gallons/total/", methods=["GET"]) def total_gallons_delivered_driver(driver_id): + logger.info(f"GET /stats/gallons/total/{driver_id} - Calculating total gallons for driver") gallons_list = [] - + total_gallons = db.session\ .query(Delivery_Delivery)\ .filter(Delivery_Delivery.driver_employee_id == driver_id)\ .all() - + for f in total_gallons: gallons_list.append(f.gallons_delivered) sum_of_gallons = (sum(gallons_list)) - + return jsonify({"ok": True, 'data': sum_of_gallons, }), 200 @@ -74,6 +80,7 @@ def total_gallons_delivered_driver(driver_id): @stats.route("/delivery/total/", methods=["GET"]) def total_deliveries_driver(driver_id): + logger.info(f"GET /stats/delivery/total/{driver_id} - Counting total deliveries for driver") total_stops = (db.session .query(Delivery_Delivery) .filter(Delivery_Delivery.driver_employee_id == driver_id) @@ -85,12 +92,13 @@ def total_deliveries_driver(driver_id): @stats.route("/primes/total/", methods=["GET"]) def total_primes_driver(driver_id): + logger.info(f"GET /stats/primes/total/{driver_id} - Counting prime deliveries for driver") total_stops = (db.session .query(Delivery_Delivery) .filter(Delivery_Delivery.driver_employee_id == driver_id) .filter(Delivery_Delivery.prime == 1) .count()) - + return jsonify({"ok": True, 'data': total_stops, @@ -98,6 +106,7 @@ def total_primes_driver(driver_id): @stats.route("/delivery/count/today", methods=["GET"]) def total_deliveries_today(): + logger.info("GET /stats/delivery/count/today - Counting today's deliveries") total_stops = (db.session .query(Delivery_Delivery) .filter(Delivery_Delivery.expected_delivery_date == date.today()) @@ -109,12 +118,13 @@ def total_deliveries_today(): @stats.route("/delivery/count/delivered/today", methods=["GET"]) def total_deliveries_today_finished(): + logger.info("GET /stats/delivery/count/delivered/today - Counting completed deliveries today") total_stops = (db.session .query(Delivery_Delivery) .filter(Delivery_Delivery.expected_delivery_date == date.today()) .filter((Delivery_Delivery.delivery_status == 10)) .count()) - + return jsonify({"ok": True, 'data': total_stops, }), 200 @@ -125,6 +135,7 @@ def get_user_stats(user_id): """ gets stats of user """ + logger.info(f"GET /stats/user/{user_id} - Fetching user statistics") get_user = db.session \ .query(Stats_Customer) \ .filter(Stats_Customer.customer_id == user_id) \ @@ -157,6 +168,7 @@ def get_user_last_delivery(user_id): """ gets users last delivery. used on profile page """ + logger.info(f"GET /stats/user/lastdelivery/{user_id} - Fetching user's last delivery date") get_delivery= db.session \ .query(Delivery_Delivery) \ .filter(Delivery_Delivery.customer_id == user_id) \ @@ -174,6 +186,7 @@ def get_user_last_delivery(user_id): @stats.route("/gallons/week", methods=["GET"]) def total_gallons_delivered_this_week(): + logger.info("GET /stats/gallons/week - Calculating weekly gallons delivered") # Get today's date total_gallons = 0 @@ -194,6 +207,7 @@ def total_gallons_delivered_this_week(): @stats.route("/gallons/check/total/", methods=["GET"]) def calculate_gallons_user(user_id): + logger.info(f"GET /stats/gallons/check/total/{user_id} - Recalculating user total gallons") # Get today's date total_gallons = 0 @@ -215,4 +229,3 @@ def calculate_gallons_user(user_id): db.session.commit() return jsonify({"ok": True, }), 200 - diff --git a/app/ticket/views.py b/app/ticket/views.py index 1389e0a..8adbe37 100644 --- a/app/ticket/views.py +++ b/app/ticket/views.py @@ -1,14 +1,16 @@ +import logging from flask import jsonify from app.ticket import ticket from app import db from app.classes.delivery import Delivery_Delivery +logger = logging.getLogger(__name__) + @ticket.route("/", methods=["GET"]) def get_ticket_printer_letter(ticket_id): + logger.info(f"GET /ticket/{ticket_id} - Generating ticket printer letter") - - return jsonify({"ok": True, - + }), 200 diff --git a/config.py b/config.py index 8946cc2..03cad7d 100644 --- a/config.py +++ b/config.py @@ -3,7 +3,6 @@ import os def load_config(mode=os.environ.get('MODE')): try: - print(f"mode is {mode}") if mode == 'PRODUCTION': from settings_prod import ApplicationConfig return ApplicationConfig @@ -21,4 +20,4 @@ def load_config(mode=os.environ.get('MODE')): except ImportError: from settings_local import ApplicationConfig - return ApplicationConfig \ No newline at end of file + return ApplicationConfig diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/b43a39b1cf25_initial_baseline.py b/migrations/versions/b43a39b1cf25_initial_baseline.py new file mode 100644 index 0000000..de419ca --- /dev/null +++ b/migrations/versions/b43a39b1cf25_initial_baseline.py @@ -0,0 +1,185 @@ +"""Initial baseline + +Revision ID: b43a39b1cf25 +Revises: +Create Date: 2026-01-21 02:25:55.179218 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'b43a39b1cf25' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('printer_jobs') + op.drop_table('query_town_ist') + op.drop_table('taxes_pricing') + op.drop_table('pricing_service_general') + op.drop_table('portal_user') + op.drop_table('delivery_payment') + with op.batch_alter_table('street_reference', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_public_street_reference_osm_id')) + batch_op.drop_index(batch_op.f('ix_public_street_reference_street_name_normalized')) + batch_op.drop_index(batch_op.f('ix_street_ref_name_town')) + batch_op.drop_index(batch_op.f('ix_street_ref_town_state')) + + op.drop_table('street_reference') + with op.batch_alter_table('auth_users', schema=None) as batch_op: + batch_op.create_unique_constraint(None, ['id']) + + with op.batch_alter_table('auto_delivery', schema=None) as batch_op: + batch_op.alter_column('estimated_gallons_left', + existing_type=sa.INTEGER(), + type_=sa.DECIMAL(precision=6, scale=2), + existing_nullable=True) + batch_op.alter_column('estimated_gallons_left_prev_day', + existing_type=sa.INTEGER(), + type_=sa.DECIMAL(precision=6, scale=2), + existing_nullable=True) + + with op.batch_alter_table('customer_customer', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_public_customer_customer_auth_net_profile_id'), ['auth_net_profile_id'], unique=True) + batch_op.drop_column('verified_at') + + with op.batch_alter_table('service_service', schema=None) as batch_op: + batch_op.alter_column('when_ordered', + existing_type=sa.DATE(), + type_=sa.DATETIME(), + existing_nullable=True) + batch_op.alter_column('scheduled_date', + existing_type=postgresql.DOMAIN('time_stamp', TIMESTAMP()), + type_=sa.DATETIME(), + existing_nullable=True, + existing_server_default=sa.text('CURRENT_TIMESTAMP(2)')) + batch_op.alter_column('service_cost', + existing_type=sa.NUMERIC(precision=10, scale=2), + nullable=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('service_service', schema=None) as batch_op: + batch_op.alter_column('service_cost', + existing_type=sa.NUMERIC(precision=10, scale=2), + nullable=True) + batch_op.alter_column('scheduled_date', + existing_type=sa.DATETIME(), + type_=postgresql.DOMAIN('time_stamp', TIMESTAMP()), + existing_nullable=True, + existing_server_default=sa.text('CURRENT_TIMESTAMP(2)')) + batch_op.alter_column('when_ordered', + existing_type=sa.DATETIME(), + type_=sa.DATE(), + existing_nullable=True) + + with op.batch_alter_table('customer_customer', schema=None) as batch_op: + batch_op.add_column(sa.Column('verified_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True)) + batch_op.drop_index(batch_op.f('ix_public_customer_customer_auth_net_profile_id')) + + with op.batch_alter_table('auto_delivery', schema=None) as batch_op: + batch_op.alter_column('estimated_gallons_left_prev_day', + existing_type=sa.DECIMAL(precision=6, scale=2), + type_=sa.INTEGER(), + existing_nullable=True) + batch_op.alter_column('estimated_gallons_left', + existing_type=sa.DECIMAL(precision=6, scale=2), + type_=sa.INTEGER(), + existing_nullable=True) + + with op.batch_alter_table('auth_users', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='unique') + + op.create_table('street_reference', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('street_name', sa.VARCHAR(length=500), autoincrement=False, nullable=False), + sa.Column('street_name_normalized', sa.VARCHAR(length=500), autoincrement=False, nullable=False), + sa.Column('street_number_low', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('street_number_high', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('town', sa.VARCHAR(length=140), autoincrement=False, nullable=False), + sa.Column('town_normalized', sa.VARCHAR(length=140), autoincrement=False, nullable=False), + sa.Column('state', sa.VARCHAR(length=2), autoincrement=False, nullable=False), + sa.Column('zip_codes', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.Column('osm_id', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('street_reference_pkey')) + ) + with op.batch_alter_table('street_reference', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_street_ref_town_state'), ['town_normalized', 'state'], unique=False) + batch_op.create_index(batch_op.f('ix_street_ref_name_town'), ['street_name_normalized', 'town_normalized'], unique=False) + batch_op.create_index(batch_op.f('ix_public_street_reference_street_name_normalized'), ['street_name_normalized'], unique=False) + batch_op.create_index(batch_op.f('ix_public_street_reference_osm_id'), ['osm_id'], unique=False) + + op.create_table('delivery_payment', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('delivery_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('time_added', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), + sa.Column('total_amount_oil', sa.NUMERIC(precision=50, scale=2), autoincrement=False, nullable=True), + sa.Column('total_amount_emergency', sa.NUMERIC(precision=50, scale=2), autoincrement=False, nullable=True), + sa.Column('total_amount_prime', sa.NUMERIC(precision=50, scale=2), autoincrement=False, nullable=True), + sa.Column('total_amount_fee', sa.NUMERIC(precision=50, scale=2), autoincrement=False, nullable=True), + sa.Column('total_amount', sa.NUMERIC(precision=50, scale=2), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('delivery_payment_pkey')) + ) + op.create_table('portal_user', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('username', sa.VARCHAR(length=50), autoincrement=False, nullable=True), + sa.Column('account_number', sa.VARCHAR(length=32), autoincrement=False, nullable=True), + sa.Column('house_number', sa.VARCHAR(length=32), autoincrement=False, nullable=True), + sa.Column('email', sa.VARCHAR(length=350), autoincrement=False, nullable=True), + sa.Column('password_hash', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('member_since', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), + sa.Column('last_seen', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), + sa.Column('admin', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('admin_role', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('confirmed', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('active', sa.INTEGER(), server_default=sa.text('1'), autoincrement=False, nullable=True), + sa.Column('password_reset_expires', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), + sa.Column('password_reset_token', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('confirmation_token', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('confirmation_sent_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), + sa.Column('confirmed_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('portal_user_pkey')) + ) + op.create_table('pricing_service_general', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('price_service_hour', sa.NUMERIC(precision=50, scale=2), autoincrement=False, nullable=True), + sa.Column('price_emergency_service_hour', sa.NUMERIC(precision=50, scale=2), autoincrement=False, nullable=True), + sa.Column('price_emergency_call', sa.NUMERIC(precision=50, scale=2), autoincrement=False, nullable=True), + sa.Column('price_out_of_oil', sa.NUMERIC(precision=50, scale=2), autoincrement=False, nullable=True), + sa.Column('price_prime', sa.NUMERIC(precision=50, scale=2), autoincrement=False, nullable=True), + sa.Column('price_cleaning', sa.NUMERIC(precision=50, scale=2), autoincrement=False, nullable=True), + sa.Column('date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pricing_service_general_pkey')) + ) + op.create_table('taxes_pricing', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('state_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('taxes_oil', sa.NUMERIC(precision=50, scale=2), autoincrement=False, nullable=True), + sa.Column('taxes_other', sa.NUMERIC(precision=50, scale=2), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('taxes_pricing_pkey')) + ) + op.create_table('query_town_ist', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('value', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('text', sa.VARCHAR(length=240), autoincrement=False, nullable=True) + ) + op.create_table('printer_jobs', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('delivery_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('date_added', sa.DATE(), autoincrement=False, nullable=True), + sa.Column('date_completed', sa.DATE(), autoincrement=False, nullable=True), + sa.Column('employee_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('status', sa.INTEGER(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('printer_jobs_pkey')) + ) + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index ac49817..3865176 100755 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ Flask-Bcrypt==1.0.1 flask-cors==5.0.1 Flask-Login==0.6.3 Flask-Mail==0.10.0 +Flask-Migrate==4.0.7 flask-marshmallow==1.3.0 Flask-Moment==1.0.6 Flask-Paranoid==0.3.0 diff --git a/settings_dev.py b/settings_dev.py index 0dbf686..04f1adb 100644 --- a/settings_dev.py +++ b/settings_dev.py @@ -1,38 +1,42 @@ +import os class ApplicationConfig: """ - Basic Configuration for a generic User + Development Configuration """ - CURRENT_SETTINGS = 'LOCAL' - # databases info - POSTGRES_USERNAME = 'postgres' - POSTGRES_PW = 'password' - POSTGRES_SERVER = '192.168.1.204:5432' - POSTGRES_DBNAME00 = 'eamco' - SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME, - POSTGRES_PW, - POSTGRES_SERVER, - POSTGRES_DBNAME00 - ) + CURRENT_SETTINGS = 'DEVELOPMENT' + + # Database credentials (defaults for local dev) + POSTGRES_USERNAME = os.environ.get('POSTGRES_USERNAME', 'postgres') + POSTGRES_PW = os.environ.get('POSTGRES_PW', 'password') + POSTGRES_SERVER = os.environ.get('POSTGRES_SERVER', '192.168.1.204:5432') + POSTGRES_DBNAME00 = os.environ.get('POSTGRES_DBNAME', 'eamco') + + SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format( + POSTGRES_USERNAME, + POSTGRES_PW, + POSTGRES_SERVER, + POSTGRES_DBNAME00 + ) SQLALCHEMY_BINDS = {'eamco': SQLALCHEMY_DATABASE_URI} - # sqlalchemy config + + # SQLAlchemy config SQLALCHEMY_TRACK_MODIFICATIONS = False TRAP_HTTP_EXCEPTIONS = True PROPAGATE_EXCEPTIONS = True DEBUG = True UPLOADED_FILES_DEST_ITEM = '/data/item' - # file uploads + # File uploads UPLOADED_FILES_ALLOW = ['png', 'jpeg', 'jpg', 'png', 'gif'] MAX_CONTENT_LENGTH = 5 * 2500 * 2500 ALLOWED_EXTENSIONS = ['png', 'jpeg', 'jpg', 'png', 'gif'] - # secret keys - SECRET_KEY = "youwillneverguessthiskeycia" + # Secret key (default for local dev only) + SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key-not-for-production') - # sessions - # Available SESSION_TYPE options: 'redis', 'sqlalchemy', 'mongodb', 'filesystem', 'memcached' + # Sessions SESSION_TYPE = "sqlalchemy" SESSION_COOKIE_NAME = "eamco_session" SESSION_COOKIE_SECURE = False @@ -43,13 +47,8 @@ class ApplicationConfig: SESSION_USE_SIGNER = True # CORS - - CORS_ALLOWED_ORIGINS = [ - "*" - ] + CORS_ALLOWED_ORIGINS = ["*"] CORS_SEND_WILDCARD = False CORS_SUPPORT_CREDENTIALS = True CORS_EXPOSE_HEADERS = None CORS_ALLOW_HEADERS = "*" - - diff --git a/settings_local.py b/settings_local.py index 54d9ad1..f1be435 100755 --- a/settings_local.py +++ b/settings_local.py @@ -1,38 +1,42 @@ +import os class ApplicationConfig: """ - Basic Configuration for a generic User + Local Configuration (LAN deployment) """ CURRENT_SETTINGS = 'LOCAL' - # databases info - POSTGRES_USERNAME = 'postgres' - POSTGRES_PW = 'password' - POSTGRES_SERVER = '192.168.1.204:5432' - POSTGRES_DBNAME00 = 'auburnoil' - SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME, - POSTGRES_PW, - POSTGRES_SERVER, - POSTGRES_DBNAME00 - ) + + # Database credentials from environment variables + POSTGRES_USERNAME = os.environ.get('POSTGRES_USERNAME', 'postgres') + POSTGRES_PW = os.environ.get('POSTGRES_PW') + POSTGRES_SERVER = os.environ.get('POSTGRES_SERVER', '192.168.1.204:5432') + POSTGRES_DBNAME00 = os.environ.get('POSTGRES_DBNAME', 'auburnoil') + + SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format( + POSTGRES_USERNAME, + POSTGRES_PW, + POSTGRES_SERVER, + POSTGRES_DBNAME00 + ) SQLALCHEMY_BINDS = {'auburnoil': SQLALCHEMY_DATABASE_URI} - # sqlalchemy config + + # SQLAlchemy config SQLALCHEMY_TRACK_MODIFICATIONS = False TRAP_HTTP_EXCEPTIONS = True PROPAGATE_EXCEPTIONS = True DEBUG = True UPLOADED_FILES_DEST_ITEM = '/data/item' - # file uploads + # File uploads UPLOADED_FILES_ALLOW = ['png', 'jpeg', 'jpg', 'png', 'gif'] MAX_CONTENT_LENGTH = 5 * 2500 * 2500 ALLOWED_EXTENSIONS = ['png', 'jpeg', 'jpg', 'png', 'gif'] - # secret keys - SECRET_KEY = "youwillneverguessthiskeycia" + # Secret key from environment variable + SECRET_KEY = os.environ.get('SECRET_KEY') - # sessions - # Available SESSION_TYPE options: 'redis', 'sqlalchemy', 'mongodb', 'filesystem', 'memcached' + # Sessions SESSION_TYPE = "sqlalchemy" SESSION_COOKIE_NAME = "eamco_session" SESSION_COOKIE_SECURE = False @@ -43,7 +47,6 @@ class ApplicationConfig: SESSION_USE_SIGNER = True # CORS - CORS_SEND_WILDCARD = False CORS_SUPPORT_CREDENTIALS = True CORS_EXPOSE_HEADERS = None @@ -54,5 +57,4 @@ class ApplicationConfig: 'http://192.168.1.204:9612', 'http://192.168.1.204:9613', 'http://192.168.1.204:9614', - - ] \ No newline at end of file + ] diff --git a/settings_prod.py b/settings_prod.py index f580868..8a9f5e6 100644 --- a/settings_prod.py +++ b/settings_prod.py @@ -1,38 +1,42 @@ +import os class ApplicationConfig: """ - Basic Configuration for a generic User + Production Configuration """ CURRENT_SETTINGS = 'PRODUCTION' - # databases info - POSTGRES_USERNAME = 'postgres' - POSTGRES_PW = 'password' - POSTGRES_SERVER = '192.168.1.204:5432' - - POSTGRES_DBNAME00 = 'auburnoil' - SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME, - POSTGRES_PW, - POSTGRES_SERVER, - POSTGRES_DBNAME00 - ) + + # Database credentials from environment variables + POSTGRES_USERNAME = os.environ.get('POSTGRES_USERNAME', 'postgres') + POSTGRES_PW = os.environ.get('POSTGRES_PW') + POSTGRES_SERVER = os.environ.get('POSTGRES_SERVER', '192.168.1.204:5432') + POSTGRES_DBNAME00 = os.environ.get('POSTGRES_DBNAME', 'auburnoil') + + SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format( + POSTGRES_USERNAME, + POSTGRES_PW, + POSTGRES_SERVER, + POSTGRES_DBNAME00 + ) SQLALCHEMY_BINDS = {'auburnoil': SQLALCHEMY_DATABASE_URI} - # sqlalchemy config + + # SQLAlchemy config SQLALCHEMY_TRACK_MODIFICATIONS = False TRAP_HTTP_EXCEPTIONS = True PROPAGATE_EXCEPTIONS = True DEBUG = False UPLOADED_FILES_DEST_ITEM = '/data/item' - # file uploads + # File uploads UPLOADED_FILES_ALLOW = ['png', 'jpeg', 'jpg', 'png', 'gif'] MAX_CONTENT_LENGTH = 5 * 2500 * 2500 ALLOWED_EXTENSIONS = ['png', 'jpeg', 'jpg', 'png', 'gif'] - # secret keys - SECRET_KEY = "34dsfkjh43123cxzfvqwer23432dsf233214efdasf2134321" + # Secret key from environment variable + SECRET_KEY = os.environ.get('SECRET_KEY') - # sessions + # Sessions SESSION_TYPE = "sqlalchemy" SESSION_COOKIE_NAME = "eamco_session" SESSION_COOKIE_SECURE = False @@ -43,9 +47,6 @@ class ApplicationConfig: SESSION_USE_SIGNER = True # CORS - - - CORS_SEND_WILDCARD = False CORS_SUPPORT_CREDENTIALS = True CORS_EXPOSE_HEADERS = None @@ -53,4 +54,4 @@ class ApplicationConfig: CORS_ALLOWED_ORIGINS = [ 'https://oil.edwineames.com', 'https://edwineames.com' - ] \ No newline at end of file + ] diff --git a/start.sh b/start.sh index a697749..0604f80 100644 --- a/start.sh +++ b/start.sh @@ -1,6 +1,9 @@ #!/bin/bash set -e +# Run database migrations +flask db upgrade + # Start Gunicorn gunicorn --bind 127.0.0.1:8000 --workers 4 --timeout 120 app:app &