first commit

This commit is contained in:
2026-01-17 15:21:41 -05:00
commit b93d41c1ae
36 changed files with 3391 additions and 0 deletions

122
.gitignore vendored Normal file
View File

@@ -0,0 +1,122 @@
# Created by .ignore support plugin (hsz.mobi)
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
*.idea
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
*.iml
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
db.sqlite3
# Flask stuff:
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
venvwindows/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
app/package.json
app/package-lock.json
.vscode/
instance/config.py
.idea/
/passwords.py
getnewitems.py
helperfunctions/
test.py
tools/
nginx.txt
app/node_modules/

19
Dockerfile.dev Normal file
View File

@@ -0,0 +1,19 @@
FROM python:3.12-bullseye
ENV PYTHONFAULTHANDLER=1
ENV PYTHONUNBUFFERED=1
ENV MODE="DEVELOPMENT"
RUN mkdir -p /app
COPY requirements.txt /app
WORKDIR /app
RUN pip3 install -r requirements.txt
EXPOSE 8000
COPY . /app

19
Dockerfile.local Normal file
View File

@@ -0,0 +1,19 @@
FROM python:3.12-bullseye
ENV PYTHONFAULTHANDLER=1
ENV PYTHONUNBUFFERED=1
ENV MODE="LOCAL"
RUN mkdir -p /app
COPY requirements.txt /app
WORKDIR /app
RUN pip3 install -r requirements.txt
EXPOSE 8000
COPY . /app

21
Dockerfile.prod Normal file
View File

@@ -0,0 +1,21 @@
FROM python:3.12-bullseye
ENV PYTHONFAULTHANDLER=1
ENV PYTHONUNBUFFERED=1
ENV MODE="PRODUCTION"
RUN mkdir -p /app
COPY requirements.txt /app
WORKDIR /app
RUN pip3 install -r requirements.txt
EXPOSE 80
COPY . /app
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]

1
README.md Normal file
View File

@@ -0,0 +1 @@
# API

0
__init__.py Normal file
View File

29
config.py Normal file
View File

@@ -0,0 +1,29 @@
import os
def load_config(mode=os.environ.get('MODE')):
"""
Load application configuration from environment variables.
The MODE environment variable determines which default CORS origins to use,
but all secrets are loaded from environment variables.
"""
from settings import ApplicationConfig
print(f"Mode is {mode or 'not set (defaulting to DEVELOPMENT)'}")
# Validate required configuration
try:
ApplicationConfig.validate_required()
except ValueError as e:
print(f"\033[91mConfiguration Error: {e}\033[0m")
raise
# Print appropriate warning based on database
if ApplicationConfig.POSTGRES_DBNAME00 == 'eamco':
print("\033[92mTest database confirmed\033[0m")
else:
print("\033[91mPRODUCTION DATABASE\033[0m")
if mode == 'PRODUCTION':
print("\033[91mPRODUCTION MODE\033[0m")
return ApplicationConfig

36
database.py Normal file
View File

@@ -0,0 +1,36 @@
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.engine import URL
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import declarative_base
from config import load_config
ApplicationConfig = load_config()
url = URL.create(
drivername="postgresql+asyncpg",
username=ApplicationConfig.POSTGRES_USERNAME,
password=ApplicationConfig.POSTGRES_PW,
host=ApplicationConfig.POSTGRES_SERVER,
database=ApplicationConfig.POSTGRES_DBNAME00,
port=ApplicationConfig.POSTGRES_PORT
)
engine = create_async_engine(url, echo=False)
AsyncSessionLocal = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
Base = declarative_base()
# Dependency to get DB session
async def get_db() -> AsyncSession:
async with AsyncSessionLocal() as session:
try:
yield session
finally:
await session.close()

43
main.py Normal file
View File

