Initial commit: Complete Fixture Manager daemon system

- Comprehensive Python daemon system for Linux servers
- Secure web dashboard with authentication and authorization
- RESTful API with JWT authentication
- MySQL database connectivity with connection pooling
- Advanced file upload system with real-time progress tracking
- Intelligent CSV/XLSX fixture parsing algorithms
- Two-stage upload workflow (fixture files + ZIP files)
- Full Linux daemon process management with systemd integration
- Complete security implementation with rate limiting and validation
- SHA1 checksum calculation and verification
- Automated installation and deployment scripts
- Comprehensive documentation and configuration management
parents
Pipeline #168 canceled with stages
# Database Configuration
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_USER=fixture_user
MYSQL_PASSWORD=secure_password_here
MYSQL_DATABASE=fixture_manager
# Security Configuration
SECRET_KEY=your-secret-key-here-change-in-production
JWT_SECRET_KEY=your-jwt-secret-key-here
BCRYPT_LOG_ROUNDS=12
# File Upload Configuration
UPLOAD_FOLDER=/var/lib/fixture-daemon/uploads
MAX_CONTENT_LENGTH=524288000
CHUNK_SIZE=8192
MAX_CONCURRENT_UPLOADS=5
# Daemon Configuration
DAEMON_PID_FILE=/var/run/fixture-daemon.pid
DAEMON_LOG_FILE=/var/log/fixture-daemon.log
DAEMON_WORKING_DIR=/var/lib/fixture-daemon
# Web Server Configuration
HOST=0.0.0.0
PORT=5000
DEBUG=false
# Logging Configuration
LOG_LEVEL=INFO
# JWT Configuration
JWT_ACCESS_TOKEN_EXPIRES=3600
\ No newline at end of file
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# 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/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Project specific
uploads/
logs/
backups/
*.pid
*.sock
\ No newline at end of file
This diff is collapsed.
import os
import logging
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_jwt_extended import JWTManager
from config import config
import colorlog
# Initialize extensions
db = SQLAlchemy()
login_manager = LoginManager()
jwt = JWTManager()
def create_app(config_name=None):
"""Application factory pattern"""
if config_name is None:
config_name = os.environ.get('FLASK_ENV', 'default')
app = Flask(__name__)
app.config.from_object(config[config_name])
config[config_name].init_app(app)
# Initialize extensions
db.init_app(app)
login_manager.init_app(app)
jwt.init_app(app)
# Configure login manager
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Please log in to access this page.'
login_manager.login_message_category = 'info'
# Configure logging
setup_logging(app)
# Register blueprints
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
from app.api import bp as api_bp
app.register_blueprint(api_bp, url_prefix='/api')
from app.main import bp as main_bp
app.register_blueprint(main_bp)
from app.upload import bp as upload_bp
app.register_blueprint(upload_bp, url_prefix='/upload')
# Create database tables
with app.app_context():
db.create_all()
return app
def setup_logging(app):
"""Setup application logging with colors for development"""
if not app.debug and not app.testing:
# Production logging
if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = logging.handlers.RotatingFileHandler(
'logs/fixture-daemon.log', maxBytes=10240, backupCount=10
)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Fixture Daemon startup')
else:
# Development logging with colors
handler = colorlog.StreamHandler()
handler.setFormatter(colorlog.ColoredFormatter(
'%(log_color)s%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red,bg_white',
}
))
app.logger.addHandler(handler)
app.logger.setLevel(logging.DEBUG)
@login_manager.user_loader
def load_user(user_id):
"""Load user for Flask-Login"""
from app.models import User
return User.query.get(int(user_id))
\ No newline at end of file
from flask import Blueprint
bp = Blueprint('api', __name__)
from app.api import routes
\ No newline at end of file
This diff is collapsed.
from flask import Blueprint
bp = Blueprint('auth', __name__)
from app.auth import routes
\ No newline at end of file
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
from app.models import User
import re
class LoginForm(FlaskForm):
"""Login form"""
username = StringField('Username', validators=[
DataRequired(message='Username is required'),
Length(min=3, max=80, message='Username must be between 3 and 80 characters')
])
password = PasswordField('Password', validators=[
DataRequired(message='Password is required')
])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Sign In')
class RegistrationForm(FlaskForm):
"""Registration form"""
username = StringField('Username', validators=[
DataRequired(message='Username is required'),
Length(min=3, max=80, message='Username must be between 3 and 80 characters')
])
email = StringField('Email', validators=[
DataRequired(message='Email is required'),
Email(message='Invalid email address'),
Length(max=120, message='Email must be less than 120 characters')
])
password = PasswordField('Password', validators=[
DataRequired(message='Password is required'),
Length(min=8, message='Password must be at least 8 characters long')
])
password2 = PasswordField('Repeat Password', validators=[
DataRequired(message='Please confirm your password'),
EqualTo('password', message='Passwords must match')
])
submit = SubmitField('Register')
def validate_username(self, username):
"""Validate username uniqueness and format"""
# Check for valid characters (alphanumeric and underscore only)
if not re.match(r'^[a-zA-Z0-9_]+$', username.data):
raise ValidationError('Username can only contain letters, numbers, and underscores')
# Check if username already exists
user = User.query.filter_by(username=username.data).first()
if user is not None:
raise ValidationError('Username already exists. Please choose a different one.')
def validate_email(self, email):
"""Validate email uniqueness"""
user = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('Email already registered. Please use a different email address.')
def validate_password(self, password):
"""Validate password strength"""
password_value = password.data
# Check minimum length
if len(password_value) < 8:
raise ValidationError('Password must be at least 8 characters long')
# Check for at least one uppercase letter
if not re.search(r'[A-Z]', password_value):
raise ValidationError('Password must contain at least one uppercase letter')
# Check for at least one lowercase letter
if not re.search(r'[a-z]', password_value):
raise ValidationError('Password must contain at least one lowercase letter')
# Check for at least one digit
if not re.search(r'\d', password_value):
raise ValidationError('Password must contain at least one number')
# Check for at least one special character
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password_value):
raise ValidationError('Password must contain at least one special character')
# Check for common weak passwords
weak_passwords = [
'password', '12345678', 'qwerty123', 'admin123',
'password123', '123456789', 'welcome123'
]
if password_value.lower() in weak_passwords:
raise ValidationError('Password is too common. Please choose a stronger password.')
class ChangePasswordForm(FlaskForm):
"""Change password form"""
current_password = PasswordField('Current Password', validators=[
DataRequired(message='Current password is required')
])
new_password = PasswordField('New Password', validators=[
DataRequired(message='New password is required'),
Length(min=8, message='Password must be at least 8 characters long')
])
new_password2 = PasswordField('Repeat New Password', validators=[
DataRequired(message='Please confirm your new password'),
EqualTo('new_password', message='Passwords must match')
])
submit = SubmitField('Change Password')
def validate_new_password(self, new_password):
"""Validate new password strength"""
password_value = new_password.data
# Check minimum length
if len(password_value) < 8:
raise ValidationError('Password must be at least 8 characters long')
# Check for at least one uppercase letter
if not re.search(r'[A-Z]', password_value):
raise ValidationError('Password must contain at least one uppercase letter')
# Check for at least one lowercase letter
if not re.search(r'[a-z]', password_value):
raise ValidationError('Password must contain at least one lowercase letter')
# Check for at least one digit
if not re.search(r'\d', password_value):
raise ValidationError('Password must contain at least one number')
# Check for at least one special character
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password_value):
raise ValidationError('Password must contain at least one special character')
# Check for common weak passwords
weak_passwords = [
'password', '12345678', 'qwerty123', 'admin123',
'password123', '123456789', 'welcome123'
]
if password_value.lower() in weak_passwords:
raise ValidationError('Password is too common. Please choose a stronger password.')
class ForgotPasswordForm(FlaskForm):
"""Forgot password form"""
email = StringField('Email', validators=[
DataRequired(message='Email is required'),
Email(message='Invalid email address')
])
submit = SubmitField('Reset Password')
def validate_email(self, email):
"""Validate email exists"""
user = User.query.filter_by(email=email.data).first()
if user is None:
raise ValidationError('No account found with that email address.')
class ResetPasswordForm(FlaskForm):
"""Reset password form"""
password = PasswordField('New Password', validators=[
DataRequired(message='Password is required'),
Length(min=8, message='Password must be at least 8 characters long')
])
password2 = PasswordField('Repeat Password', validators=[
DataRequired(message='Please confirm your password'),
EqualTo('password', message='Passwords must match')
])
submit = SubmitField('Reset Password')
def validate_password(self, password):
"""Validate password strength"""
password_value = password.data
# Check minimum length
if len(password_value) < 8:
raise ValidationError('Password must be at least 8 characters long')
# Check for at least one uppercase letter
if not re.search(r'[A-Z]', password_value):
raise ValidationError('Password must contain at least one uppercase letter')
# Check for at least one lowercase letter
if not re.search(r'[a-z]', password_value):
raise ValidationError('Password must contain at least one lowercase letter')
# Check for at least one digit
if not re.search(r'\d', password_value):
raise ValidationError('Password must contain at least one number')
# Check for at least one special character
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password_value):
raise ValidationError('Password must contain at least one special character')
# Check for common weak passwords
weak_passwords = [
'password', '12345678', 'qwerty123', 'admin123',
'password123', '123456789', 'welcome123'
]
if password_value.lower() in weak_passwords:
raise ValidationError('Password is too common. Please choose a stronger password.')
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
from flask import Blueprint
bp = Blueprint('main', __name__)
from app.main import routes
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
from flask import Blueprint
bp = Blueprint('upload', __name__)
from app.upload import routes
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired, FileAllowed
from wtforms import SubmitField, TextAreaField
from wtforms.validators import Optional
class FixtureUploadForm(FlaskForm):
"""Form for uploading fixture files (CSV/XLSX)"""
fixture_file = FileField('Fixture File', validators=[
FileRequired(message='Please select a fixture file'),
FileAllowed(['csv', 'xlsx', 'xls'],
message='Only CSV and Excel files are allowed')
])
description = TextAreaField('Description (Optional)', validators=[Optional()])
submit = SubmitField('Upload Fixture')
class ZipUploadForm(FlaskForm):
"""Form for uploading ZIP files for matches"""
zip_file = FileField('ZIP File', validators=[
FileRequired(message='Please select a ZIP file'),
FileAllowed(['zip'], message='Only ZIP files are allowed')
])
description = TextAreaField('Description (Optional)', validators=[Optional()])
submit = SubmitField('Upload ZIP')
\ No newline at end of file
This diff is collapsed.
# Utilities package
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
create a new python project composed by 3 main interacting programs, start to write the first one, a daemon, to be run on a linux server exposed to internet implementing a web dashboard and a RestAPI with authentication, connect to a mysql database. The dashboard offer a csv file or an xlsx file upload and parsing, the upload form will call this file "Fixture", where the fixture data contain the following columns: "Match #","Fighter1 (Township)","Fighter2 (Township)","Venue (Kampala Township)" and a dynamic list of other optional colums that from now on i will refer as "outcome results". the parsing will populate a mysql database, connection data is fetched from a configuration file, with a table where "Match #" is an integer, all the other mandatory data is a varchar255, and all the optional columns will be stored in a separate table with an id reference to the first table row id (autoincrement), a column "result" as varchar255 and containing the file upload optional column name, and a column "value" containing a float number with 2 digits precision. The first table will contain also the following columns that are NULL by default: "start time" ad datetime, "end time" as datetime, "result" as varchar255, and filename as varchar1024, file sha1sum as varchar255, and a fixture id unique for every fixture upload. When uploaded a fixture file, the dashboard should ask to upload a zip file ( large file upload here with progress bar ) for every row inserted in the database from the fixture file, when upload is done calculate the sha1sum of the zip file, and update the database with the file name and sha1sum. Only when the zip file is uploaded and the database update with sha1sum, then the record in the database is "active", you can use a flag in an additional column for it. Also create the SQL script to create the tables of the database.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment