From 595440099f54fedc84e48e94418ad6490ffcdd7c Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Mon, 20 Oct 2025 19:43:12 -0400 Subject: [PATCH 1/5] Add GitHub issue templates for bug reports and feature requests - Add structured bug report template with component selection, environment details, and reproduction steps - Add feature request template with priority levels, breaking change indicators, and use case descriptions - Templates aligned with project components including API endpoints, admin interface, barcode generation, and Entra ID authentication --- .github/ISSUE_TEMPLATE/bug_report.yml | 127 +++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.yml | 155 +++++++++++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..7f49d6f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,127 @@ +name: Bug Report +description: Report a bug or issue with the Flask AR API Demo Server +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report a bug! Please fill out the information below to help us resolve the issue. + + - type: dropdown + id: component + attributes: + label: Component + description: Which component is affected? + options: + - API Endpoints (login, arcontentfields, arinfo) + - Admin Interface + - Barcode Generation + - Image Serving + - Authentication (Entra ID) + - Database/Models + - Other + validations: + required: true + + - type: textarea + id: description + attributes: + label: Bug Description + description: A clear and concise description of what the bug is. + placeholder: Describe the bug... + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Steps to Reproduce + description: Steps to reproduce the behavior + placeholder: | + 1. Navigate to '...' + 2. Click on '...' + 3. Make API call to '...' + 4. See error + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What did you expect to happen? + placeholder: Describe what should happen... + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual Behavior + description: What actually happened? + placeholder: Describe what actually happened... + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Error Logs/Screenshots + description: If applicable, add error logs, stack traces, or screenshots + placeholder: Paste logs or drag and drop screenshots here... + validations: + required: false + + - type: dropdown + id: environment + attributes: + label: Environment + description: Where are you running the application? + options: + - Local Development + - Docker + - Cloud Deployment + - Other + validations: + required: true + + - type: input + id: python-version + attributes: + label: Python Version + description: What version of Python are you using? + placeholder: e.g., 3.9.0, 3.10.2, 3.11.5 + validations: + required: false + + - type: input + id: flask-version + attributes: + label: Flask Version + description: What version of Flask are you using? + placeholder: e.g., 2.2.5 + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Add any other context about the problem here + placeholder: Any additional information... + validations: + required: false + + - type: checkboxes + id: checklist + attributes: + label: Pre-submission Checklist + description: Please confirm the following + options: + - label: I have searched existing issues to ensure this bug hasn't been reported + required: true + - label: I have included all relevant information above + required: true + - label: I can reproduce this bug consistently + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..09ba4a5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,155 @@ +name: Feature Request +description: Suggest a new feature or enhancement for the Flask AR API Demo Server +title: "[Feature]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for suggesting a new feature! Please provide as much detail as possible to help us understand your request. + + - type: dropdown + id: component + attributes: + label: Component + description: Which component would this feature affect? + options: + - API Endpoints + - Admin Interface + - Barcode Generation + - Image Management + - Authentication/Authorization + - Database/Models + - Product Management + - Tenant Management + - Documentation + - Other + validations: + required: true + + - type: dropdown + id: feature-type + attributes: + label: Feature Type + description: What type of feature is this? + options: + - New Functionality + - Enhancement to Existing Feature + - UI/UX Improvement + - Performance Improvement + - Developer Experience + - Documentation + - Testing + - Security + - Other + validations: + required: true + + - type: textarea + id: problem + attributes: + label: Problem Statement + description: Is your feature request related to a problem? Please describe. + placeholder: I'm frustrated when... / There's no way to... / It would be helpful if... + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: Describe the solution you'd like to see + placeholder: I would like to be able to... / The system should... / Add a new endpoint that... + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Have you considered any alternative solutions or workarounds? + placeholder: I've thought about... / Another approach could be... / Currently using... + validations: + required: false + + - type: textarea + id: use-case + attributes: + label: Use Case + description: Describe your use case and how this feature would benefit you or others + placeholder: | + This feature would allow me to... + It would be useful for... + The expected workflow would be... + validations: + required: true + + - type: textarea + id: implementation + attributes: + label: Implementation Ideas + description: If you have ideas about how to implement this feature, please share them + placeholder: | + This could be implemented by... + Possible technical approach... + Required changes... + validations: + required: false + + - type: dropdown + id: priority + attributes: + label: Priority + description: How important is this feature to you? + options: + - Nice to have + - Would be useful + - Important + - Critical + validations: + required: true + + - type: dropdown + id: breaking + attributes: + label: Breaking Change + description: Would this feature require breaking changes to the API or existing functionality? + options: + - "No" + - "Yes" + - "Unsure" + validations: + required: true + + - type: textarea + id: mockups + attributes: + label: Mockups/Examples + description: If applicable, add mockups, screenshots, or examples from other systems + placeholder: Drag and drop images or paste links here... + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Add any other context, links, or references about the feature request + placeholder: Related to... / Similar to... / Reference documentation... + validations: + required: false + + - type: checkboxes + id: checklist + attributes: + label: Pre-submission Checklist + description: Please confirm the following + options: + - label: I have searched existing issues to ensure this feature hasn't been requested + required: true + - label: I have clearly described the feature and its benefits + required: true + - label: I am willing to help test this feature if implemented + required: false + - label: I am willing to contribute to the implementation + required: false From 2bf87e6d7267e9941cf2555b1db578c276db7fb5 Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Mon, 20 Oct 2025 19:54:33 -0400 Subject: [PATCH 2/5] Add Docker support and update README with deployment options - Add Dockerfile with multi-stage build for production deployment - Add docker-compose.yml for easy local development - Add .dockerignore to optimize Docker build context - Update README with Docker and local setup instructions --- .dockerignore | 50 +++++++++++++++++++++++++++++++++++++++ Dockerfile | 33 ++++++++++++++++++++++++++ README.md | 58 ++++++++++++++++++++++++++++++++++++++++------ docker-compose.yml | 19 +++++++++++++++ 4 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3309913 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,50 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +ENV/ +*.egg-info/ +dist/ +build/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Git +.git/ +.gitignore + +# Environment files (will be mounted separately) +.env + +# Documentation +README.md + +# CI/CD +.github/ + +# Claude +.claude/ + +# Instance data (will be mounted as volume) +src/app/instance/ +src/app/static/images/ +src/app/static/barcodes/ + +# Test files +*.pyc +.pytest_cache/ +.coverage +htmlcov/ + +# OS +.DS_Store +Thumbs.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e440308 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# Use Python 3.11 slim image as base +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + FLASK_APP=src/run.py + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better layer caching +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create necessary directories for runtime +RUN mkdir -p src/app/static/images src/app/static/barcodes src/app/instance + +# Expose port 5555 +EXPOSE 5555 + +# Run the application +CMD ["python", "src/run.py"] diff --git a/README.md b/README.md index a725579..ab60f5d 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,47 @@ This project is a simple Flask API for demonstrating AR content retrieval for ba ## Setup +### Option 1: Docker (Recommended) + +1. **Clone the Repository:** + +```bash +git clone +cd +``` + +2. **Configure Environment:** + +Copy `.env.example` to `.env` and update with your Azure AD credentials: + +```bash +cp .env.example .env +``` + +Edit `.env` and set your Azure AD configuration values. + +3. **Run with Docker Compose:** + +```bash +docker-compose up -d +``` + +The application will be available at `http://localhost:5555` + +4. **View Logs:** + +```bash +docker-compose logs -f +``` + +5. **Stop the Application:** + +```bash +docker-compose down +``` + +### Option 2: Local Python Setup + 1. **Clone the Repository:** ```bash @@ -49,23 +90,26 @@ cd pip install -r requirements.txt ``` -3. **Run the Server:** +3. **Configure Environment:** + +Copy `.env.example` to `.env` and update with your configuration. + +4. **Run the Server:** ```bash -python app.py +python src/run.py ``` -The application will automatically create the necessary directories and initialize the products.json file if it doesn't exist. +The application will automatically create the necessary directories and initialize the database if it doesn't exist. -4. **Access the Admin Interface:** +### Accessing the Application -Open your browser and navigate to: +**Admin Interface:** ``` http://localhost:5555/admin ``` -5. **Test the Endpoints:** - +**API Endpoints:** * Login: `GET http://localhost:5555/login` * Content Fields: `GET http://localhost:5555/arcontentfields` * AR Info (Example): `GET http://localhost:5555/arinfo?barcode=123456` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..189a88d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3.8' + +services: + web: + build: . + container_name: kcap-demo-server + ports: + - "5555:5555" + environment: + - FLASK_DEBUG=1 + env_file: + - .env + volumes: + # Mount static directories for persistent storage + - ./src/app/static/images:/app/src/app/static/images + - ./src/app/static/barcodes:/app/src/app/static/barcodes + # Mount instance directory for database persistence + - ./src/app/instance:/app/src/app/instance + restart: unless-stopped From 2b471ef09f2d41928b60e07ba4f3bbfb3941882d Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Mon, 20 Oct 2025 20:03:46 -0400 Subject: [PATCH 3/5] Add optional no-auth mode for simplified local testing This update introduces an AUTH_MODE configuration that allows developers to easily test the application locally without requiring Entra ID setup. Changes: - Add AUTH_MODE environment variable (entra/none) to .env.example and config - Update auth decorators to support both authentication modes - Create role selection page for no-auth mode (admin/user choice) - Modify auth routes to conditionally use MSAL only in Entra ID mode - Update UserModel to support mock test users with string IDs - Add visual indicator in navbar when running in no-auth mode Benefits: - Zero Azure AD configuration needed for local development - Quick testing of role-based features (admin vs user) - Existing Entra ID functionality remains unchanged - Simple toggle via environment variable To use no-auth mode, set AUTH_MODE=none in .env file. For production, use AUTH_MODE=entra (default). --- .env.example | 4 ++ src/app/blueprints/auth/routes.py | 62 ++++++++++++++++- src/app/config.py | 4 ++ src/app/decorators/auth.py | 47 +++++++++++-- src/app/models/user.py | 26 +++++-- src/app/templates/layout.html | 9 +++ src/app/templates/select_role.html | 105 +++++++++++++++++++++++++++++ 7 files changed, 245 insertions(+), 12 deletions(-) create mode 100644 src/app/templates/select_role.html diff --git a/.env.example b/.env.example index a7aeed6..00cf69f 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,10 @@ SECRET_KEY=your-secret-key-change-this-in-production FLASK_DEBUG=1 +# Authentication Mode +# Options: 'entra' (Entra ID/Azure AD) or 'none' (no authentication for local testing) +AUTH_MODE=none + # Entra ID (Azure AD) Configuration # Get these values from your Azure Portal > App Registrations AZURE_CLIENT_ID=your-client-id-here diff --git a/src/app/blueprints/auth/routes.py b/src/app/blueprints/auth/routes.py index 08ab072..0374488 100644 --- a/src/app/blueprints/auth/routes.py +++ b/src/app/blueprints/auth/routes.py @@ -1,14 +1,20 @@ -"""Authentication routes for Entra ID login/logout""" +"""Authentication routes for Entra ID login/logout and no-auth mode""" import secrets from flask import session, redirect, url_for, request, current_app, render_template, flash from . import auth_bp -from app.services.msal_service import MSALService from app.models.user import UserModel @auth_bp.route('/login') def login(): - """Initiate Entra ID login flow""" + """Initiate Entra ID login flow or redirect to role selection in no-auth mode""" + # Check if we're in no-auth mode + if current_app.config.get('AUTH_MODE') == 'none': + return redirect(url_for('auth.select_role')) + + # Entra ID mode + from app.services.msal_service import MSALService + # Generate state token for CSRF protection state = secrets.token_urlsafe(16) session['auth_state'] = state @@ -23,9 +29,51 @@ def login(): return redirect(auth_url) +@auth_bp.route('/select-role', methods=['GET', 'POST']) +def select_role(): + """Role selection page for no-auth mode""" + # Only available in no-auth mode + if current_app.config.get('AUTH_MODE') != 'none': + flash('Role selection is only available in no-auth mode', 'error') + return redirect(url_for('auth.login')) + + if request.method == 'POST': + role = request.form.get('role', 'user') + + # Validate role + if role not in ['admin', 'user']: + role = 'user' + + # Create a mock user for testing + # Use a consistent ID based on role for session persistence + user_id = f"test-{role}-user" + + # Store user in session + session['user'] = { + 'id': user_id, + 'email': f'{role}@test.local', + 'name': f'Test {role.capitalize()}', + 'role': role + } + + current_app.logger.info(f"No-auth mode: User selected {role} role") + + # Redirect to the originally requested page or home + next_url = session.pop('next', None) + if next_url: + return redirect(next_url) + + return redirect(url_for('main.index')) + + return render_template('select_role.html') + + @auth_bp.route('/callback') def callback(): """Handle the callback from Entra ID after authentication""" + # Import MSAL service only when needed (Entra ID mode) + from app.services.msal_service import MSALService + # Verify state to prevent CSRF state = request.args.get('state') if state != session.get('auth_state'): @@ -104,6 +152,14 @@ def logout(): # Clear session session.clear() + # Check if we're in no-auth mode + if current_app.config.get('AUTH_MODE') == 'none': + # Just redirect to home in no-auth mode + return redirect(url_for('main.index')) + + # Entra ID mode - logout from Azure AD + from app.services.msal_service import MSALService + # Remove from MSAL cache MSALService.remove_account() diff --git a/src/app/config.py b/src/app/config.py index 64b2c8b..01387f0 100644 --- a/src/app/config.py +++ b/src/app/config.py @@ -28,6 +28,10 @@ class Config: SESSION_USE_SIGNER = True SESSION_FILE_DIR = os.path.join(DATA_FOLDER, 'flask_session') + # Authentication Mode + # Options: 'entra' (Entra ID/Azure AD) or 'none' (no authentication) + AUTH_MODE = os.environ.get('AUTH_MODE', 'entra').lower() + # Entra ID (Azure AD) Configuration AZURE_CLIENT_ID = os.environ.get('AZURE_CLIENT_ID') AZURE_CLIENT_SECRET = os.environ.get('AZURE_CLIENT_SECRET') diff --git a/src/app/decorators/auth.py b/src/app/decorators/auth.py index 5c3b27e..8b662f5 100644 --- a/src/app/decorators/auth.py +++ b/src/app/decorators/auth.py @@ -4,17 +4,36 @@ from flask import session, redirect, url_for, request, abort, current_app from app.models.user import UserModel +def _ensure_no_auth_user(): + """ + Helper function to ensure a user exists in session when AUTH_MODE is 'none'. + If no user in session, redirect to role selection page. + """ + if current_app.config.get('AUTH_MODE') == 'none' and 'user' not in session: + # Redirect to role selection page + session['next'] = request.url + return redirect(url_for('auth.select_role')) + return None + + def login_required(f): """ Decorator to require user authentication. Redirects to login if user is not authenticated. + In 'none' auth mode, redirects to role selection if no session exists. """ @wraps(f) def decorated_function(*args, **kwargs): - if 'user' not in session: - # Store the requested URL to redirect back after login + # Check if we're in no-auth mode and need to set up a user + if current_app.config.get('AUTH_MODE') == 'none': + redirect_response = _ensure_no_auth_user() + if redirect_response: + return redirect_response + elif 'user' not in session: + # Entra ID mode - redirect to login session['next'] = request.url return redirect(url_for('auth.login')) + return f(*args, **kwargs) return decorated_function @@ -26,7 +45,13 @@ def admin_required(f): """ @wraps(f) def decorated_function(*args, **kwargs): - if 'user' not in session: + # Check if we're in no-auth mode and need to set up a user + if current_app.config.get('AUTH_MODE') == 'none': + redirect_response = _ensure_no_auth_user() + if redirect_response: + return redirect_response + elif 'user' not in session: + # Entra ID mode - redirect to login session['next'] = request.url return redirect(url_for('auth.login')) @@ -47,7 +72,13 @@ def tenant_access_required(f): """ @wraps(f) def decorated_function(*args, **kwargs): - if 'user' not in session: + # Check if we're in no-auth mode and need to set up a user + if current_app.config.get('AUTH_MODE') == 'none': + redirect_response = _ensure_no_auth_user() + if redirect_response: + return redirect_response + elif 'user' not in session: + # Entra ID mode - redirect to login session['next'] = request.url return redirect(url_for('auth.login')) @@ -76,7 +107,13 @@ def settings_access_required(f): """ @wraps(f) def decorated_function(*args, **kwargs): - if 'user' not in session: + # Check if we're in no-auth mode and need to set up a user + if current_app.config.get('AUTH_MODE') == 'none': + redirect_response = _ensure_no_auth_user() + if redirect_response: + return redirect_response + elif 'user' not in session: + # Entra ID mode - redirect to login session['next'] = request.url return redirect(url_for('auth.login')) diff --git a/src/app/models/user.py b/src/app/models/user.py index 55a3ba3..372b193 100644 --- a/src/app/models/user.py +++ b/src/app/models/user.py @@ -67,8 +67,22 @@ class UserModel: return None @staticmethod - def get_by_id(user_id: int) -> Optional[Dict]: - """Get user by ID""" + def get_by_id(user_id) -> Optional[Dict]: + """Get user by ID (supports both integer IDs and string test IDs for no-auth mode)""" + # Handle test user IDs for no-auth mode + if isinstance(user_id, str) and user_id.startswith('test-'): + # Mock test user + role = 'admin' if 'admin' in user_id else 'user' + return { + 'id': user_id, + 'email': f'{role}@test.local', + 'azure_oid': None, + 'name': f'Test {role.capitalize()}', + 'role': role, + 'created_at': None, + 'updated_at': None + } + with get_db() as conn: cursor = conn.cursor() cursor.execute( @@ -211,13 +225,17 @@ class UserModel: return [row[0] for row in cursor.fetchall()] @staticmethod - def has_access_to_tenant(user_id: int, tenant_id: str) -> bool: + def has_access_to_tenant(user_id, tenant_id: str) -> bool: """Check if user has access to a specific tenant""" # Admin has access to everything user = UserModel.get_by_id(user_id) if user and user['role'] == UserModel.ROLE_ADMIN: return True + # Test users in no-auth mode don't have tenant associations + if isinstance(user_id, str) and user_id.startswith('test-'): + return False + # Check if user owns the tenant with get_db() as conn: cursor = conn.cursor() @@ -228,7 +246,7 @@ class UserModel: return cursor.fetchone() is not None @staticmethod - def is_admin(user_id: int) -> bool: + def is_admin(user_id) -> bool: """Check if user has admin role""" user = UserModel.get_by_id(user_id) return user and user['role'] == UserModel.ROLE_ADMIN diff --git a/src/app/templates/layout.html b/src/app/templates/layout.html index 262405e..81ee272 100644 --- a/src/app/templates/layout.html +++ b/src/app/templates/layout.html @@ -72,6 +72,15 @@ {% endif %}