@@ -0,0 +1,43 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from contextlib import asynccontextmanager
from routes import auth, info, order, payment
import os
from config import load_config
from database import engine, Base
ApplicationConfig = load_config()
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: Create database tables
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
# Shutdown: cleanup if needed
app = FastAPI(title="Oil Customer Gateway API", version="1.0.0", lifespan=lifespan)
app.include_router(auth, prefix="/auth", tags=["auth"])
app.include_router(info, prefix="/info", tags=["info"])
app.include_router(order, prefix="/order", tags=["order"])
app.include_router(payment, prefix="/payment", tags=["payment"])
@app.get("/")
async def root():
return {"message": "Oil Customer Gateway API", "status": "Api is online"}
app.add_middleware(
CORSMiddleware,
allow_origins=ApplicationConfig.origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Mount static files for uploaded images
app.mount("/images", StaticFiles(directory="/images"), name="images")

15
models/__init__.py Normal file
View File

@@ -0,0 +1,15 @@
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime
import uuid
def get_uuid():
return str(uuid.uuid4())
Base = declarative_base()
from .customer import Customer_Customer, Customer_Description, Customer_Tank_Inspection, Customer_Stats
from .delivery import Delivery_Delivery
from .pricing import Pricing_Oil_Oil
from .company import Company_Company
from .account import Account_User
from .card import Card, Transaction

62
models/account.py Normal file
View File

@@ -0,0 +1,62 @@
from sqlalchemy import Column, Integer, String, TIMESTAMP, TEXT, VARCHAR
from datetime import datetime, timezone
from . import Base
class Account_User(Base):
__tablename__ = 'portal_user'
__table_args__ = {"schema": "public"}
id = Column(Integer, autoincrement=True, primary_key=True, unique=True)
username = Column(String(50))
account_number = Column(String(32))
house_number = Column(String(32))
email = Column(VARCHAR(350))
password_hash = Column(TEXT)
member_since = Column(TIMESTAMP(timezone=True), default=lambda: datetime.now(timezone.utc))
last_seen = Column(TIMESTAMP(timezone=True), default=lambda: datetime.now(timezone.utc))
password_reset_token = Column(TEXT, nullable=True)
password_reset_expires = Column(TIMESTAMP(timezone=True), nullable=True)
admin = Column(Integer)
admin_role = Column(Integer)
confirmed = Column(Integer)
active = Column(Integer, default=1)
user_id = Column(Integer, nullable=True) # References Customer_Customer.id
def __init__(self,
username,
account_number,
house_number,
password_hash,
member_since,
email,
last_seen,
admin,
admin_role,
confirmed,
active=1,
user_id=None,
):
self.username = username
self.account_number = account_number
self.house_number = house_number
self.password_hash = password_hash
self.member_since = member_since
self.email = email
self.last_seen = last_seen
self.admin = admin
self.admin_role = admin_role
self.confirmed = confirmed
self.active = active
self.user_id = user_id
def is_authenticated(self):
return True
def is_active(self):
return True
def is_anonymous(self):
return False
def get_id(self):
return str(self.id)

46
models/card.py Normal file
View File

@@ -0,0 +1,46 @@
from sqlalchemy import Column, Integer, String, TIMESTAMP, TEXT, VARCHAR, Numeric, DateTime, Boolean
from datetime import datetime, timezone
from . import Base
class Card(Base):
__tablename__ = "card_card"
id = Column(Integer, primary_key=True, index=True)
date_added = Column(DateTime, default=datetime.utcnow)
user_id = Column(Integer, nullable=False)
# This stores the payment profile ID for this specific card from Authorize.Net's CIM.
auth_net_payment_profile_id = Column(String, nullable=True)
# Columns to store non-sensitive card info for display purposes
card_number = Column(String(50), nullable=True)
last_four_digits = Column(Integer, nullable=False)
name_on_card = Column(String(500), nullable=True)
expiration_month = Column(String(20), nullable=False)
expiration_year = Column(String(20), nullable=False)
type_of_card = Column(String(50), nullable=True)
security_number = Column(String(10), nullable=True)
accepted_or_declined = Column(Integer, nullable=True)
main_card = Column(Boolean, nullable=True)
zip_code = Column(String(20), nullable=True)
class Transaction(Base):
__tablename__ = "transactions"
id = Column(Integer, primary_key=True, index=True)
# Recommended change: Use Numeric for precision
preauthorize_amount = Column(Numeric(10, 2), nullable=True)
charge_amount = Column(Numeric(10, 2), nullable=True)
customer_id = Column(Integer)
transaction_type = Column(Integer)# 0 = charge, 1 = auth, 2 = capture
status = Column(Integer)
auth_net_transaction_id = Column(String, unique=True, index=True, nullable=True)
service_id = Column(Integer, nullable=True)
delivery_id = Column(Integer, nullable=True)
auto_id = Column(Integer, nullable=True)
card_id = Column(Integer, nullable=True)
payment_gateway = Column(Integer, default=1)
rejection_reason = Column(String, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)

14
models/company.py Normal file
View File

@@ -0,0 +1,14 @@
from sqlalchemy import Column, Integer, String, Boolean, DECIMAL, TIMESTAMP, DATE, TEXT, VARCHAR
from . import Base
class Company_Company(Base):
__tablename__ = 'company_company'
__table_args__ = {"schema": "public"}
id = Column(Integer, primary_key=True, autoincrement=True, unique=False)
company_dba_name = Column(VARCHAR(250))
company_llc_name = Column(VARCHAR(250))
company_town = Column(VARCHAR(140))
company_state = Column(Integer)
company_zip = Column(VARCHAR(25))

65
models/customer.py Normal file
View File

@@ -0,0 +1,65 @@
from sqlalchemy import Column, Integer, String, Boolean, DECIMAL, TIMESTAMP, DATE, TEXT, VARCHAR, JSON
from . import Base
class Customer_Customer(Base):
__tablename__ = 'customer_customer'
__table_args__ = {"schema": "public"}
id = Column(Integer, primary_key=True, autoincrement=True, unique=False)
auth_net_profile_id = Column(String, unique=True, index=True, nullable=True)
account_number = Column(VARCHAR(25))
customer_last_name = Column(VARCHAR(250))
customer_first_name = Column(VARCHAR(250))
customer_town = Column(VARCHAR(140))
customer_state = Column(Integer)
customer_zip = Column(VARCHAR(25))
customer_first_call = Column(TIMESTAMP())
customer_email = Column(VARCHAR(500))
customer_automatic = Column(Integer)
customer_phone_number = Column(VARCHAR(25))
customer_home_type = Column(Integer)
customer_apt = Column(VARCHAR(140))
customer_address = Column(VARCHAR(1000))
company_id = Column(Integer)
customer_latitude = Column(VARCHAR(250))
customer_longitude = Column(VARCHAR(250))
correct_address = Column(Boolean)
class Customer_Description(Base):
__tablename__ = 'customer_description'
__table_args__ = {"schema": "public"}
id = Column(Integer, primary_key=True, autoincrement=True, unique=False)
customer_id = Column(Integer)
account_number = Column(VARCHAR(25))
company_id = Column(Integer)
fill_location = Column(Integer)
description = Column(VARCHAR(2000))
class Customer_Tank_Inspection(Base):
__tablename__ = 'customer_tank'
__table_args__ = {"schema": "public"}
id = Column(Integer, primary_key=True, autoincrement=True, unique=False)
customer_id = Column(Integer)
last_tank_inspection = Column(DATE)
tank_status = Column(Boolean)
outside_or_inside = Column(Boolean)
tank_size = Column(Integer)
tank_images = Column(Integer, default=0) # Number of image sets uploaded (each set = 3 images)
tank_image_upload_dates = Column(JSON, default=list) # List of upload dates for each set
class Customer_Stats(Base):
__tablename__ = 'stats_customer'
__table_args__ = {"schema": "public"}
id = Column(Integer, primary_key=True, autoincrement=True, unique=False)
customer_id = Column(Integer)
total_calls = Column(Integer, default=0)
service_calls_total = Column(Integer, default=0)
service_calls_total_spent = Column(DECIMAL(6, 2), default=0.00)
service_calls_total_profit = Column(DECIMAL(6, 2), default=0.00)
oil_deliveries = Column(Integer, default=0)
oil_total_gallons = Column(DECIMAL(6, 2), default=0.00)
oil_total_spent = Column(DECIMAL(6, 2), default=0.00)
oil_total_profit = Column(DECIMAL(6, 2), default=0.00)

77
models/delivery.py Normal file
View File

@@ -0,0 +1,77 @@
from sqlalchemy import Column, Integer, String, Boolean, DECIMAL, TIMESTAMP, DATE, TEXT, VARCHAR
from . import Base
class Delivery_Delivery(Base):
__tablename__ = 'delivery_delivery'
__table_args__ = {"schema": "public"}
id = Column(Integer, primary_key=True, autoincrement=True, unique=False)
customer_id = Column(Integer)
customer_name = Column(VARCHAR(1000))
customer_address = Column(VARCHAR(1000))
customer_town = Column(VARCHAR(140))
customer_state = Column(VARCHAR(140))
customer_zip = Column(Integer)
# how many gallons ordered
gallons_ordered = Column(Integer)
# if customer asked for a fill
customer_asked_for_fill = Column(Integer)
# integer value if delivered, waiting, cancelled etc
gallons_delivered = Column(DECIMAL(6, 2))
# if customer has a full tank
customer_filled = Column(Integer)
# integer value if delivered, waiting, cancelled etc
# waiting = 0
# cancelled = 1
# out for delivery = 2
# tommorrow = 3
# issue = 5
# finalized = 10
delivery_status = Column(Integer)
# when the call to order took place
when_ordered = Column(DATE(), default=None)
# when the delivery date happened
when_delivered = Column(DATE(), default=None)
# when the delivery is expected ie what day
expected_delivery_date = Column(DATE(), default=None)
# automatic delivery
automatic = Column(Integer)
automatic_id = Column(Integer)
# OIL info and id from table
oil_id = Column(Integer)
supplier_price = Column(DECIMAL(6, 2))
customer_price = Column(DECIMAL(6, 2))
# weather
customer_temperature = Column(DECIMAL(6, 2))
dispatcher_notes = Column(TEXT())
prime = Column(Integer)
same_day = Column(Integer)
emergency = Column(Integer)
# cash = 0
# credit = 1
# credit/cash = 2
# check = 3
# other = 4
payment_type = Column(Integer)
payment_card_id = Column(Integer)
cash_recieved = Column(DECIMAL(6, 2))
driver_employee_id = Column(Integer)
driver_first_name = Column(VARCHAR(140))
driver_last_name = Column(VARCHAR(140))
pre_charge_amount = Column(DECIMAL(6, 2))
total_price = Column(DECIMAL(6, 2))
final_price = Column(DECIMAL(6, 2))
check_number = Column(VARCHAR(20))
promo_id = Column(Integer)
promo_money_discount = Column(DECIMAL(6, 2))

17
models/pricing.py Normal file
View File

@@ -0,0 +1,17 @@
from sqlalchemy import Column, Integer, String, Boolean, DECIMAL, TIMESTAMP, DATE, TEXT, VARCHAR
from datetime import datetime
from . import Base
class Pricing_Oil_Oil(Base):
__tablename__ = 'pricing_oil_oil'
__table_args__ = {"schema": "public"}
id = Column(Integer, primary_key=True, autoincrement=True, unique=False)
price_from_supplier = Column(DECIMAL(6, 2))
price_for_customer = Column(DECIMAL(6, 2))
price_for_employee = Column(DECIMAL(6, 2))
price_same_day = Column(DECIMAL(6, 2))
price_prime = Column(DECIMAL(6, 2))
price_emergency = Column(DECIMAL(6, 2))
date = Column(TIMESTAMP(), default=datetime.utcnow())

12
requirements.txt Normal file
View File

@@ -0,0 +1,12 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
sqlalchemy==2.0.23
alembic==1.13.1
pydantic==2.5.0
python-multipart==0.0.6
asyncpg==0.29.0
psycopg2-binary==2.9.9
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
Pillow==10.1.0
requests==2.31.0

4
routes/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
from .auth import router as auth
from .info import router as info
from .order import router as order
from .payment import router as payment

13
routes/auth/__init__.py Normal file
View File

@@ -0,0 +1,13 @@
from fastapi import APIRouter
from .login import router as login_router
from .register import router as register_router
from .new import router as new_router
from .current_user import router as current_user_router, oauth2_scheme
from .lost_password import router as lost_password_router
router = APIRouter()
router.include_router(login_router)
router.include_router(register_router)
router.include_router(new_router)
router.include_router(current_user_router)
router.include_router(lost_password_router)

191
routes/auth/current_user.py Normal file
View File

@@ -0,0 +1,191 @@
from fastapi import Depends, HTTPException, status, APIRouter
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
import logging
from database import get_db
from models import Account_User, Customer_Customer, Customer_Description, Card
from schemas import TokenData, CustomerUpdate
from jose import JWTError, jwt
from config import load_config
logger = logging.getLogger(__name__)
# Load JWT configuration from environment
ApplicationConfig = load_config()
SECRET_KEY = ApplicationConfig.JWT_SECRET_KEY
ALGORITHM = ApplicationConfig.JWT_ALGORITHM
async def get_current_user(token: str = Depends(OAuth2PasswordBearer(tokenUrl="auth/login")), db: AsyncSession = Depends(get_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user = await db.execute(select(Account_User).where(Account_User.username == token_data.username))
user = user.scalar_one_or_none()
if user is None:
raise credentials_exception
return user
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
router = APIRouter()
@router.get("/me")
async def read_users_me(current_user: Account_User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
# Get customer details for the current user using user_id
customer = None
if current_user.user_id:
customer_result = await db.execute(select(Customer_Customer).where(Customer_Customer.id == current_user.user_id))
customer = customer_result.scalar_one_or_none()
# Get house description if exists
house_description = None
if customer:
description_record = await db.execute(
select(Customer_Description).where(Customer_Description.customer_id == customer.id)
)
description_record = description_record.scalar_one_or_none()
if description_record:
house_description = description_record.description
# Map state code to name
state_mapping = {0: "MA", 1: "NH"} # Add more as needed
state_name = state_mapping.get(customer.customer_state, str(customer.customer_state)) if customer else None
return {
"id": current_user.id,
"username": current_user.username,
"email": current_user.email,
"account_number": current_user.account_number,
"customer_first_name": customer.customer_first_name if customer else None,
"customer_last_name": customer.customer_last_name if customer else None,
"customer_address": customer.customer_address if customer else None,
"customer_apt": customer.customer_apt if customer else None,
"customer_town": customer.customer_town if customer else None,
"customer_state": state_name,
"customer_zip": customer.customer_zip if customer else None,
"customer_phone_number": customer.customer_phone_number if customer else None,
"customer_home_type": customer.customer_home_type if customer else None,
"customer_email": customer.customer_email if customer else None,
"house_description": house_description,
"member_since": current_user.member_since,
"last_seen": current_user.last_seen
}
@router.put("/update-customer")
async def update_customer_info(
customer_data: CustomerUpdate,
current_user: Account_User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
if not current_user.user_id:
raise HTTPException(status_code=404, detail="Customer not found")
try:
# Update Customer_Customer table
customer_update_data = customer_data.dict(exclude_unset=True)
customer_fields = {
'customer_first_name', 'customer_last_name', 'customer_address', 'customer_apt',
'customer_town', 'customer_state', 'customer_zip', 'customer_phone_number',
'customer_home_type', 'customer_email'
}
customer_update_dict = {k: v for k, v in customer_update_data.items() if k in customer_fields}
if customer_update_dict:
await db.execute(
update(Customer_Customer)
.where(Customer_Customer.id == current_user.user_id)
.values(**customer_update_dict)
)
# Update house description if provided
if 'house_description' in customer_update_data and customer_update_data['house_description']:
# Get customer record
customer = await db.execute(
select(Customer_Customer).where(Customer_Customer.id == current_user.user_id)
)
customer = customer.scalar_one_or_none()
if customer:
# Check if description record exists
description_record = await db.execute(
select(Customer_Description).where(Customer_Description.customer_id == customer.id)
)
description_record = description_record.scalar_one_or_none()
if description_record:
# Update existing
await db.execute(
update(Customer_Description)
.where(Customer_Description.customer_id == customer.id)
.values(description=customer_update_data['house_description'])
)
else:
# Create new
new_description = Customer_Description(
customer_id=customer.id,
account_number=current_user.account_number,
company_id=customer.company_id or 1,
fill_location=0,
description=customer_update_data['house_description']
)
db.add(new_description)
await db.commit()
# Sync billing info to Authorize.net if address-related fields were updated
billing_fields = {
'customer_first_name', 'customer_last_name', 'customer_address',
'customer_town', 'customer_state', 'customer_zip', 'customer_phone_number',
'customer_email'
}
if any(k in customer_update_dict for k in billing_fields):
# Late import to avoid circular dependency
from routes.payment.routes import update_customer_profile, update_payment_profile_billing
# Refetch the updated customer
customer_result = await db.execute(
select(Customer_Customer).where(Customer_Customer.id == current_user.user_id)
)
updated_customer = customer_result.scalar_one_or_none()
if updated_customer and updated_customer.auth_net_profile_id:
# Update customer profile in Authorize.net
update_customer_profile(updated_customer.auth_net_profile_id, updated_customer)
# Update all saved payment profiles with new billing info
cards_result = await db.execute(
select(Card).where(Card.user_id == updated_customer.id)
)
cards = cards_result.scalars().all()
for card in cards:
if card.auth_net_payment_profile_id:
update_payment_profile_billing(
customer_profile_id=updated_customer.auth_net_profile_id,
payment_profile_id=card.auth_net_payment_profile_id,
customer=updated_customer,
card=card
)
logger.info(f"Synced billing info to Authorize.net for customer {updated_customer.id}")
return {"message": "Customer information updated successfully"}
except Exception as e:
await db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update customer information: {str(e)}"
)

60
routes/auth/login.py Normal file
View File

@@ -0,0 +1,60 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from database import get_db
from models import Account_User
from schemas import UserLogin, Token
from passlib.context import CryptContext
from jose import jwt
from datetime import datetime, timedelta
from config import load_config
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
# Load JWT configuration from environment
ApplicationConfig = load_config()
SECRET_KEY = ApplicationConfig.JWT_SECRET_KEY
ALGORITHM = ApplicationConfig.JWT_ALGORITHM
ACCESS_TOKEN_EXPIRE_MINUTES = ApplicationConfig.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(data: dict, expires_delta = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def authenticate_user(db: AsyncSession, email: str, password: str):
user = await db.execute(select(Account_User).where(Account_User.email == email))
user = user.scalar_one_or_none()
if not user:
return False
if not verify_password(password, user.password_hash):
return False
# Update last_seen
user.last_seen = datetime.utcnow()
await db.commit()
return user
router = APIRouter()
@router.post("/login", response_model=Token)
async def login(user: UserLogin, db: AsyncSession = Depends(get_db)):
user_obj = await authenticate_user(db, user.email, user.password)
if not user_obj:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user_obj.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}

View File

@@ -0,0 +1,126 @@
import secrets
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from database import get_db
from models import Account_User
from schemas import ForgotPasswordRequest, ResetPasswordRequest
from passlib.context import CryptContext
from config import load_config
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
# Load configuration from environment
ApplicationConfig = load_config()
# Email configuration from environment variables
SMTP_SERVER = ApplicationConfig.SMTP_SERVER
SMTP_PORT = ApplicationConfig.SMTP_PORT
SMTP_USERNAME = ApplicationConfig.SMTP_USERNAME
SMTP_PASSWORD = ApplicationConfig.SMTP_PASSWORD
FROM_EMAIL = ApplicationConfig.SMTP_FROM_EMAIL
router = APIRouter()
def get_password_hash(password):
return pwd_context.hash(password)
def generate_reset_token():
return secrets.token_urlsafe(32)
def send_reset_email(to_email: str, reset_token: str):
"""Send password reset email with link"""
reset_url = f"{ApplicationConfig.FRONTEND_URL}/reset-password?token={reset_token}"
msg = MIMEMultipart()
msg['From'] = FROM_EMAIL
msg['To'] = to_email
msg['Subject'] = "Password Reset - Oil Customer Gateway"
body = f"""
You have requested a password reset for your Oil Customer Gateway account.
Click the following link to reset your password:
{reset_url}
This link will expire in 24 hours.
If you didn't request this reset, please ignore this email.
Best regards,
Oil Customer Gateway Team
"""
msg.attach(MIMEText(body, 'plain'))
try:
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
server.starttls()
server.login(SMTP_USERNAME, SMTP_PASSWORD)
text = msg.as_string()
server.sendmail(FROM_EMAIL, to_email, text)
server.quit()
return True
except Exception as e:
print(f"Email sending failed: {e}")
return False
@router.post("/forgot-password")
async def forgot_password(request: ForgotPasswordRequest, db: AsyncSession = Depends(get_db)):
"""Request password reset - sends email with reset link"""
# Find user by email
result = await db.execute(select(Account_User).where(Account_User.email == request.email))
user = result.scalar_one_or_none()
if not user:
# Don't reveal if email exists or not for security
return {"message": "If an account with that email exists, a password reset link has been sent."}
# Generate reset token and expiry (24 hours)
reset_token = generate_reset_token()
reset_expires = datetime.utcnow() + timedelta(hours=24)
# Update user with reset token
user.password_reset_token = reset_token
user.password_reset_expires = reset_expires
await db.commit()
# Send email
if send_reset_email(user.email, reset_token):
return {"message": "Password reset link sent to your email."}
else:
raise HTTPException(status_code=500, detail="Failed to send reset email")
@router.post("/reset-password")
async def reset_password(request: ResetPasswordRequest, db: AsyncSession = Depends(get_db)):
"""Reset password using token"""
# Verify passwords match
if request.password != request.confirm_password:
raise HTTPException(status_code=400, detail="Passwords do not match")
# Find user by reset token
result = await db.execute(
select(Account_User).where(
Account_User.password_reset_token == request.token,
Account_User.password_reset_expires > datetime.utcnow()
)
)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=400, detail="Invalid or expired reset token")
# Update password
hashed_password = get_password_hash(request.password)
user.password_hash = hashed_password
user.password_reset_token = None
user.password_reset_expires = None
user.last_seen = datetime.utcnow()
await db.commit()
return {"message": "Password reset successfully"}

588
routes/auth/new.py Normal file
View File

@@ -0,0 +1,588 @@
import random
import string
import os
import shutil
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database import get_db
from models import Customer_Customer, Customer_Description, Customer_Tank_Inspection, Customer_Stats, Account_User
from schemas import NewCustomerCreate, UserResponse, CustomerCreateStep1, CustomerAccountCreate
from passlib.context import CryptContext
from datetime import datetime
from PIL import Image
import io
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
def generate_random_number_string(length):
if length < 1:
raise ValueError("Length must be at least 1")
random_number = ''.join(random.choices(string.digits, k=length))
return random_number
def get_password_hash(password):
# Truncate password to 72 bytes max for bcrypt compatibility
truncated_password = password.encode('utf-8')[:72].decode('utf-8', errors='ignore')
return pwd_context.hash(truncated_password)
def resize_image(image_data, max_size=(1024, 1024), quality=85):
"""
Resize image to fit within max_size while maintaining aspect ratio.
Convert to JPEG format and strip metadata for security.
"""
try:
# Open image from bytes
img = Image.open(io.BytesIO(image_data))
# Convert to RGB if necessary (for PNG with transparency, etc.)
if img.mode in ('RGBA', 'LA', 'P'):
img = img.convert('RGB')
# Resize if larger than max_size
if img.width > max_size[0] or img.height > max_size[1]:
img.thumbnail(max_size, Image.Resampling.LANCZOS)
# Save as JPEG with specified quality
output = io.BytesIO()
img.save(output, format='JPEG', quality=quality, optimize=True)
output.seek(0)
return output.getvalue()
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid image file: {str(e)}")
router = APIRouter()
@router.post("/new", response_model=UserResponse)
async def register_new_customer(customer: NewCustomerCreate, db: AsyncSession = Depends(get_db)):
# Verify passwords match
if customer.password != customer.confirm_password:
raise HTTPException(status_code=400, detail="Passwords do not match")
# Check if email already registered
result = await db.execute(select(Account_User).where(Account_User.email == customer.customer_email))
if result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Email already registered")
while True:
random_part = generate_random_number_string(6)
account_number = 'AO-' + random_part
result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number))
existing_customer = result.scalar_one_or_none()
if not existing_customer:
break
# Create customer
customer_data = customer.model_dump()
customer_data.pop('password')
customer_data.pop('confirm_password')
customer_data.update({
'account_number': account_number,
'customer_state': 1, # Default
'customer_automatic': 0,
'company_id': 1, # Default
'customer_latitude': '0',
'customer_longitude': '0',
'correct_address': True,
'customer_first_call': datetime.utcnow()
})
db_customer = Customer_Customer(**customer_data)
db.add(db_customer)
await db.commit()
await db.refresh(db_customer)
# Extract house number from customer address (first part before space)
house_number = customer.customer_address.split()[0] if customer.customer_address else ''
# Create account user
username = account_number
hashed_password = get_password_hash(customer.password)
db_user = Account_User(
username=username,
account_number=account_number,
house_number=house_number,
password_hash=hashed_password,
member_since=datetime.utcnow(),
email=customer.customer_email,
last_seen=datetime.utcnow(),
admin=0,
admin_role=0,
confirmed=1,
active=1,
user_id=db_customer.id
)
db.add(db_user)
await db.commit()
await db.refresh(db_user)
return db_user
@router.post("/step3")
async def upload_tank_images(
account_number: str = Form(...),
tank_image_1: UploadFile = File(...),
tank_image_2: UploadFile = File(...),
tank_image_3: UploadFile = File(...),
db: AsyncSession = Depends(get_db)
):
print("=== STEP3 DEBUG START ===")
print(f"Account number received: '{account_number}'")
# Debug: Check all parameters received
images = [tank_image_1, tank_image_2, tank_image_3]
for i, image in enumerate(images, 1):
print(f"Image {i}: filename='{image.filename}', content_type='{image.content_type}', size={image.size}")
# Validate account number
if not account_number:
print("ERROR: Account number is empty")
raise HTTPException(status_code=400, detail="Account number is required")
# Get customer info for description record
customer_result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number))
customer = customer_result.scalar_one_or_none()
if not customer:
raise HTTPException(status_code=400, detail="Customer not found")
print(f"Creating directory: /images/{account_number}")
# Create directory for account number in the mounted images volume
account_dir = f"/images/{account_number}"
os.makedirs(account_dir, exist_ok=True)
current_datetime = datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S") # YYYY-MM-DD_HH-MM-SS format
# Track if images were uploaded successfully
images_uploaded_successfully = False
# Validate and save images
saved_files = []
try:
for i, image in enumerate(images, 1):
print(f"Processing image {i}...")
# Read image data
image_data = await image.read()
print(f"Image {i} data read: {len(image_data)} bytes")
# Validate file size (max 20MB before processing)
if len(image_data) > 20 * 1024 * 1024:
print(f"ERROR: Image {i} too large: {len(image_data)} bytes")
raise HTTPException(status_code=400, detail=f"File {i} is too large (max 20MB)")
# Resize and process image (this also validates it's a valid image)
print(f"Resizing image {i}...")
processed_image_data = resize_image(image_data, max_size=(1024, 1024), quality=85)
print(f"Image {i} resized: {len(processed_image_data)} bytes")
# Save processed image with datetime-based filename
filename = f"{current_datetime}-{i}.jpg"
file_path = os.path.join(account_dir, filename)
print(f"Saving image {i} to: {file_path}")
with open(file_path, "wb") as buffer:
buffer.write(processed_image_data)
saved_files.append(filename)
print(f"Image {i} saved successfully")
images_uploaded_successfully = True
print(f"All images processed successfully. Saved files: {saved_files}")
except Exception as e:
print(f"ERROR processing images: {str(e)}")
# Don't raise exception - we want to track the failure in the database
images_uploaded_successfully = False
# Update or create customer tank inspection record with tank_images status
print("Updating customer tank inspection record...")
tank_result = await db.execute(
select(Customer_Tank_Inspection).where(Customer_Tank_Inspection.customer_id == customer.id)
)
existing_tank = tank_result.scalar_one_or_none()
if existing_tank:
# Update existing record
if images_uploaded_successfully:
existing_tank.tank_images += 1 # Increment count of image sets
# Append current datetime to upload dates list
current_dates = existing_tank.tank_image_upload_dates or []
current_dates.append(current_datetime)
existing_tank.tank_image_upload_dates = current_dates
await db.commit()
print(f"Updated existing tank inspection record with tank_images = {existing_tank.tank_images}, dates = {existing_tank.tank_image_upload_dates}")
else:
# Create new record
new_tank = Customer_Tank_Inspection(
customer_id=customer.id,
tank_images=1 if images_uploaded_successfully else 0,
tank_image_upload_dates=[current_datetime] if images_uploaded_successfully else []
# Other fields will be null/None initially
)
db.add(new_tank)
await db.commit()
print(f"Created new tank inspection record with tank_images = {new_tank.tank_images}, dates = {new_tank.tank_image_upload_dates}")
print("=== STEP3 DEBUG END ===")
if images_uploaded_successfully:
return {
"message": "Tank images uploaded successfully",
"account_number": account_number,
"uploaded_files": saved_files
}
else:
return {
"message": "Tank images upload skipped or failed",
"account_number": account_number,
"uploaded_files": []
}
@router.post("/upload-tank-images")
async def upload_additional_tank_images(
account_number: str = Form(...),
tank_image_1: UploadFile = File(...),
tank_image_2: UploadFile = File(...),
tank_image_3: UploadFile = File(...),
db: AsyncSession = Depends(get_db)
):
"""
Endpoint for customers to upload additional sets of 3 tank images after registration.
Similar to step3 but for existing customers.
"""
print("=== UPLOAD ADDITIONAL TANK IMAGES DEBUG START ===")
print(f"Account number received: '{account_number}'")
# Debug: Check all parameters received
images = [tank_image_1, tank_image_2, tank_image_3]
for i, image in enumerate(images, 1):
print(f"Image {i}: filename='{image.filename}', content_type='{image.content_type}', size={image.size}")
# Validate account number
if not account_number:
print("ERROR: Account number is empty")
raise HTTPException(status_code=400, detail="Account number is required")
# Get customer info
customer_result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number))
customer = customer_result.scalar_one_or_none()
if not customer:
raise HTTPException(status_code=400, detail="Customer not found")
print(f"Creating directory: /images/{account_number}")
# Create directory for account number in the mounted images volume
account_dir = f"/images/{account_number}"
os.makedirs(account_dir, exist_ok=True)
current_datetime = datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S") # YYYY-MM-DD_HH-MM-SS format
# Track if images were uploaded successfully
images_uploaded_successfully = False
# Validate and save images
saved_files = []
try:
for i, image in enumerate(images, 1):
print(f"Processing image {i}...")
# Read image data
image_data = await image.read()
print(f"Image {i} data read: {len(image_data)} bytes")
# Validate file size (max 20MB before processing)
if len(image_data) > 20 * 1024 * 1024:
print(f"ERROR: Image {i} too large: {len(image_data)} bytes")
raise HTTPException(status_code=400, detail=f"File {i} is too large (max 20MB)")
# Resize and process image (this also validates it's a valid image)
print(f"Resizing image {i}...")
processed_image_data = resize_image(image_data, max_size=(1024, 1024), quality=85)
print(f"Image {i} resized: {len(processed_image_data)} bytes")
# Save processed image with datetime-based filename
filename = f"{current_datetime}-{i}.jpg"
file_path = os.path.join(account_dir, filename)
print(f"Saving image {i} to: {file_path}")
with open(file_path, "wb") as buffer:
buffer.write(processed_image_data)
saved_files.append(filename)
print(f"Image {i} saved successfully")
images_uploaded_successfully = True
print(f"All images processed successfully. Saved files: {saved_files}")
except Exception as e:
print(f"ERROR processing images: {str(e)}")
# Don't raise exception - we want to track the failure in the database
images_uploaded_successfully = False
# Update customer tank inspection record
print("Updating customer tank inspection record...")
tank_result = await db.execute(
select(Customer_Tank_Inspection).where(Customer_Tank_Inspection.customer_id == customer.id)
)
existing_tank = tank_result.scalar_one_or_none()
if existing_tank:
# Update existing record
if images_uploaded_successfully:
existing_tank.tank_images += 1 # Increment count of image sets
# Append current datetime to upload dates list
current_dates = existing_tank.tank_image_upload_dates or []
current_dates.append(current_datetime)
existing_tank.tank_image_upload_dates = current_dates
await db.commit()
print(f"Updated existing tank inspection record with tank_images = {existing_tank.tank_images}, dates = {existing_tank.tank_image_upload_dates}")
else:
# This shouldn't happen for additional uploads, but handle it just in case
new_tank = Customer_Tank_Inspection(
customer_id=customer.id,
tank_images=1 if images_uploaded_successfully else 0,
tank_image_upload_dates=[current_datetime] if images_uploaded_successfully else []
)
db.add(new_tank)
await db.commit()
print(f"Created new tank inspection record with tank_images = {new_tank.tank_images}, dates = {new_tank.tank_image_upload_dates}")
print("=== UPLOAD ADDITIONAL TANK IMAGES DEBUG END ===")
if images_uploaded_successfully:
return {
"message": "Additional tank images uploaded successfully",
"account_number": account_number,
"uploaded_files": saved_files,
"upload_date": current_datetime
}
else:
raise HTTPException(status_code=400, detail="Failed to upload tank images")
@router.get("/tank-images/{account_number}")
async def get_tank_images(account_number: str, db: AsyncSession = Depends(get_db)):
"""
Get tank images information for a customer including upload dates.
"""
# Get customer info
customer_result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number))
customer = customer_result.scalar_one_or_none()
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
# Get tank inspection record
tank_result = await db.execute(
select(Customer_Tank_Inspection).where(Customer_Tank_Inspection.customer_id == customer.id)
)
tank_record = tank_result.scalar_one_or_none()
# Build image sets - first from database, then scan for any additional files
image_sets = []
upload_dates = tank_record.tank_image_upload_dates or [] if tank_record else []
# Add sets from database
for i, upload_date in enumerate(upload_dates):
# Handle backward compatibility: old format used date-only strings and tank_*.jpg files
# New format uses datetime strings and {datetime}-*.jpg files
if "_" in upload_date:
# New datetime format (YYYY-MM-DD_HH-MM-SS)
image_set = {
"date": upload_date,
"images": [
f"/images/{account_number}/{upload_date}-1.jpg",
f"/images/{account_number}/{upload_date}-2.jpg",
f"/images/{account_number}/{upload_date}-3.jpg"
]
}
else:
# Old date-only format (YYYY-MM-DD) - uses tank_*.jpg files
image_set = {
"date": upload_date,
"images": [
f"/images/{account_number}/tank_1.jpg",
f"/images/{account_number}/tank_2.jpg",
f"/images/{account_number}/tank_3.jpg"
]
}
image_sets.append(image_set)
# Scan for any additional image sets that might not be in database
account_dir = f"/images/{account_number}"
if os.path.exists(account_dir):
# Find all image files
all_files = os.listdir(account_dir)
image_files = [f for f in all_files if f.endswith('.jpg')]
# Group by datetime prefix
datetime_groups = {}
for file in image_files:
if file.startswith('tank_'):
# Old tank_*.jpg files - already handled above
continue
elif '_' in file and file.endswith('.jpg'): # datetime format like 2026-01-08_23-25-31-1.jpg
# Extract datetime prefix by removing the image number and .jpg
# Example: 2026-01-08_23-25-31-1.jpg -> 2026-01-08_23-25-31
datetime_prefix = file.rsplit('-', 1)[0] # Remove everything after last dash
if datetime_prefix not in datetime_groups:
datetime_groups[datetime_prefix] = []
datetime_groups[datetime_prefix].append(file)
# Add any datetime groups not already in database
for datetime_prefix, files in datetime_groups.items():
if datetime_prefix not in upload_dates and len(files) >= 3:
# Sort files to ensure correct order
sorted_files = sorted(files)
image_set = {
"date": datetime_prefix,
"images": [
f"/images/{account_number}/{sorted_files[0]}",
f"/images/{account_number}/{sorted_files[1]}",
f"/images/{account_number}/{sorted_files[2]}"
]
}
image_sets.append(image_set)
# Also check for date-prefixed files (like 2026-01-08-1.jpg)
date_groups = {}
for file in image_files:
if file.startswith('tank_') or '_' in file:
continue # Skip old format and datetime format
parts = file.split('-')
if len(parts) == 4 and parts[3] in ['1.jpg', '2.jpg', '3.jpg']:
date_prefix = '-'.join(parts[:3]) # 2026-01-08
if date_prefix not in date_groups:
date_groups[date_prefix] = []
date_groups[date_prefix].append(file)
# Add date groups not already in database
for date_prefix, files in date_groups.items():
if date_prefix not in upload_dates and len(files) >= 3:
# Sort files to ensure correct order
sorted_files = sorted(files)
image_set = {
"date": date_prefix,
"images": [
f"/images/{account_number}/{sorted_files[0]}",
f"/images/{account_number}/{sorted_files[1]}",
f"/images/{account_number}/{sorted_files[2]}"
]
}
image_sets.append(image_set)
# Sort image sets by date descending (newest first)
def sort_key(item):
date_str = item['date']
try:
if '_' in date_str:
return datetime.strptime(date_str, "%Y-%m-%d_%H-%M-%S")
else:
return datetime.strptime(date_str, "%Y-%m-%d")
except:
return datetime.min
image_sets.sort(key=sort_key, reverse=True)
return {
"account_number": account_number,
"image_sets": image_sets
}
@router.post("/step1")
async def create_customer_step1(customer: CustomerCreateStep1, db: AsyncSession = Depends(get_db)):
while True:
random_part = generate_random_number_string(6)
account_number = 'AO-' + random_part
result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number))
existing_customer = result.scalar_one_or_none()
if not existing_customer:
break
# Extract house_description for separate table
house_description = customer.house_description
# Create customer
customer_data = customer.model_dump()
customer_data.pop('house_description') # Remove from customer data
customer_data.update({
'account_number': account_number,
'customer_state': 1, # Default
'customer_automatic': 0,
'company_id': 1, # Default
'customer_latitude': '0',
'customer_longitude': '0',
'correct_address': True,
'customer_first_call': datetime.utcnow()
})
db_customer = Customer_Customer(**customer_data)
db.add(db_customer)
await db.commit()
await db.refresh(db_customer)
# Create customer description if house_description provided
if house_description:
db_description = Customer_Description(
customer_id=db_customer.id,
account_number=account_number,
company_id=1, # Default
fill_location=None, # Will work on this later
description=house_description
# tank_images is now tracked in customer_tank table
)
db.add(db_description)
await db.commit()
# Create customer stats record for tracking metrics
db_stats = Customer_Stats(
customer_id=db_customer.id
# All other fields default to 0/0.00 as defined in the model
)
db.add(db_stats)
await db.commit()
return {"account_number": account_number}
@router.post("/step2", response_model=UserResponse)
async def create_customer_account(account_data: CustomerAccountCreate, db: AsyncSession = Depends(get_db)):
# Verify passwords match
if account_data.password != account_data.confirm_password:
raise HTTPException(status_code=400, detail="Passwords do not match")
# Check if customer exists
result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_data.account_number))
customer = result.scalar_one_or_none()
if not customer:
raise HTTPException(status_code=400, detail="Customer not found")
# Check if email already registered
result = await db.execute(select(Account_User).where(Account_User.email == account_data.customer_email))
if result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Email already registered")
# Update customer email
customer.customer_email = account_data.customer_email
db.add(customer)
await db.commit()
# Extract house number from customer address (first part before space)
house_number = customer.customer_address.split()[0] if customer.customer_address else ''
# Create account user
username = account_data.account_number
hashed_password = get_password_hash(account_data.password)
db_user = Account_User(
username=username,
account_number=account_data.account_number,
house_number=house_number,
password_hash=hashed_password,
member_since=datetime.utcnow(),
email=account_data.customer_email,
last_seen=datetime.utcnow(),
admin=0,
admin_role=0,
confirmed=1,
active=1,
user_id=customer.id
)
db.add(db_user)
await db.commit()
await db.refresh(db_user)
return db_user

