Replace submodules with full folder contents

This commit is contained in:
2025-10-14 17:07:03 +05:30
parent 8805b6146e
commit c24f610178
909 changed files with 116738 additions and 3 deletions

Submodule Machine-Backend deleted from 07b92e34c6

20
Machine-Backend/.env Normal file
View File

@ -0,0 +1,20 @@
SECRET_KEY=9d80e4410a5572c153bc8059e23b7fb6e2eeeb0bf1c4aff851cd60a3220efc6a
JWT_SECRET_KEY=e57e8ed9d7af16276c128027d3c381380f39446fbe16bbd6fa8c73b0f64ac023
PAYU_MERCHANT_KEY=VqvG3m
PAYU_MERCHANT_SALT=DusXSSjqqSMTPSpw32hlFXF6LKY2Zm3y
PAYU_SUCCESS_URL=http://localhost:4200/payment-success
PAYU_FAILURE_URL=http://localhost:4200/payment-failure
FLASK_ENV=production
MYSQL_HOST=db
MYSQL_USER=vendinguser
MYSQL_PASSWORD=vendingpass
MYSQL_DATABASE=vending
SQLITE_DB_PATH=machines
BREVO_SMTP_EMAIL=smukeshsn2000@gmail.com
BREVO_SMTP_KEY=your-brevo-smtp-key
BREVO_SENDER_EMAIL=smukeshsn2000@gmail.com

View File

