589 lines
23 KiB
Python
589 lines
23 KiB
Python
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
|