71
routes/auth/register.py Normal file
View File

@@ -0,0 +1,71 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from database import get_db
from models import Account_User, Customer_Customer
from schemas import UserCreate, UserResponse
from passlib.context import CryptContext
from datetime import datetime
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
def get_password_hash(password):
return pwd_context.hash(password)
def escape_like_pattern(value: str) -> str:
"""Escape special characters for SQL LIKE patterns.
Escapes %, _, and \ which have special meaning in LIKE clauses
to prevent SQL injection via wildcards.
"""
# Escape backslash first, then the wildcards
return value.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')
router = APIRouter()
@router.post("/register", response_model=UserResponse)
async def register(user: UserCreate, db: AsyncSession = Depends(get_db)):
# Verify passwords match
if user.password != user.confirm_password:
raise HTTPException(status_code=400, detail="Passwords do not match")
# Check if customer exists in Customer_Customer table
# Escape SQL LIKE wildcards to prevent injection attacks
escaped_house_number = escape_like_pattern(user.house_number)
customer_result = await db.execute(
select(Customer_Customer).where(
(Customer_Customer.account_number == user.account_number) &
(Customer_Customer.customer_address.like(f'{escaped_house_number} %', escape='\\'))
)
)
customer = customer_result.scalar_one_or_none()
if not customer:
raise HTTPException(status_code=400, detail="Customer not found with provided account and house number")
# Check if email already registered
result = await db.execute(select(Account_User).where(Account_User.email == user.email))
if result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Email already registered")
username = f"{user.account_number}-{user.house_number}"
hashed_password = get_password_hash(user.password)
db_user = Account_User(
username=username,
account_number=user.account_number,
house_number=user.house_number,
password_hash=hashed_password,
member_since=datetime.utcnow(),
email=user.email,
last_seen=datetime.utcnow(),
admin=0,
admin_role=0,
confirmed=1,
active=1,
user_id=customer.id
)
db.add(db_user)
await db.commit()
await db.refresh(db_user)
return db_user

7
routes/info/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
from fastapi import APIRouter
from .deliveries import router as deliveries_router
from .pricing import router as pricing_router
router = APIRouter()
router.include_router(deliveries_router, tags=["info"])
router.include_router(pricing_router, prefix="/pricing", tags=["pricing"])

77
routes/info/deliveries.py Normal file
View File

@@ -0,0 +1,77 @@
from fastapi import Depends, HTTPException, APIRouter
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc, func
from database import get_db
from models import Delivery_Delivery, Customer_Customer
from routes.auth.current_user import get_current_user
from models import Account_User
router = APIRouter()
STATUS_MAPPING = {
0: "Waiting",
1: "Cancelled",
2: "Today",
3: "Tomorrow_Delivery",
4: "Partial_Delivery",
5: "Issue_Delivery",
9: "Pending",
10: "Finalized"
}
PAYMENT_MAPPING = {
0: "Cash",
1: "Credit",
2: "Credit/Cash",
3: "Check",
4: "Other"
}
@router.get("/deliveries")
async def get_user_deliveries(
current_user: Account_User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
if not current_user.user_id:
raise HTTPException(status_code=404, detail="Customer not found")
# Get customer from user_id
customer = await db.execute(
select(Customer_Customer).where(Customer_Customer.id == current_user.user_id)
)
customer = customer.scalar_one_or_none()
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
# Get last 20 deliveries ordered by delivery date (when_delivered or when_ordered) desc
deliveries_query = await db.execute(
select(Delivery_Delivery)
.where(Delivery_Delivery.customer_id == customer.id)
.order_by(desc(func.coalesce(Delivery_Delivery.when_delivered, Delivery_Delivery.when_ordered)))
.limit(20)
)
deliveries = deliveries_query.scalars().all()
# Format deliveries
result = []
for delivery in deliveries:
# Use when_delivered if available, otherwise when_ordered
delivery_date = delivery.when_delivered or delivery.when_ordered
# Use gallons_delivered if available, otherwise gallons_ordered
gallons = delivery.gallons_delivered if delivery.gallons_delivered is not None else delivery.gallons_ordered
result.append({
"id": delivery.id,
"when_ordered": delivery.when_ordered.isoformat() if delivery.when_ordered else None,
"when_delivered": delivery.when_delivered.isoformat() if delivery.when_delivered else None,
"delivery_date": delivery_date.isoformat() if delivery_date else None,
"gallons_ordered": delivery.gallons_ordered,
"gallons_delivered": float(delivery.gallons_delivered) if delivery.gallons_delivered else None,
"gallons": float(gallons) if gallons is not None else None,
"customer_price": float(delivery.customer_price) if delivery.customer_price else None,
"delivery_status": STATUS_MAPPING.get(delivery.delivery_status, f"Unknown ({delivery.delivery_status})"),
"payment_type": PAYMENT_MAPPING.get(delivery.payment_type, f"Unknown ({delivery.payment_type})"),
"dispatcher_notes": delivery.dispatcher_notes
})
return result

23
routes/info/pricing.py Normal file
View File

@@ -0,0 +1,23 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from database import get_db
from models import Pricing_Oil_Oil
router = APIRouter()
@router.get("/current")
async def get_current_pricing(db: AsyncSession = Depends(get_db)):
# Get the latest pricing record
pricing = await db.execute(
select(Pricing_Oil_Oil).order_by(Pricing_Oil_Oil.id.desc()).limit(1)
)
pricing = pricing.scalar_one_or_none()
if not pricing:
return {"price_per_gallon": 0.00, "message": "No pricing available"}
return {
"price_per_gallon": float(pricing.price_for_customer),
"date": pricing.date.isoformat() if pricing.date else None
}

5
routes/order/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from fastapi import APIRouter
from . import deliveries
router = APIRouter()
router.include_router(deliveries.router, prefix="/deliveries", tags=["deliveries"])

View File

@@ -0,0 +1,95 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from datetime import date, datetime
from database import get_db
from models import Delivery_Delivery, Customer_Customer, Pricing_Oil_Oil
from schemas import DeliveryCreate
from routes.auth.current_user import get_current_user
from models import Account_User
router = APIRouter()
# State mapping for converting integer codes to string abbreviations
state_mapping = {0: "MA", 1: "NH"} # Add more as needed
@router.post("/")
async def create_delivery(
delivery_data: DeliveryCreate,
current_user: Account_User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
if not current_user.user_id:
raise HTTPException(status_code=404, detail="Customer not found")
# Get customer details
customer = await db.execute(
select(Customer_Customer).where(Customer_Customer.id == current_user.user_id)
)
customer = customer.scalar_one_or_none()
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
# Validate gallons_ordered >= 100
if delivery_data.gallons_ordered < 100:
raise HTTPException(status_code=400, detail="Gallons ordered must be at least 100")
# Validate expected_delivery_date >= today
today = date.today()
if delivery_data.expected_delivery_date < today:
raise HTTPException(status_code=400, detail="Expected delivery date cannot be in the past")
# Get latest oil pricing
oil = await db.execute(
select(Pricing_Oil_Oil).order_by(Pricing_Oil_Oil.id.desc()).limit(1)
)
oil = oil.scalar_one_or_none()
if not oil:
raise HTTPException(status_code=500, detail="No oil pricing available")
# Create delivery
new_delivery = Delivery_Delivery(
customer_id=customer.id,
customer_name=f"{customer.customer_first_name} {customer.customer_last_name}",
customer_address=customer.customer_address,
customer_town=customer.customer_town,
customer_state=str(customer.customer_state),
customer_zip=int(customer.customer_zip),
gallons_ordered=delivery_data.gallons_ordered,
gallons_delivered=None,
customer_filled=None,
delivery_status=0, # waiting
when_ordered=today,
when_delivered=None,
expected_delivery_date=delivery_data.expected_delivery_date,
automatic=None,
automatic_id=None,
oil_id=oil.id,
supplier_price=oil.price_for_customer, # assuming this is the price
customer_price=oil.price_for_customer,
dispatcher_notes=delivery_data.dispatcher_notes,
driver_employee_id=None,
driver_first_name=None,
driver_last_name=None,
promo_id=None,
promo_money_discount=None,
payment_type=delivery_data.payment_type,
# Set other fields to None or defaults
customer_asked_for_fill=0,
customer_temperature=None,
prime=delivery_data.prime,
same_day=delivery_data.same_day,
emergency=delivery_data.emergency,
payment_card_id=None,
cash_recieved=None,
pre_charge_amount=delivery_data.pre_charge_amount,
total_price=None,
final_price=None,
check_number=None,
)
db.add(new_delivery)
await db.commit()
await db.refresh(new_delivery)
return {"message": "Delivery created successfully", "delivery_id": new_delivery.id}

View File

@@ -0,0 +1,5 @@
from fastapi import APIRouter
from . import routes
router = APIRouter()
router.include_router(routes.router, tags=["payment"])

1003
routes/payment/routes.py Normal file

File diff suppressed because it is too large Load Diff

278
schemas.py Normal file
View File

@@ -0,0 +1,278 @@
from pydantic import BaseModel, field_validator
from typing import Optional
from datetime import datetime, date
from decimal import Decimal
# Customer schemas
class CustomerBase(BaseModel):
auth_net_profile_id: Optional[str] = None
account_number: str
customer_last_name: str
customer_first_name: str
customer_town: str
customer_state: int
customer_zip: str
customer_first_call: Optional[datetime] = None
customer_email: str
customer_automatic: int
customer_phone_number: str
customer_home_type: int
customer_apt: str
customer_address: str
company_id: int
customer_latitude: str
customer_longitude: str
correct_address: bool
class CustomerCreate(BaseModel):
customer_first_name: str
customer_last_name: str
customer_phone_number: str
customer_email: str
customer_address: str
customer_apt: Optional[str] = None
customer_town: str
customer_zip: str
customer_home_type: int = 0
personal_notes: Optional[str] = None
house_description: Optional[str] = None
class CustomerCreateStep1(BaseModel):
customer_first_name: str
customer_last_name: str
customer_phone_number: str # Now accepts formatted: (123) 456-7890
customer_address: str
customer_apt: Optional[str] = None
customer_town: str
customer_zip: str
customer_home_type: int = 0
house_description: Optional[str] = None
class CustomerAccountCreate(BaseModel):
account_number: str
password: str
confirm_password: str
customer_email: str
class NewCustomerCreate(CustomerCreate):
password: str
confirm_password: str
class CustomerUpdate(BaseModel):
auth_net_profile_id: Optional[str] = None
account_number: Optional[str] = None
customer_last_name: Optional[str] = None
customer_first_name: Optional[str] = None
customer_town: Optional[str] = None
customer_state: Optional[int] = None
customer_zip: Optional[str] = None
customer_first_call: Optional[datetime] = None
customer_email: Optional[str] = None
customer_automatic: Optional[int] = None
customer_phone_number: Optional[str] = None
customer_home_type: Optional[int] = None
customer_apt: Optional[str] = None
customer_address: Optional[str] = None
company_id: Optional[int] = None
customer_latitude: Optional[str] = None
customer_longitude: Optional[str] = None
correct_address: Optional[bool] = None
class CustomerResponse(CustomerBase):
id: int
# Order schemas (Delivery_Delivery)
class OrderBase(BaseModel):
customer_id: int
customer_name: str
customer_address: str
customer_town: str
customer_state: str
customer_zip: int
gallons_ordered: int
customer_asked_for_fill: int
gallons_delivered: Decimal
customer_filled: int
delivery_status: int
when_ordered: Optional[date] = None
when_delivered: Optional[date] = None
expected_delivery_date: Optional[date] = None
automatic: int
automatic_id: int
oil_id: int
supplier_price: Decimal
customer_price: Decimal
customer_temperature: Decimal
dispatcher_notes: str
prime: int
same_day: int
emergency: int
payment_type: int
payment_card_id: int
cash_recieved: Decimal
driver_employee_id: int
driver_first_name: str
driver_last_name: str
pre_charge_amount: Decimal
total_price: Decimal
final_price: Decimal
check_number: str
promo_id: int
promo_money_discount: Decimal
class OrderCreate(OrderBase):
pass
class OrderUpdate(BaseModel):
customer_id: Optional[int] = None
customer_name: Optional[str] = None
customer_address: Optional[str] = None
customer_town: Optional[str] = None
customer_state: Optional[str] = None
customer_zip: Optional[int] = None
gallons_ordered: Optional[int] = None
customer_asked_for_fill: Optional[int] = None
gallons_delivered: Optional[Decimal] = None
customer_filled: Optional[int] = None
delivery_status: Optional[int] = None
when_ordered: Optional[date] = None
when_delivered: Optional[date] = None
expected_delivery_date: Optional[date] = None
automatic: Optional[int] = None
automatic_id: Optional[int] = None
oil_id: Optional[int] = None
supplier_price: Optional[Decimal] = None
customer_price: Optional[Decimal] = None
customer_temperature: Optional[Decimal] = None
dispatcher_notes: Optional[str] = None
prime: Optional[int] = None
same_day: Optional[int] = None
emergency: Optional[int] = None
payment_type: Optional[int] = None
payment_card_id: Optional[int] = None
cash_recieved: Optional[Decimal] = None
driver_employee_id: Optional[int] = None
driver_first_name: Optional[str] = None
driver_last_name: Optional[str] = None
pre_charge_amount: Optional[Decimal] = None
total_price: Optional[Decimal] = None
final_price: Optional[Decimal] = None
check_number: Optional[str] = None
promo_id: Optional[int] = None
promo_money_discount: Optional[Decimal] = None
class OrderResponse(OrderBase):
id: int
class DeliveryCreate(BaseModel):
gallons_ordered: int
expected_delivery_date: date
dispatcher_notes: Optional[str] = None
payment_type: int # 0=cash, 1=credit
prime: Optional[int] = 0
same_day: Optional[int] = 0
emergency: Optional[int] = 0
pre_charge_amount: Optional[float] = None
# Card schemas
class CardBase(BaseModel):
auth_net_payment_profile_id: Optional[str] = None
card_number: Optional[str] = None
last_four_digits: int
name_on_card: Optional[str] = None
expiration_month: str
expiration_year: str
type_of_card: Optional[str] = None
security_number: Optional[str] = None
accepted_or_declined: Optional[int] = None
main_card: Optional[bool] = None
zip_code: Optional[str] = None
class CardCreate(BaseModel):
card_number: str
name_on_card: str
expiration_month: str
expiration_year: str
security_number: str
zip_code: str
save_card: bool = False
class CardResponse(CardBase):
id: int
date_added: datetime
user_id: int
class CardUpdate(BaseModel):
name_on_card: Optional[str] = None
expiration_month: Optional[str] = None
expiration_year: Optional[str] = None
zip_code: Optional[str] = None
# Payment schemas
class PaymentCreate(BaseModel):
amount: Decimal
card_id: Optional[int] = None # For saved cards
card_details: Optional[CardCreate] = None # For new cards
delivery_id: Optional[int] = None
class PaymentResponse(BaseModel):
transaction_id: str
status: str
amount: Decimal
auth_net_transaction_id: str
# Auth schemas
class UserCreate(BaseModel):
account_number: str
house_number: str
email: str
password: str
confirm_password: str
@field_validator('house_number')
@classmethod
def validate_house_number(cls, v: str) -> str:
"""Validate house_number contains only safe characters.
Allows alphanumeric characters, spaces, hyphens, and forward slashes
(for addresses like "123-A" or "45 1/2").
"""
import re
if not v or not v.strip():
raise ValueError('House number cannot be empty')
# Allow alphanumeric, spaces, hyphens, forward slashes
if not re.match(r'^[a-zA-Z0-9\s\-/]+$', v):
raise ValueError('House number contains invalid characters')
if len(v) > 20:
raise ValueError('House number is too long')
return v.strip()
class UserLogin(BaseModel):
email: str
password: str
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None
class UserResponse(BaseModel):
id: int
username: str
email: str
member_since: datetime
last_seen: datetime
admin: int
admin_role: int
confirmed: int
active: int
class ForgotPasswordRequest(BaseModel):
email: str
class ResetPasswordRequest(BaseModel):
token: str
password: str
confirm_password: str

154
settings.py Normal file
View File

@@ -0,0 +1,154 @@
"""
Unified configuration that reads secrets from environment variables.
Non-secret configuration (like CORS origins) can have defaults per environment.
"""
import os
class ApplicationConfig:
"""
Application configuration loaded from environment variables.
All secrets MUST be provided via environment variables.
"""
# Current environment mode
CURRENT_SETTINGS = os.environ.get('MODE', 'DEVELOPMENT')
# ===========================================
# DATABASE CONFIGURATION (Required)
# ===========================================
POSTGRES_USERNAME = os.environ.get('POSTGRES_USERNAME')
POSTGRES_PW = os.environ.get('POSTGRES_PASSWORD')
POSTGRES_SERVER = os.environ.get('POSTGRES_SERVER')
POSTGRES_PORT = os.environ.get('POSTGRES_PORT', '5432')
POSTGRES_DBNAME00 = os.environ.get('POSTGRES_DBNAME')
# ===========================================
# JWT CONFIGURATION (Required)
# ===========================================
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY')
JWT_ALGORITHM = os.environ.get('JWT_ALGORITHM', 'HS256')
# Token expiry in minutes (default: 30 min for prod, can override for dev)
JWT_ACCESS_TOKEN_EXPIRE_MINUTES = int(os.environ.get('JWT_ACCESS_TOKEN_EXPIRE_MINUTES', '30'))
# ===========================================
# AUTHORIZE.NET CONFIGURATION (Required for payments)
# ===========================================
AUTH_NET_API_LOGIN_ID = os.environ.get('AUTH_NET_API_LOGIN_ID')
AUTH_NET_TRANSACTION_KEY = os.environ.get('AUTH_NET_TRANSACTION_KEY')
# ===========================================
# SMTP CONFIGURATION (Required for password reset)
# ===========================================
SMTP_SERVER = os.environ.get('SMTP_SERVER', 'smtp.gmail.com')
SMTP_PORT = int(os.environ.get('SMTP_PORT', '587'))
SMTP_USERNAME = os.environ.get('SMTP_USERNAME')
SMTP_PASSWORD = os.environ.get('SMTP_PASSWORD')
SMTP_FROM_EMAIL = os.environ.get('SMTP_FROM_EMAIL')
# ===========================================
# FRONTEND URL (Required for password reset links)
# ===========================================
FRONTEND_URL = os.environ.get('FRONTEND_URL', 'http://localhost:3000')
# ===========================================
# CORS ORIGINS - Environment specific defaults
# ===========================================
_origins_env = os.environ.get('CORS_ORIGINS', '')
if _origins_env:
# Use comma-separated list from environment
origins = [origin.strip() for origin in _origins_env.split(',') if origin.strip()]
else:
# Default origins based on MODE
_mode = os.environ.get('MODE', 'DEVELOPMENT')
if _mode == 'PRODUCTION':
origins = [
"https://oil.edwineames.com",
"https://apiauto.edwineames.com",
"https://portal.auburnoil.com",
]
elif _mode == 'LOCAL':
origins = [
"http://192.168.1.204:9000",
"http://192.168.1.204:9613",
"http://192.168.1.204:9614",
"http://192.168.1.204:9612",
"http://192.168.1.204:9611",
]
else: # DEVELOPMENT
origins = [
"http://localhost:9000",
"https://localhost:9513",
"http://localhost:9514",
"http://localhost:9512",
"http://localhost:9511",
"http://localhost:5173",
"http://localhost:8000",
]
# Legacy compatibility - build SQLAlchemy URI if all parts provided
if all([POSTGRES_USERNAME, POSTGRES_PW, POSTGRES_SERVER, POSTGRES_DBNAME00]):
SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}:{}/{}".format(
POSTGRES_USERNAME,
POSTGRES_PW,
POSTGRES_SERVER,
POSTGRES_PORT,
POSTGRES_DBNAME00
)
SQLALCHEMY_BINDS = {POSTGRES_DBNAME00: SQLALCHEMY_DATABASE_URI}
else:
SQLALCHEMY_DATABASE_URI = None
SQLALCHEMY_BINDS = {}
@classmethod
def validate_required(cls):
"""Validate that all required environment variables are set."""
required = {
'POSTGRES_USERNAME': cls.POSTGRES_USERNAME,
'POSTGRES_PASSWORD': cls.POSTGRES_PW,
'POSTGRES_SERVER': cls.POSTGRES_SERVER,
'POSTGRES_DBNAME': cls.POSTGRES_DBNAME00,
'JWT_SECRET_KEY': cls.JWT_SECRET_KEY,
}
missing = [key for key, value in required.items() if not value]
if missing:
raise ValueError(
f"Missing required environment variables: {', '.join(missing)}\n"
"Please set these in your .env file or docker-compose environment."
)
# Warn about weak JWT secret in production
if cls.CURRENT_SETTINGS == 'PRODUCTION':
if cls.JWT_SECRET_KEY and len(cls.JWT_SECRET_KEY) < 32:
print("\033[93mWARNING: JWT_SECRET_KEY should be at least 32 characters for production\033[0m")
return True
@classmethod
def validate_payment_config(cls):
"""Validate payment configuration (call before processing payments)."""
if not cls.AUTH_NET_API_LOGIN_ID or not cls.AUTH_NET_TRANSACTION_KEY:
raise ValueError(
"Payment processing requires AUTH_NET_API_LOGIN_ID and AUTH_NET_TRANSACTION_KEY"
)
return True
@classmethod
def validate_email_config(cls):
"""Validate email configuration (call before sending emails)."""
required = {
'SMTP_USERNAME': cls.SMTP_USERNAME,
'SMTP_PASSWORD': cls.SMTP_PASSWORD,
'SMTP_FROM_EMAIL': cls.SMTP_FROM_EMAIL,
}
missing = [key for key, value in required.items() if not value]
if missing:
raise ValueError(
f"Email sending requires: {', '.join(missing)}"
)
return True