@ -0,0 +1,23 @@
# Backend Dockerfile
FROM python:3.10-slim
WORKDIR /app
# Install system dependencies (for mysqlclient or pymysql)
RUN apt-get update && apt-get install -y gcc python3-dev default-libmysqlclient-dev && rm -rf /var/lib/apt/lists/*
# Copy dependencies
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Copy project files
COPY . .
# Expose Flask port
EXPOSE 5000
# Environment (set via docker-compose)
ENV FLASK_ENV=production
# Start the Flask app
CMD ["python", "run.py"]

View File

@ -0,0 +1,86 @@
import os
from flask import Flask, send_from_directory
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from dotenv import load_dotenv
load_dotenv()
db = SQLAlchemy()
def create_app():
app = Flask(__name__, instance_relative_config=True)
# CORS Configuration
CORS(app, resources={
r"/*": {
"origins": ["http://localhost:4200", "http://localhost:4300"],
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"allow_headers": ["Content-Type", "Authorization"],
"supports_credentials": True
}
})
# Ensure directories exist
instance_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'instance'))
uploads_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'Uploads'))
for path in [instance_path, uploads_path]:
if not os.path.exists(path):
os.makedirs(path)
app.config['UPLOAD_FOLDER'] = uploads_path
# Database configuration
flask_env = os.getenv('FLASK_ENV', 'development').lower()
if flask_env == 'production':
mysql_host = os.getenv('MYSQL_HOST')
mysql_user = os.getenv('MYSQL_USER')
mysql_password = os.getenv('MYSQL_PASSWORD')
mysql_db = os.getenv('MYSQL_DATABASE')
if not all([mysql_host, mysql_user, mysql_password, mysql_db]):
raise ValueError("Missing required MySQL environment variables")
app.config['SQLALCHEMY_DATABASE_URI'] = (
f'mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:3306/{mysql_db}'
)
else:
sqlite_db_path = os.getenv('SQLITE_DB_PATH', 'machines.db')
db_path = os.path.join(instance_path, sqlite_db_path)
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = os.getenv('SQLALCHEMY_ECHO', 'False').lower() == 'true'
# Initialize extensions
db.init_app(app)
# Import models
from app.models.models import Machine, User, Product, VendingSlot, Transaction
# Create tables
with app.app_context():
db.create_all()
print(f"✓ Database initialized ({flask_env})")
# FIXED: Serial service connection BEFORE return
from app.services.services import serial_service
try:
connected = serial_service.connect_to_machine('/dev/ttyUSB0', 9600)
if connected:
print("✓ Vending machine connected successfully")
else:
print("✗ Failed to connect to vending machine")
except Exception as e:
print(f"⚠ Serial connection error: {e}")
# Register blueprints
from app.routes.routes import bp
app.register_blueprint(bp, url_prefix='/')
@app.route('/uploads/<filename>')
def uploaded_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
return app

View File

@ -0,0 +1,12 @@
{
"web": {
"issuer": "http://localhost:8080/realms/myrealm",
"auth_uri": "http://localhost:8080/realms/myrealm/protocol/openid-connect/auth",
"client_id": "my-flask-app",
"client_secret": "7kAD4BvMWbE7TP5f79sVX4NzzvsGyrFd",
"redirect_uris": ["http://localhost:5000/oidc/callback"],
"userinfo_uri": "http://localhost:8080/realms/myrealm/protocol/openid-connect/userinfo",
"token_uri": "http://localhost:8080/realms/myrealm/protocol/openid-connect/token",
"token_introspection_uri": "http://localhost:8080/realms/myrealm/protocol/openid-connect/token/introspect"
}
}

Binary file not shown.

View File

@ -0,0 +1 @@
from .models import Machine, User, Product, VendingSlot

View File

@ -0,0 +1,213 @@
from app import db
import datetime
import time
import json
from werkzeug.security import generate_password_hash, check_password_hash
# Machine Model
class Machine(db.Model):
__tablename__ = 'machines'
id = db.Column(db.Integer, primary_key=True)
machine_id = db.Column(db.String(10), unique=True, nullable=False)
machine_model = db.Column(db.String(100), nullable=False)
machine_type = db.Column(db.String(100), nullable=False)
client_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
client_name = db.Column(db.String(100), nullable=True)
branch_id = db.Column(db.String(10), nullable=False)
branch_name = db.Column(db.String(100), nullable=False)
operation_status = db.Column(db.String(50), nullable=False)
connection_status = db.Column(db.String(50), nullable=False)
created_on = db.Column(db.String(20), nullable=False)
password = db.Column(db.String(128), nullable=False)
slots = db.relationship('VendingSlot', backref='machine', lazy=True)
client = db.relationship('User', backref='machines')
def to_dict(self):
return {
'id': self.id,
'machine_id': self.machine_id,
'machine_model': self.machine_model,
'machine_type': self.machine_type,
'client_id': self.client_id,
'client_name': self.client.username if self.client else self.client_name,
'branch_id': self.branch_id,
'branch_name': self.branch_name,
'operation_status': self.operation_status,
'connection_status': self.connection_status,
'created_on': self.created_on,
'password': self.password
}
def set_password(self, password):
self.password = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password, password)
# User Model - UPDATED with proper password hashing
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.String(50), unique=True, nullable=False)
username = db.Column(db.String(50), nullable=False)
email = db.Column(db.String(100), nullable=False, unique=True)
contact = db.Column(db.String(20), nullable=False)
roles = db.Column(db.String(50), nullable=False)
user_status = db.Column(db.String(50), nullable=False)
password = db.Column(db.String(255), nullable=False) # Increased length for hash
# File storage fields
photo = db.Column(db.String(255), nullable=True)
company_logo = db.Column(db.String(255), nullable=True)
documents = db.Column(db.Text, nullable=True)
# Timestamps
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
def to_dict(self):
"""Convert user object to dictionary"""
return {
'id': self.id,
'user_id': self.user_id,
'username': self.username,
'email': self.email,
'contact': self.contact,
'roles': self.roles,
'user_status': self.user_status,
'photo': self.photo,
'company_logo': self.company_logo,
'documents': json.loads(self.documents) if self.documents else [],
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None,
'machines': [m.to_dict() for m in self.machines] if hasattr(self, 'machines') else []
}
def set_password(self, password):
"""Hash and set the password"""
self.password = generate_password_hash(password, method='pbkdf2:sha256')
print(f"Password hashed for user {self.username}")
def check_password(self, password):
"""Verify password against hash"""
result = check_password_hash(self.password, password)
print(f"Password check for {self.username}: {result}")
return result
@staticmethod
def generate_user_id(username, email):
"""Generate unique user ID from username and email"""
username_part = ''.join(filter(str.isalnum, username[:4])).upper()
email_part = ''.join(filter(str.isalnum, email.split('@')[0][:4])).upper()
timestamp = str(int(time.time()))[-4:]
user_id = f"USR-{username_part}{email_part}{timestamp}"
# Ensure uniqueness
counter = 1
original_id = user_id
while User.query.filter_by(user_id=user_id).first():
user_id = f"{original_id}{counter}"
counter += 1
return user_id
# Product Model
class Product(db.Model):
__tablename__ = 'products'
id = db.Column(db.Integer, primary_key=True)
product_id = db.Column(db.String(10), unique=True, nullable=False)
product_name = db.Column(db.String(100), nullable=False)
price = db.Column(db.Float, nullable=False)
product_image = db.Column(db.String(255), nullable=False)
created_date = db.Column(db.String(20), nullable=False)
def to_dict(self):
return {
'id': self.id,
'product_id': self.product_id,
'product_name': self.product_name,
'price': str(self.price),
'product_image': self.product_image,
'created_date': self.created_date
}
# VendingSlot Model
class VendingSlot(db.Model):
__tablename__ = 'vending_slots'
id = db.Column(db.Integer, primary_key=True)
machine_id = db.Column(db.String(10), db.ForeignKey('machines.machine_id'), nullable=False)
row_id = db.Column(db.String(1), nullable=False)
slot_name = db.Column(db.String(3), nullable=False)
enabled = db.Column(db.Boolean, default=True)
product_id = db.Column(db.String(10), db.ForeignKey('products.product_id'), nullable=True)
units = db.Column(db.Integer, default=0)
price = db.Column(db.String(10), default="N/A")
def to_dict(self):
return {
'name': self.slot_name,
'enabled': self.enabled,
'productId': self.product_id,
'units': self.units,
'price': self.price
}
# Transaction Model
class Transaction(db.Model):
__tablename__ = 'transactions'
id = db.Column(db.Integer, primary_key=True)
transaction_id = db.Column(db.String(50), unique=True, nullable=False)
machine_id = db.Column(db.String(10), db.ForeignKey('machines.machine_id'), nullable=False)
product_name = db.Column(db.String(100), nullable=False)
quantity = db.Column(db.Integer, nullable=False)
amount = db.Column(db.Float, nullable=False)
payment_type = db.Column(db.String(50), nullable=False) # wallet/qr/card
status = db.Column(db.String(50), nullable=False) # Success/Dispense failed/Unprocessed
amount_receiving_status = db.Column(db.String(50), nullable=True) # Dispense failed refund
return_amount = db.Column(db.Float, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
machine = db.relationship('Machine', backref='transactions')
def to_dict(self):
return {
'id': self.id,
'transaction_id': self.transaction_id,
'machine_id': self.machine_id,
'product_name': self.product_name,
'quantity': self.quantity,
'amount': self.amount,
'payment_type': self.payment_type,
'status': self.status,
'amount_receiving_status': self.amount_receiving_status,
'return_amount': self.return_amount,
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None
}
# Add to your models.py
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True, nullable=False)
description = db.Column(db.String(255))
permissions = db.Column(db.Text) # JSON string of permission IDs
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'description': self.description,
'permissions': json.loads(self.permissions) if self.permissions else [],
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None
}

View File

@ -0,0 +1 @@
from .routes import bp

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
from .services import MachineService, UserService, ProductService

View File

@ -0,0 +1,967 @@
import uuid
import datetime
import datetime
import re
import os
import json
import serial
import time
import threading
from typing import Optional, Dict, Any
import smtplib
from email.mime.text import MIMEText
from app import db
from app.models.models import Machine, User, Product, VendingSlot, Transaction, Role
from flask import current_app
import secrets
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
class SerialCommunicationService:
def __init__(self):
self.serial_port: Optional[serial.Serial] = None
self.is_connected = False
self.response_buffer = ""
self.last_response = None
self.response_lock = threading.Lock()
def connect_to_machine(self, port: str = '/dev/ttyUSB0', baudrate: int = 9600) -> bool:
"""
Connect to vending machine via serial port
Common ports:
- Linux: /dev/ttyUSB0, /dev/ttyACM0
- Windows: COM3, COM4, etc.
- macOS: /dev/cu.usbserial-*
"""
try:
self.serial_port = serial.Serial(
port=port,
baudrate=baudrate,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1 # 1 second timeout for reads
)
# Test connection with a status command
time.sleep(2) # Allow time for Arduino to reset
self.serial_port.write(b'STATUS\n')
time.sleep(1)
if self.serial_port.in_waiting > 0:
response = self.serial_port.readline().decode('utf-8').strip()
print(f"Machine connected successfully. Status: {response}")
self.is_connected = True
return True
else:
print("No response from machine")
self.serial_port.close()
return False
except Exception as e:
print(f"Failed to connect to machine: {e}")
self.is_connected = False
return False
def send_dispense_command(self, slot_id: str, timeout: int = 10) -> Dict[str, Any]:
"""
Send dispense command and wait for drop sensor response
"""
if not self.is_connected or not self.serial_port:
return {
'success': False,
'error': 'Not connected to machine',
'response': 'NO_CONNECTION'
}
try:
# Clear any pending data
self.serial_port.reset_input_buffer()
# Send dispense command
command = f"DISPENSE:{slot_id}\n"
self.serial_port.write(command.encode('utf-8'))
print(f"Sent command: {command.strip()}")
# Wait for response with timeout
start_time = time.time()
response = ""
while time.time() - start_time < timeout:
if self.serial_port.in_waiting > 0:
line = self.serial_port.readline().decode('utf-8').strip()
print(f"Received: {line}")
if line in ['PRODUCT_DISPENSED', 'DISPENSE_FAILED', 'SLOT_EMPTY', 'MOTOR_ERROR']:
response = line
break
time.sleep(0.1) # Small delay to prevent busy waiting
if not response:
return {
'success': False,
'error': 'Timeout waiting for drop sensor',
'response': 'TIMEOUT'
}
success = response == 'PRODUCT_DISPENSED'
return {
'success': success,
'response': response,
'slot_id': slot_id,
'message': self._get_response_message(response, slot_id)
}
except Exception as e:
print(f"Error during dispensing: {e}")
return {
'success': False,
'error': str(e),
'response': 'COMMUNICATION_ERROR'
}
def _get_response_message(self, response: str, slot_id: str) -> str:
"""Get user-friendly message for response codes"""
messages = {
'PRODUCT_DISPENSED': f'Product successfully dispensed from slot {slot_id}',
'DISPENSE_FAILED': f'Dispensing failed - no product detected from slot {slot_id}',
'SLOT_EMPTY': f'Slot {slot_id} is empty',
'MOTOR_ERROR': f'Motor malfunction in slot {slot_id}',
'TIMEOUT': f'Timeout - no response from slot {slot_id}',
'COMMUNICATION_ERROR': 'Communication error with machine'
}
return messages.get(response, f'Unknown response: {response}')
def check_machine_status(self) -> Dict[str, Any]:
"""Check overall machine status"""
if not self.is_connected or not self.serial_port:
return {'status': 'disconnected', 'message': 'Not connected to machine'}
try:
self.serial_port.reset_input_buffer()
self.serial_port.write(b'STATUS\n')
time.sleep(1)
if self.serial_port.in_waiting > 0:
response = self.serial_port.readline().decode('utf-8').strip()
return {
'status': 'connected',
'response': response,
'message': 'Machine is responding'
}
else:
return {
'status': 'no_response',
'message': 'Machine not responding'
}
except Exception as e:
return {
'status': 'error',
'error': str(e),
'message': 'Communication error'
}
def reset_machine(self) -> Dict[str, Any]:
"""Send reset command to clear any error states"""
if not self.is_connected or not self.serial_port:
return {'success': False, 'error': 'Not connected to machine'}
try:
self.serial_port.write(b'RESET\n')
time.sleep(2)
response = self.serial_port.readline().decode('utf-8').strip()
return {
'success': True,
'response': response,
'message': 'Reset command sent'
}
except Exception as e:
return {'success': False, 'error': str(e)}
def disconnect(self):
"""Disconnect from serial port"""
if self.serial_port and self.serial_port.is_open:
self.serial_port.close()
self.is_connected = False
print("Disconnected from vending machine")
# Global instance
serial_service = SerialCommunicationService()
# Machine Service (UNCHANGED)
class MachineService:
@staticmethod
def generate_id(prefix):
return f"{prefix}{uuid.uuid4().hex[:8].upper()}"
@staticmethod
def generate_unique_password():
while True:
password = secrets.token_hex(4).upper() # e.g., "A1B2C3D4"
if not Machine.query.filter_by(password=password).first():
return password
@staticmethod
def validate_machine_data(data):
required_fields = ['machine_model', 'machine_type', 'client_name', 'branch_name', 'operation_status', 'connection_status']
for field in required_fields:
if field not in data:
raise ValueError(f"Missing required field: {field}")
status_mapping = {
'connected': 'connected',
'disconnected': 'disconnected',
'maintenance':'maintenance',
'active': 'active',
'inactive': 'inactive'
}
if data['operation_status'] not in status_mapping:
raise ValueError(f"Invalid operation status. Allowed statuses: {list(status_mapping.keys())}")
data['operation_status'] = status_mapping[data['operation_status']]
if data['connection_status'] not in status_mapping:
raise ValueError(f"Invalid connection status. Allowed statuses: {list(status_mapping.keys())}")
data['connection_status'] = status_mapping[data['connection_status']]
@staticmethod
def get_all_machines():
return Machine.query.all()
@staticmethod
def get_machine_slots(machine_id: str):
machine = Machine.query.filter_by(machine_id=machine_id).first()
if not machine:
return None
slots = VendingSlot.query.filter_by(machine_id=machine_id).all()
product_ids = [slot.product_id for slot in slots if slot.product_id]
products = {p.product_id: p for p in Product.query.filter(Product.product_id.in_(product_ids)).all()} if product_ids else {}
return [{
'slot_name': slot.row_id,
'column': int(slot.slot_name[1:]) if slot.slot_name[1:].isdigit() else 1,
'enabled': slot.enabled,
'product_id': slot.product_id,
'units': slot.units or 0,
'price': slot.price or '0.0',
'product_name': products.get(slot.product_id, {'product_name': 'N/A'}).product_name if slot.product_id else 'N/A',
'product_image': products.get(slot.product_id, {'product_image': None}).product_image if slot.product_id else None
} for slot in slots]
@staticmethod
def create_machine(data):
# Modified validation to accept client_id instead of client_name
required_fields = ['machine_model', 'machine_type', 'client_id', 'branch_name', 'operation_status', 'connection_status']
for field in required_fields:
if field not in data:
raise ValueError(f"Missing required field: {field}")
status_mapping = {
'connected': 'connected',
'disconnected': 'disconnected',
'maintenance':'maintenance',
'active': 'active',
'inactive': 'inactive'
}
if data['operation_status'] not in status_mapping:
raise ValueError(f"Invalid operation status. Allowed statuses: {list(status_mapping.keys())}")
data['operation_status'] = status_mapping[data['operation_status']]
if data['connection_status'] not in status_mapping:
raise ValueError(f"Invalid connection status. Allowed statuses: {list(status_mapping.keys())}")
data['connection_status'] = status_mapping[data['connection_status']]
machine_id = MachineService.generate_id("M")
branch_id = MachineService.generate_id("B")
created_on = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
password = MachineService.generate_unique_password()
# Validate client_id exists in users
client_id = data.get('client_id')
if not client_id or not User.query.get(client_id):
raise ValueError("Invalid or missing client_id")
# Get client_name from user
user = User.query.get(client_id)
client_name = user.username
new_machine = Machine(
machine_id=machine_id,
machine_model=data['machine_model'],
machine_type=data['machine_type'],
client_id=client_id,
client_name=client_name, # Set client_name from user record
branch_id=branch_id,
branch_name=data['branch_name'],
operation_status=data['operation_status'],
connection_status=data['connection_status'],
created_on=created_on,
password=password
)
db.session.add(new_machine)
db.session.commit()
return new_machine
@staticmethod
def update_machine(id, data):
machine = Machine.query.get_or_404(id)
if any(field in data for field in ['machine_model', 'machine_type', 'branch_name', 'operation_status', 'connection_status']):
MachineService.validate_machine_data({k: data.get(k, getattr(machine, k)) for k in ['machine_model', 'machine_type', 'branch_name', 'operation_status', 'connection_status']})
machine.machine_model = data.get('machine_model', machine.machine_model)
machine.machine_type = data.get('machine_type', machine.machine_type)
if 'client_id' in data:
client_id = data['client_id']
if not User.query.get(client_id):
raise ValueError("Invalid client_id")
machine.client_id = client_id
machine.branch_name = data.get('branch_name', machine.branch_name)
machine.operation_status = data.get('operation_status', machine.operation_status)
machine.connection_status = data.get('connection_status', machine.connection_status)
db.session.commit()
return machine
@staticmethod
def delete_machine(id):
machine = Machine.query.get_or_404(id)
db.session.delete(machine)
db.session.commit()
return True
@staticmethod
def get_machine_by_id(id):
return Machine.query.get_or_404(id)
@staticmethod
def get_vending_state(machine_id):
machine = Machine.query.filter_by(machine_id=machine_id).first_or_404()
slots = VendingSlot.query.filter_by(machine_id=machine_id).all()
vending_rows = {}
for slot in slots:
if slot.row_id not in vending_rows:
vending_rows[slot.row_id] = []
vending_rows[slot.row_id].append(slot.to_dict())
return {
'vendingRows': [{'rowId': row_id, 'slots': slots} for row_id, slots in vending_rows.items()],
'lastUpdated': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
@staticmethod
def update_vending_state(machine_id, vending_rows):
machine = Machine.query.filter_by(machine_id=machine_id).first_or_404()
VendingSlot.query.filter_by(machine_id=machine_id).delete()
for row in vending_rows:
row_id = row['rowId']
for slot_data in row['slots']:
new_slot = VendingSlot(
machine_id=machine_id,
row_id=row_id,
slot_name=slot_data['name'],
enabled=slot_data['enabled'],
product_id=slot_data['productId'],
units=slot_data['units'],
price=slot_data['price']
)
db.session.add(new_slot)
db.session.commit()
return {'success': True, 'message': 'Vending state updated', 'lastUpdated': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
# User Service (MODIFIED FOR BREVO SMTP WITH ENV VARIABLES)
# Updated UserService in services.py
class UserService:
@staticmethod
def generate_unique_password():
"""Generate a secure random password"""
return secrets.token_hex(4).upper() # 8 character hex password
@staticmethod
def save_file(file, folder='user_uploads'):
"""Save uploaded file and return path"""
if not file:
return None
filename = f"{uuid.uuid4().hex}_{file.filename}"
upload_folder = os.path.join(current_app.config['UPLOAD_FOLDER'], folder)
os.makedirs(upload_folder, exist_ok=True)
file_path = os.path.join(upload_folder, filename)
file.save(file_path)
return f"/uploads/{folder}/{filename}"
@staticmethod
def save_multiple_documents(files, metadata_json=None):
"""Save multiple document files with metadata"""
document_paths = []
metadata_list = []
if metadata_json:
try:
metadata_list = json.loads(metadata_json)
print(f"Parsed metadata: {metadata_list}")
except Exception as e:
print(f"Error parsing metadata: {e}")
metadata_list = []
if not isinstance(files, list):
files = [files]
for idx, file in enumerate(files):
if file and hasattr(file, 'filename'):
try:
path = UserService.save_file(file, 'user_documents')
if path:
metadata = metadata_list[idx] if idx < len(metadata_list) else {}
document_paths.append({
'filename': file.filename,
'path': path,
'uploaded_at': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
'document_type': metadata.get('document_type', 'other'),
'document_type_other': metadata.get('document_type_other', '')
})
print(f"Saved document: {file.filename}")
except Exception as e:
print(f"Error saving file {file.filename}: {e}")
continue
return document_paths
@staticmethod
def send_email_notification(username, email, contact, password, user_id):
"""Send email notification with user credentials"""
subject = "Your Account Credentials"
body = f"""
Dear {username},
Your account has been created successfully!
Login Credentials:
==================
User ID: {user_id}
Username: {username}
Email: {email}
Contact: {contact}
Password: {password}
Important Security Notice:
- Please change your password after first login
- Keep this information secure and confidential
- Do not share your credentials with anyone
You can login using either your email or username along with your password.
Best regards,
System Administration Team
"""
msg = MIMEText(body)
msg['Subject'] = subject
msg['From'] = os.getenv('BREVO_SENDER_EMAIL')
msg['To'] = email
smtp_email = os.getenv('BREVO_SMTP_EMAIL')
smtp_key = os.getenv('BREVO_SMTP_KEY')
if not all([smtp_email, smtp_key, msg['From']]):
print("Error: Brevo SMTP configuration missing in .env")
return False
try:
with smtplib.SMTP('smtp-relay.brevo.com', 587) as server:
server.starttls()
server.login(smtp_email, smtp_key)
server.send_message(msg)
print(f"✓ Email sent successfully to {email}")
return True
except Exception as e:
print(f"✗ Failed to send email to {email}: {str(e)}")
return False
@staticmethod
def log_sms_notification(contact, username, email, password, user_id):
"""Log SMS notification (for future SMS integration)"""
notification = {
"timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"user_id": user_id,
"username": username,
"email": email,
"contact": contact,
"password": password,
"message": f"Your account credentials: User ID: {user_id}, Username: {username}, Email: {email}, Password: {password}"
}
notifications_file = os.path.join(current_app.root_path, "sms_notifications.json")
try:
notifications = []
if os.path.exists(notifications_file):
with open(notifications_file, "r") as f:
notifications = json.load(f)
notifications.append(notification)
with open(notifications_file, "w") as f:
json.dump(notifications, f, indent=4)
print(f"✓ SMS notification logged for {contact}")
return True
except Exception as e:
print(f"✗ Failed to log SMS notification: {str(e)}")
return False
@staticmethod
def validate_user_data(data):
"""Validate user data before creating/updating"""
import re
required_fields = ['username', 'email', 'contact', 'roles', 'user_status']
for field in required_fields:
if field not in data:
raise ValueError(f"Missing required field: {field}")
# Email validation - FIXED: Added closing quote
email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(email_regex, data['email']):
raise ValueError("Invalid email format")
@staticmethod
def get_all_users():
"""Get all users from database"""
return User.query.all()
@staticmethod
def create_user(data, photo_file=None, logo_file=None, document_files=None, documents_metadata=None):
"""
Create new user with proper password hashing
Password must be provided in data
"""
print(f"\n{'='*60}")
print(f"CREATING NEW USER")
print(f"{'='*60}")
# Validate user data
UserService.validate_user_data(data)
# CHECK: Password must be provided
if 'password' not in data or not data['password']:
raise ValueError("Password is required")
# Generate user ID only
user_id = User.generate_user_id(data['username'], data['email'])
password = data['password'] # Use provided password, not auto-generated
print(f"Generated User ID: {user_id}")
print(f"Password provided by admin")
# Handle file uploads
photo_path = UserService.save_file(photo_file, 'user_photos') if photo_file else None
logo_path = UserService.save_file(logo_file, 'company_logos') if logo_file else None
documents = UserService.save_multiple_documents(document_files, documents_metadata) if document_files else []
# Create new user
new_user = User(
user_id=user_id,
username=data['username'],
email=data['email'],
contact=data['contact'],
roles=data['roles'],
user_status=data['user_status'],
photo=photo_path,
company_logo=logo_path,
documents=json.dumps(documents)
)
# Set password (this will hash it automatically)
new_user.set_password(password)
print(f"Password hashed and set for user")
# Save to database
db.session.add(new_user)
db.session.commit()
print(f"✓ User created successfully with ID: {new_user.id}")
print(f"{'='*60}\n")
# Optional: Send email notification with the password they set
# UserService.send_email_notification(...)
return new_user
@staticmethod
def update_user(id, data, photo_file=None, logo_file=None, document_files=None, documents_metadata=None):
"""
Update existing user
Password is only updated if explicitly provided in data
"""
user = User.query.get_or_404(id)
print(f"\n{'='*60}")
print(f"UPDATING USER: {user.username}")
print(f"{'='*60}")
# Validate only if fields are being updated
if any(field in data for field in ['username', 'email', 'contact', 'roles', 'user_status']):
UserService.validate_user_data({
'username': data.get('username', user.username),
'email': data.get('email', user.email),
'contact': data.get('contact', user.contact),
'roles': data.get('roles', user.roles),
'user_status': data.get('user_status', user.user_status)
})
# Update basic fields
user.username = data.get('username', user.username)
user.email = data.get('email', user.email)
user.contact = data.get('contact', user.contact)
user.roles = data.get('roles', user.roles)
user.user_status = data.get('user_status', user.user_status)
# Update password if provided (will be hashed automatically)
if 'password' in data and data['password']:
print(f"Updating password for user")
user.set_password(data['password'])
# Handle file updates
if photo_file:
if user.photo:
old_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
user.photo.replace('/uploads/', ''))
if os.path.exists(old_path):
os.remove(old_path)
user.photo = UserService.save_file(photo_file, 'user_photos')
if logo_file:
if user.company_logo:
old_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
user.company_logo.replace('/uploads/', ''))
if os.path.exists(old_path):
os.remove(old_path)
user.company_logo = UserService.save_file(logo_file, 'company_logos')
if document_files:
existing_docs = json.loads(user.documents) if user.documents else []
new_docs = UserService.save_multiple_documents(document_files, documents_metadata)
existing_docs.extend(new_docs)
user.documents = json.dumps(existing_docs)
user.updated_at = datetime.datetime.utcnow()
db.session.commit()
print(f"✓ User updated successfully")
print(f"{'='*60}\n")
return user
@staticmethod
def delete_user_document(user_id, document_path):
"""Delete a specific document from a user"""
user = User.query.get_or_404(user_id)
if user.documents:
documents = json.loads(user.documents)
documents = [doc for doc in documents if doc['path'] != document_path]
user.documents = json.dumps(documents)
# Delete physical file
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
document_path.replace('/uploads/', ''))
if os.path.exists(file_path):
os.remove(file_path)
db.session.commit()
return True
return False
@staticmethod
def delete_user(id):
"""Delete user and all associated files"""
user = User.query.get_or_404(id)
# Delete all associated files
if user.photo:
photo_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
user.photo.replace('/uploads/', ''))
if os.path.exists(photo_path):
os.remove(photo_path)
if user.company_logo:
logo_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
user.company_logo.replace('/uploads/', ''))
if os.path.exists(logo_path):
os.remove(logo_path)
if user.documents:
documents = json.loads(user.documents)
for doc in documents:
doc_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
doc['path'].replace('/uploads/', ''))
if os.path.exists(doc_path):
os.remove(doc_path)
db.session.delete(user)
db.session.commit()
return True
@staticmethod
def validate_file(file, maxSizeMB=10):
"""Validate file size"""
max_size_bytes = maxSizeMB * 1024 * 1024
if file.size > max_size_bytes:
return f"File size exceeds {maxSizeMB}MB limit"
return None
@staticmethod
def is_valid_image_type(file):
"""Check if file is valid image type"""
valid_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']
return valid_types.includes(file.type) if hasattr(file, 'type') else False
@staticmethod
def is_valid_document_type(file):
"""Check if file is valid document type"""
valid_types = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'image/jpeg',
'image/jpg',
'image/png'
]
return valid_types.includes(file.type) if hasattr(file, 'type') else False
# Product Service (UNCHANGED)
class ProductService:
@staticmethod
def generate_id(prefix):
return f"{prefix}{uuid.uuid4().hex[:4].upper()}"
@staticmethod
def validate_product_data(data):
required_fields = ['product_name', 'price']
for field in required_fields:
if field not in data or not data[field]:
raise ValueError(f"Missing or empty required field: {field}")
try:
price = float(data['price'])
if price < 0:
raise ValueError("Price must be a positive number")
except (ValueError, TypeError):
raise ValueError("Price must be a valid number")
@staticmethod
def save_image(file):
if not file:
raise ValueError("Product image is required")
filename = f"{uuid.uuid4().hex}_{file.filename}"
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
file.save(file_path)
return f"/uploads/{filename}"
@staticmethod
def get_all_products():
return Product.query.all()
@staticmethod
def create_product(data, file):
ProductService.validate_product_data(data)
image_path = ProductService.save_image(file)
product_id = ProductService.generate_id("P")
created_date = data.get('created_date', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
new_product = Product(
product_id=product_id,
product_name=data['product_name'],
price=float(data['price']),
product_image=image_path,
created_date=created_date
)
db.session.add(new_product)
db.session.commit()
return new_product
@staticmethod
def update_product(id, data, file):
product = Product.query.get_or_404(id)
ProductService.validate_product_data(data)
product.product_name = data.get('product_name', product.product_name)
product.price = float(data.get('price', product.price))
product.created_date = data.get('created_date', product.created_date)
if file:
if product.product_image:
old_image_path = os.path.join(current_app.config['UPLOAD_FOLDER'], product.product_image.split('/')[-1])
if os.path.exists(old_image_path):
os.remove(old_image_path)
product.product_image = ProductService.save_image(file)
db.session.commit()
return product
@staticmethod
def delete_product(id):
product = Product.query.get_or_404(id)
if product.product_image:
image_path = os.path.join(current_app.config['UPLOAD_FOLDER'], product.product_image.split('/')[-1])
if os.path.exists(image_path):
os.remove(image_path)
db.session.delete(product)
db.session.commit()
return True
class TransactionService:
@staticmethod
def create_transaction(machine_id, product_name, quantity, amount, payment_type, status='Success'):
"""Create a new transaction record"""
transaction_id = f"TXN-{uuid.uuid4().hex[:8].upper()}"
new_transaction = Transaction(
transaction_id=transaction_id,
machine_id=machine_id,
product_name=product_name,
quantity=quantity,
amount=amount,
payment_type=payment_type,
status=status
)
db.session.add(new_transaction)
db.session.commit()
return new_transaction
@staticmethod
def update_transaction_status(transaction_id, status, amount_receiving_status=None, return_amount=None):
"""Update transaction status after dispensing"""
transaction = Transaction.query.filter_by(transaction_id=transaction_id).first()
if transaction:
transaction.status = status
if amount_receiving_status:
transaction.amount_receiving_status = amount_receiving_status
if return_amount:
transaction.return_amount = return_amount
db.session.commit()
return transaction
@staticmethod
def get_all_transactions(filters=None):
"""Get all transactions with optional filters"""
query = Transaction.query
if filters:
if 'machine_id' in filters:
query = query.filter_by(machine_id=filters['machine_id'])
if 'status' in filters:
query = query.filter_by(status=filters['status'])
if 'date_from' in filters:
query = query.filter(Transaction.created_at >= filters['date_from'])
if 'date_to' in filters:
query = query.filter(Transaction.created_at <= filters['date_to'])
return query.order_by(Transaction.created_at.desc()).all()
@staticmethod
def get_transaction_by_id(transaction_id):
"""Get single transaction by ID"""
return Transaction.query.filter_by(transaction_id=transaction_id).first()
class RoleService:
@staticmethod
def get_all_roles():
"""Get all roles"""
return Role.query.all()
@staticmethod
def get_role_by_id(role_id):
"""Get role by ID"""
return Role.query.get_or_404(role_id)
@staticmethod
def create_role(data):
"""Create new role"""
name = data.get('name')
description = data.get('description', '')
permissions = data.get('permissions', [])
# Check if role already exists
existing_role = Role.query.filter_by(name=name).first()
if existing_role:
raise ValueError(f"Role '{name}' already exists")
new_role = Role(
name=name,
description=description,
permissions=json.dumps(permissions)
)
db.session.add(new_role)
db.session.commit()
return new_role
@staticmethod
def update_role(role_id, data):
"""Update existing role"""
role = Role.query.get_or_404(role_id)
if 'name' in data:
# Check if new name conflicts with existing role
existing = Role.query.filter(Role.name == data['name'], Role.id != role_id).first()
if existing:
raise ValueError(f"Role '{data['name']}' already exists")
role.name = data['name']
if 'description' in data:
role.description = data['description']
if 'permissions' in data:
role.permissions = json.dumps(data['permissions'])
role.updated_at = datetime.datetime.utcnow()
db.session.commit()
return role
@staticmethod
def delete_role(role_id):
"""Delete role"""
role = Role.query.get_or_404(role_id)
# Check if any users have this role
users_with_role = User.query.filter_by(roles=role.name).count()
if users_with_role > 0:
raise ValueError(f"Cannot delete role '{role.name}' - {users_with_role} user(s) still assigned to it")
db.session.delete(role)
db.session.commit()
return True

View File

@ -0,0 +1,160 @@
[
{
"timestamp": "2025-05-05 15:18:35",
"username": "Mukesh",
"email": "smukesh1304@gmail.com",
"contact": "9080063331",
"password": "B4DC37B8",
"message": "Your account credentials: Username: Mukesh, Email: smukesh1304@gmail.com, Password: B4DC37B8"
},
{
"timestamp": "2025-05-05 18:31:21",
"username": "Gokul",
"email": "fate.altre@gmail.com",
"contact": "9080063331",
"password": "7CB34107",
"message": "Your account credentials: Username: Gokul, Email: fate.altre@gmail.com, Password: 7CB34107"
},
{
"timestamp": "2025-05-05 20:53:59",
"username": "Mukesh",
"email": "smukesh1304@gmail.com",
"contact": "9080063331",
"password": "0A96ABB6",
"message": "Your account credentials: Username: Mukesh, Email: smukesh1304@gmail.com, Password: 0A96ABB6"
},
{
"timestamp": "2025-05-05 20:54:21",
"username": "Joe Done",
"email": "fate.altre@gmail.com",
"contact": "9080063331",
"password": "1385EE14",
"message": "Your account credentials: Username: Joe Done, Email: fate.altre@gmail.com, Password: 1385EE14"
},
{
"timestamp": "2025-05-05 21:26:23",
"username": "Mukesh",
"email": "smukesh1304@gmail.com",
"contact": "9080063331",
"password": "A5FAED83",
"message": "Your account credentials: Username: Mukesh, Email: smukesh1304@gmail.com, Password: A5FAED83"
},
{
"timestamp": "2025-07-24 12:51:15",
"username": "admin",
"email": "fate.altre@gmail.com",
"contact": "9080063331",
"password": "D1FCD9E5",
"message": "Your account credentials: Username: admin, Email: fate.altre@gmail.com, Password: D1FCD9E5"
},
{
"timestamp": "2025-07-24 13:19:32",
"username": "testuser",
"email": "test@example.com",
"contact": "1234567890",
"password": "67CC0B4F",
"message": "Your account credentials: Username: testuser, Email: test@example.com, Password: 67CC0B4F"
},
{
"timestamp": "2025-07-24 14:40:29",
"username": "mukesh",
"email": "smukesh1304@gmail.com",
"contact": "9080063331",
"password": "5A353979",
"message": "Your account credentials: Username: mukesh, Email: smukesh1304@gmail.com, Password: 5A353979"
},
{
"timestamp": "2025-07-24 14:46:06",
"username": "mukesh",
"email": "smukesh1304@gmail.com",
"contact": "9080063331",
"password": "0C42E059",
"message": "Your account credentials: Username: mukesh, Email: smukesh1304@gmail.com, Password: 0C42E059"
},
{
"timestamp": "2025-08-19 16:37:59",
"username": "mukesh",
"email": "test@example.com",
"contact": "9999999999",
"password": "42F71FD9",
"message": "Your account credentials: Username: mukesh, Email: test@example.com, Password: 42F71FD9"
},
{
"timestamp": "2025-08-19 16:38:01",
"username": "mukesh",
"email": "test@example.com",
"contact": "9999999999",
"password": "F0DE0F08",
"message": "Your account credentials: Username: mukesh, Email: test@example.com, Password: F0DE0F08"
},
{
"timestamp": "2025-09-22 14:57:01",
"username": "John",
"email": "john@test.com",
"contact": "9876543210",
"password": "623EBBCB",
"message": "Your account credentials: Username: John, Email: john@test.com, Password: 623EBBCB"
},
{
"timestamp": "2025-09-30 11:03:10",
"username": "Rootxvending",
"email": "praveenkumar@rootxvending.com",
"contact": "9360857385",
"password": "9213F482",
"message": "Your account credentials: Username: Rootxvending, Email: praveenkumar@rootxvending.com, Password: 9213F482"
},
{
"timestamp": "2025-09-30 13:17:02",
"user_id": "USR-MUKEMUKE8421",
"username": "Mukesh S",
"email": "mukesh@rtx.com",
"contact": "9876543210",
"password": "840F9104",
"message": "Your account credentials: User ID: USR-MUKEMUKE8421, Username: Mukesh S, Email: mukesh@rtx.com, Password: 840F9104"
},
{
"timestamp": "2025-09-30 13:19:40",
"user_id": "USR-MUKEMUKE8578",
"username": "Mukesh S",
"email": "mukesh@rxw.com",
"contact": "9876543210",
"password": "8382FB34",
"message": "Your account credentials: User ID: USR-MUKEMUKE8578, Username: Mukesh S, Email: mukesh@rxw.com, Password: 8382FB34"
},
{
"timestamp": "2025-09-30 17:40:58",
"user_id": "USR-PRAVPRAV4256",
"username": "Praveen",
"email": "praveen@rtx.com",
"contact": "9876543210",
"password": "F8CB73CB",
"message": "Your account credentials: User ID: USR-PRAVPRAV4256, Username: Praveen, Email: praveen@rtx.com, Password: F8CB73CB"
},
{
"timestamp": "2025-09-30 20:17:34",
"user_id": "USR-MUKEMUKE3650",
"username": "Mukesh",
"email": "mukesh@rootxwire.com",
"contact": "9876543211",
"password": "77C1C095",
"message": "Your account credentials: User ID: USR-MUKEMUKE3650, Username: Mukesh, Email: mukesh@rootxwire.com, Password: 77C1C095"
},
{
"timestamp": "2025-09-30 20:22:22",
"user_id": "USR-MUKEMUKE3938",
"username": "Mukesh",
"email": "mukesh@rootxwire.com",
"contact": "9876543210",
"password": "0358342E",
"message": "Your account credentials: User ID: USR-MUKEMUKE3938, Username: Mukesh, Email: mukesh@rootxwire.com, Password: 0358342E"
},
{
"timestamp": "2025-09-30 20:25:49",
"user_id": "USR-MUKEMUKE4145",
"username": "mukesh",
"email": "mukesh@rootxwire.com",
"contact": "9080063331",
"password": "E95284C4",
"message": "Your account credentials: User ID: USR-MUKEMUKE4145, Username: mukesh, Email: mukesh@rootxwire.com, Password: E95284C4"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,5 @@
Flask
Flask-CORS
Flask-SQLAlchemy
python-dotenv
pymysql

5
Machine-Backend/run.py Normal file
View File

@ -0,0 +1,5 @@
from app import create_app
if __name__ == '__main__':
app = create_app()
app.run(debug=True, port=5000)

Submodule fuse-starter-v20.0.0 deleted from 02b9fb4ef3

View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

46
fuse-starter-v20.0.0/.gitignore vendored Normal file
View File

@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.angular/cache
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

View File

@ -0,0 +1 @@
legacy-peer-deps=true

View File

@ -0,0 +1 @@
20

View File

@ -0,0 +1,11 @@
{
"printWidth": 80,
"semi": true,
"bracketSameLine": false,
"trailingComma": "es5",
"tabWidth": 4,
"singleQuote": true,
"bracketSpacing": true,
"arrowParens": "always",
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"]
}

View File

@ -0,0 +1,72 @@
// -----------------------------------------------------------------------------------------------------
// @ 3rd party credits
// -----------------------------------------------------------------------------------------------------
// Flags
https://github.com/Yummygum/flagpack-core
// Icons
Material - https://material.io/tools/icons
Feather - https://feathericons.com/
Heroicons - https://github.com/refactoringui/heroicons
Iconsmind - https://iconsmind.com/
// Avatars
https://uifaces.co
// 404, 500 & Maintenance
https://undraw.co
// Mail app
Photo by Riccardo Chiarini on Unsplash - https://unsplash.com/photos/2VDa8bnLM8c
Photo by Johannes Plenio on Unsplash - https://unsplash.com/photos/RwHv7LgeC7s
Photo by Jamie Davies on Unsplash - https://unsplash.com/photos/Hao52Fu9-F8
Photo by Christian Joudrey on Unsplash - https://unsplash.com/photos/mWRR1xj95hg
// Profile page
Photo by Alex Knight on Unsplash - https://unsplash.com/photos/DpPutJwgyW8
// Cards
Photo by Kym Ellis on Unsplash - https://unsplash.com/photos/RPT3AjdXlZc
Photo by Patrick Hendry on Unsplash - https://unsplash.com/photos/Qgxk3PQsMiI
Photo by Hailey Kean on Unsplash - https://unsplash.com/photos/QxjsOlFNr_4
Photo by Nathan Anderson on Unsplash - https://unsplash.com/photos/mG8ShlWrMDI
Photo by Adrian Infernus on Unsplash - https://unsplash.com/photos/5apewqWk978
Photo by freestocks.org on Unsplash - https://unsplash.com/photos/c73TZ2sIU38
Photo by Tim Marshall on Unsplash - https://unsplash.com/photos/PKSCrmZdvwA
Photo by Daniel Koponyas on Unsplash - https://unsplash.com/photos/rbiLY6ZwvXQ
Photo by John Westrock on Unsplash - https://unsplash.com/photos/LCesauDseu8
Photo by Gabriel Sollmann on Unsplash - https://unsplash.com/photos/kFWj9y-tJB4
Photo by Kevin Wolf on Unsplash - https://unsplash.com/photos/BJyjgEdNTPs
Photo by Luca Bravo on Unsplash - https://unsplash.com/photos/hFzIoD0F_i8
Photo by Ian Baldwin on Unsplash - https://unsplash.com/photos/Dlj-SxxTlQ0
Photo by Ben Kolde on Unsplash - https://unsplash.com/photos/KRTFIBOfcFw
Photo by Chad Peltola on Unsplash - https://unsplash.com/photos/BTvQ2ET_iKc
Photo by rocknwool on Unsplash - https://unsplash.com/photos/r56oO1V5oms
Photo by Vita Vilcina on Unsplash - https://unsplash.com/photos/KtOid0FLjqU
Photo by Jia Ye on Unsplash - https://unsplash.com/photos/y8ZnQqgohLk
Photo by Parker Whitson on Unsplash - https://unsplash.com/photos/OlTYIqTjmVM
Photo by Dorian Hurst on Unsplash - https://unsplash.com/photos/a9uWPQlIbYc
Photo by Everaldo Coelho on Unsplash - https://unsplash.com/photos/KPaSCpklCZw
Photo by eberhard grossgasteiger on Unsplash - https://unsplash.com/photos/fh2JefbNlII
Photo by Orlova Maria on Unsplash - https://unsplash.com/photos/p8y4dWEMGMU
Photo by Jake Blucker on Unsplash - https://unsplash.com/photos/tMzCrBkM99Y
Photo by Jerry Zhang on Unsplash - https://unsplash.com/photos/oIBcow6n36s
Photo by John Cobb on Unsplash - https://unsplash.com/photos/IE_sifhay7o
Photo by Dan Gold on Unsplash - https://unsplash.com/photos/mDlhOIfGxNI
Photo by Ana Toma on Unsplash - https://unsplash.com/photos/XsGwe6gYg0c
Photo by Andrea on Unsplash - https://unsplash.com/photos/1AWY0N960Sk
Photo by Aswin on Unsplash - https://unsplash.com/photos/_roUcFWstas
Photo by Justin Kauffman on Unsplash - https://unsplash.com/photos/aWG_dqyhI0A
Photo by Barna Bartis on Unsplash - https://unsplash.com/photos/VVoBQqWrvkc
Photo by Kyle Hinkson on Unsplash - https://unsplash.com/photos/3439EnvnAGo
Photo by Spencer Watson on Unsplash - https://unsplash.com/photos/5TBf16GnHKg
Photo by adrian on Unsplash - https://unsplash.com/photos/1wrzvwoK8A4
Photo by Christopher Rusev on Unsplash - https://unsplash.com/photos/7gKWgCRixf0
Photo by Stephen Leonardi on Unsplash - https://unsplash.com/photos/MDmwQVgDHHM
Photo by Dwinanda Nurhanif Mujito on Unsplash - https://unsplash.com/photos/pKT5Mg16w_w
Photo by Humphrey Muleba on Unsplash - https://unsplash.com/photos/Zuvf5mxT5fs
Photo by adrian on Unsplash - https://unsplash.com/photos/PNRxLFPMyJY
Photo by Dahee Son on Unsplash - https://unsplash.com/photos/tV06QVJXVxU
Photo by Zachary Kyra-Derksen on Unsplash - https://unsplash.com/photos/vkqS7vLQUtg
Photo by Rodrigo Soares on Unsplash - https://unsplash.com/photos/8BFWBUkSqQo

View File

@ -0,0 +1,13 @@
# Stage 1: Build Angular app
FROM node:18-alpine as build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build --configuration production
# Stage 2: Serve via Nginx
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@ -0,0 +1,6 @@
Envato Standard License
Copyright (c) Withinpixels <hi@withinpixels.com>
This project is protected by Envato's Standard License. For more information,
check the official license page at [https://themeforest.net/licenses/standard](https://themeforest.net/licenses/standard)

View File

@ -0,0 +1,27 @@
# Fuse - Admin template and Starter project for Angular
This project was generated with [Angular CLI](https://github.com/angular/angular-cli)
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

View File

@ -0,0 +1,122 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"fuse": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/fuse",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"allowedCommonJsDependencies": [
"apexcharts",
"crypto-js/enc-utf8",
"crypto-js/hmac-sha256",
"crypto-js/enc-base64",
"quill-delta"
],
"assets": [
{
"glob": "**/*",
"input": "public"
},
{
"glob": "_redirects",
"input": "src",
"output": "/"
}
],
"stylePreprocessorOptions": {
"includePaths": ["src/@fuse/styles"]
},
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/@fuse/styles/tailwind.scss",
"src/@fuse/styles/themes.scss",
"src/styles/vendors.scss",
"src/@fuse/styles/main.scss",
"src/styles/styles.scss",
"src/styles/tailwind.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "3mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "75kb",
"maximumError": "90kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "fuse:build:production"
},
"development": {
"buildTarget": "fuse:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles/styles.scss"
],
"scripts": []
}
}
}
}
},
"cli": {
"analytics": false
}
}

