From e9c26d172d10ff57251215d845f78d97381d19b1 Mon Sep 17 00:00:00 2001 From: James Devine Date: Wed, 11 Mar 2026 16:35:40 +0100 Subject: [PATCH] feat: Complete EP Inspection Tool implementation with templates, security, and tests --- API_DOCS.md | 62 ++++++ README.md | 4 + app/__init__.py | 35 +++- app/models.py | 18 ++ app/routes/inspections.py | 7 - app/templates/admin/dashboard.html | 96 +++++++++ app/templates/base.html | 3 +- app/templates/dashboard.html | 151 ++++++++------ app/templates/errors/404.html | 24 ++- app/templates/errors/500.html | 24 ++- app/templates/inspection_form.html | 307 ++++++++++++----------------- app/templates/inspection_view.html | 165 ++++++++-------- config.py | 4 +- static/css/styles.css | 114 +++++++++++ tests.py | 296 +++++++++++++++++++++++++++ tests_old.py | 296 +++++++++++++++++++++++++++ 16 files changed, 1259 insertions(+), 347 deletions(-) create mode 100644 API_DOCS.md create mode 100644 app/templates/admin/dashboard.html create mode 100644 static/css/styles.css create mode 100644 tests.py create mode 100644 tests_old.py diff --git a/API_DOCS.md b/API_DOCS.md new file mode 100644 index 0000000..a6f6b96 --- /dev/null +++ b/API_DOCS.md @@ -0,0 +1,62 @@ +# EP Inspection Tool API Documentation + +## Authentication Endpoints + +### Login +- **Endpoint**: `POST /login` +- **Description**: Authenticate user and create session +- **Parameters**: + - `username` (string, required) + - `password` (string, required) + +### Logout +- **Endpoint**: `GET /logout` +- **Description**: End user session + +## Inspection Endpoints + +### Create Inspection +- **Endpoint**: `POST /inspections` +- **Description**: Create a new inspection report +- **Required Permissions**: authenticated user + +### View Inspection +- **Endpoint**: `GET /inspections/` +- **Description**: Get inspection details +- **Required Permissions**: authenticated user + +### Update Inspection +- **Endpoint**: `PUT /inspections/` +- **Description**: Update inspection details +- **Required Permissions**: authenticated user + +### Delete Inspection +- **Endpoint**: `DELETE /inspections/` +- **Description**: Delete an inspection +- **Required Permissions**: authenticated user + +## Admin Endpoints + +### User Management +- **Endpoint**: `GET /admin/users` +- **Description**: List all users +- **Required Permissions**: admin user + +### Create User +- **Endpoint**: `POST /admin/users` +- **Description**: Create a new user +- **Required Permissions**: admin user + +## File Upload Endpoints + +### Upload Photo +- **Endpoint**: `POST /inspections//photos` +- **Description**: Upload a photo for an inspection +- **Required Permissions**: authenticated user + +## Export Endpoints + +### Export PDF +- **Endpoint**: `GET /export/inspection//pdf` +- **Description**: Export inspection as PDF +- **Required Permissions**: authenticated user \ No newline at end of file diff --git a/README.md b/README.md index e1df377..1573c37 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ A web application for inspection reporting and management built with Flask and S 2. Run setup script: `python setup.py` 3. Run the application: `python run.py` +## Testing + +Run tests with: `python tests.py` + ## Usage 1. Start the server: `python run.py` diff --git a/app/__init__.py b/app/__init__.py index fc9890f..89d1272 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,14 +1,19 @@ +import logging +from logging.handlers import RotatingFileHandler +import os from flask import Flask, render_template from flask_login import LoginManager from flask_wtf.csrf import CSRFProtect from config import Config from app.models import db, Config as ConfigModel, User -import os from datetime import datetime +import secrets login_manager = LoginManager() login_manager.login_view = 'auth.login' login_manager.login_message = 'Please log in to access this page.' +# Enhanced session security +login_manager.session_protection = "strong" @login_manager.user_loader def load_user(user_id): @@ -33,6 +38,24 @@ def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(config_class) + # Enhanced security configuration + app.config['SESSION_COOKIE_SECURE'] = True # HTTPS only + app.config['SESSION_COOKIE_HTTPONLY'] = True # Prevent XSS + app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # CSRF protection + app.config['PERMANENT_SESSION_LIFETIME'] = 3600 # 1 hour session timeout + + # Setup logging + if not app.debug and not app.testing: + if not os.path.exists('logs'): + os.mkdir('logs') + file_handler = RotatingFileHandler('logs/ep_inspection_tool.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('EP Inspection Tool startup') + # Initialize extensions db.init_app(app) login_manager.init_app(app) @@ -73,11 +96,21 @@ def create_app(config_class=Config): # Error handlers @app.errorhandler(404) def not_found_error(error): + app.logger.warning(f'Page not found: {request.url}') return render_template('errors/404.html'), 404 @app.errorhandler(500) def internal_error(error): db.session.rollback() + app.logger.error(f'Server Error: {error}') return render_template('errors/500.html'), 500 + # Security headers + @app.after_request + def after_request(response): + response.headers['X-Content-Type-Options'] = 'nosniff' + response.headers['X-Frame-Options'] = 'DENY' + response.headers['X-XSS-Protection'] = '1; mode=block' + return response + return app \ No newline at end of file diff --git a/app/models.py b/app/models.py index 5377269..6c3e685 100644 --- a/app/models.py +++ b/app/models.py @@ -2,6 +2,7 @@ from flask_sqlalchemy import SQLAlchemy from datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash from flask_login import UserMixin +from sqlalchemy import CheckConstraint db = SQLAlchemy() @@ -14,6 +15,12 @@ class User(UserMixin, db.Model): is_admin = db.Column(db.Boolean, default=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) + __table_args__ = ( + CheckConstraint('length(username) >= 3', name='username_length_check'), + CheckConstraint('length(full_name) >= 2', name='full_name_length_check'), + CheckConstraint('length(email) >= 5', name='email_length_check'), + ) + def set_password(self, password): self.password_hash = generate_password_hash(password, salt_length=12) @@ -55,6 +62,13 @@ class Inspection(db.Model): inspectors = db.relationship('InspectionInspector', backref='inspection', lazy=True) photos = db.relationship('Photo', backref='inspection', lazy=True) + __table_args__ = ( + CheckConstraint('length(installation_name) >= 3', name='installation_name_length_check'), + CheckConstraint('length(location) >= 2', name='location_length_check'), + CheckConstraint('version >= 1', name='version_positive_check'), + CheckConstraint('reference_number >= 1', name='reference_number_positive_check'), + ) + def __repr__(self): return f'' @@ -78,5 +92,9 @@ class Photo(db.Model): action_required = db.Column(db.Enum('none', 'urgent', 'before_next'), nullable=False) uploaded_at = db.Column(db.DateTime, default=datetime.utcnow) + __table_args__ = ( + CheckConstraint('length(filename) >= 5', name='filename_length_check'), + ) + def __repr__(self): return f'' \ No newline at end of file diff --git a/app/routes/inspections.py b/app/routes/inspections.py index c24178b..0681cdd 100644 --- a/app/routes/inspections.py +++ b/app/routes/inspections.py @@ -200,13 +200,6 @@ def upload_photo(): return jsonify({'error': 'Upload failed'}), 500 -def allowed_file(filename): - """Check if file extension is allowed""" - ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} - if not filename: - return False - return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS - def allowed_file(filename): """Check if file extension is allowed""" ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} diff --git a/app/templates/admin/dashboard.html b/app/templates/admin/dashboard.html new file mode 100644 index 0000000..eb0d62d --- /dev/null +++ b/app/templates/admin/dashboard.html @@ -0,0 +1,96 @@ +{% extends "base.html" %} + +{% block title %}Admin Panel - Inspection Reporting Tool{% endblock %} + +{% block content %} +
+

Admin Panel

+

Manage users, inspections, and system settings

+
+ +
+
+
+
+ +
+
+

User Management

+

Manage system users and permissions

+
+
+ +
+ +
+
+
+ +
+
+

Inspection Management

+

View and manage inspection reports

+
+
+ +
+ +
+
+
+ +
+
+

System Settings

+

Configure application settings

+
+
+ +
+
+ +
+

Recent Activity

+
+
+
+
+ +
+
+
+

New user registered

+

John Doe registered an account

+
+
+
+
+
+ +
+
+
+

Inspection completed

+

Installation A - Inspection #123 completed

+
+
+
+
+
+ +
+
+
+

System settings updated

+

Email notifications settings modified

+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 1419e02..c7c49c4 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -6,6 +6,7 @@ {% block title %}Inspection Reporting Tool{% endblock %} +