34
settings_dev.py Normal file
View File

@@ -0,0 +1,34 @@
class ApplicationConfig:
"""
Basic Configuration for a generic User
"""
CURRENT_SETTINGS = 'DEVELOPMENT'
# databases info
POSTGRES_USERNAME = 'postgres'
POSTGRES_PW = 'password'
POSTGRES_SERVER = '192.168.1.204'
POSTGRES_PORT = '5432'
POSTGRES_DBNAME00 = 'eamco'
SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME,
POSTGRES_PW,
POSTGRES_SERVER,
POSTGRES_DBNAME00
)
SQLALCHEMY_BINDS = {'eamco': SQLALCHEMY_DATABASE_URI}
origins = [
"http://localhost:9000",
"https://localhost:9513",
"http://localhost:9514",
"http://localhost:9512",
"http://localhost:9511",
"http://localhost:5173",
]
FRONTEND_URL = "http://localhost:3000"
# Authorize.net credentials
AUTH_NET_API_LOGIN_ID = '9U6w96gZmX'
AUTH_NET_TRANSACTION_KEY = '94s6Qy458mMNJr7G'

31
settings_local.py Normal file
View File

@@ -0,0 +1,31 @@
class ApplicationConfig:
"""
Basic Configuration for a generic User
"""
CURRENT_SETTINGS = 'LOCAL'
# databases info
POSTGRES_USERNAME = 'postgres'
POSTGRES_PW = 'password'
POSTGRES_SERVER = '192.168.1.204'
POSTGRES_PORT = '5432'
POSTGRES_DBNAME00 = 'auburnoil'
SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME,
POSTGRES_PW,
POSTGRES_SERVER,
POSTGRES_DBNAME00
)
SQLALCHEMY_BINDS = {'auburnoil': SQLALCHEMY_DATABASE_URI}
origins = [
"http://192.168.1.204:9000",
"http://192.168.1.204:9613",
"http://192.168.1.204:9614",
"http://192.168.1.204:9612",
"http://192.168.1.204:9611",
]
FRONTEND_URL = "http://localhost:3000"

28
settings_prod.py Normal file
View File

@@ -0,0 +1,28 @@
class ApplicationConfig:
"""
Basic Configuration for a generic User
"""
CURRENT_SETTINGS = 'PRODUCTION'
# databases info
POSTGRES_USERNAME = 'postgres'
POSTGRES_PW = 'password'
POSTGRES_SERVER = '192.168.1.204'
POSTGRES_PORT = '5432'
POSTGRES_DBNAME00 = 'auburnoil'
SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME,
POSTGRES_PW,
POSTGRES_SERVER,
POSTGRES_DBNAME00
)
SQLALCHEMY_BINDS = {'auburnoil': SQLALCHEMY_DATABASE_URI}
origins = [
"https://oil.edwineames.com",
"https://apiauto.edwineames.com",
]
FRONTEND_URL = "https://portal.auburnoil.com"
# Authorize.net credentials
AUTH_NET_API_LOGIN_ID = '4d2Mn6H23R'
AUTH_NET_TRANSACTION_KEY = '7B94d8xfTQXv37WS'