14730
fuse-starter-v20.0.0/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,69 @@
{
"name": "fuse-angular",
"version": "20.0.0",
"description": "Fuse - Angular Admin Template and Starter Project",
"author": "https://themeforest.net/user/srcn",
"license": "https://themeforest.net/licenses/standard",
"private": true,
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"dependencies": {
"@angular/animations": "18.0.1",
"@angular/cdk": "18.0.1",
"@angular/common": "18.0.1",
"@angular/compiler": "18.0.1",
"@angular/core": "18.0.1",
"@angular/forms": "18.0.1",
"@angular/material": "18.0.1",
"@angular/material-luxon-adapter": "18.0.1",
"@angular/platform-browser": "18.0.1",
"@angular/platform-browser-dynamic": "18.0.1",
"@angular/router": "18.0.1",
"@ngneat/transloco": "6.0.4",
"apexcharts": "3.49.1",
"crypto-js": "4.2.0",
"highlight.js": "11.9.0",
"lodash-es": "4.17.21",
"luxon": "3.4.4",
"ng-apexcharts": "1.10.0",
"ngx-quill": "26.0.1",
"perfect-scrollbar": "1.5.5",
"quill": "2.0.2",
"rxjs": "7.8.1",
"tslib": "2.6.2",
"zone.js": "0.14.6"
},
"devDependencies": {
"@angular-devkit/build-angular": "^18.0.2",
"@angular/cli": "18.0.2",
"@angular/compiler-cli": "18.0.1",
"@tailwindcss/typography": "0.5.13",
"@types/chroma-js": "2.4.4",
"@types/crypto-js": "4.2.2",
"@types/highlight.js": "10.1.0",
"@types/jasmine": "5.1.4",
"@types/lodash": "4.17.4",
"@types/lodash-es": "4.17.12",
"@types/luxon": "3.4.2",
"autoprefixer": "10.4.19",
"chroma-js": "2.4.2",
"jasmine-core": "5.1.2",
"karma": "6.4.3",
"karma-chrome-launcher": "3.2.0",
"karma-coverage": "2.2.1",
"karma-jasmine": "5.1.0",
"karma-jasmine-html-reporter": "2.1.0",
"lodash": "4.17.21",
"postcss": "8.4.38",
"prettier": "3.3.0",
"prettier-plugin-organize-imports": "3.2.4",
"prettier-plugin-tailwindcss": "0.6.1",
"tailwindcss": "3.4.3",
"typescript": "5.4.5"
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

Some files were not shown because too many files have changed in this diff Show More