Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/docker-build-push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Build and Push Docker Image
run-name: Build and push todo-api image for ${{ github.ref_name }} by @${{ github.actor }}

on:
push:
branches:
- 'main'
- 'develop'
tags:
- 'v*'
pull_request:
branches:
- 'main'

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

permissions:
contents: read
packages: write

jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
# set latest tag for default branch
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
76 changes: 71 additions & 5 deletions app/config/auth_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,84 @@ def __init__(self):
self.api_key = None
self.jwt_secret = None
self.session_secret = None

def configure_api_key(self, api_key):
self.auth_method = AuthMethod.API_KEY
self.api_key = api_key

def configure_jwt(self, secret_key):
self.auth_method = AuthMethod.JWT
self.jwt_secret = secret_key

def configure_session(self, secret_key):
self.auth_method = AuthMethod.SESSION
self.session_secret = secret_key

def disable_auth(self):
self.auth_method = AuthMethod.NONE
self.auth_method = AuthMethod.NONE

def update_from_dict(self, config_dict):
"""Update authentication configuration from a dictionary.

Args:
config_dict (dict): Configuration dictionary with 'auth' key containing:
- method: one of 'none', 'api_key', 'jwt', 'session'
- api_key: API key (required if method is 'api_key')
- secret: Secret key (required if method is 'jwt' or 'session')

Raises:
ValueError: If configuration is invalid
"""
auth_config = config_dict.get('auth', {})
method = auth_config.get('method', 'none')

# Validate method
valid_methods = [e.value for e in AuthMethod]
if method not in valid_methods:
raise ValueError(f"Invalid authentication method: {method}. Must be one of: {valid_methods}")

# Reset current configuration
self.auth_method = AuthMethod.NONE
self.api_key = None
self.jwt_secret = None
self.session_secret = None

# Apply new configuration
if method == 'none':
self.disable_auth()
elif method == 'api_key':
api_key = auth_config.get('api_key')
if not api_key:
raise ValueError("API key must be provided when using api_key authentication")
self.configure_api_key(api_key)
elif method == 'jwt':
secret = auth_config.get('secret')
if not secret:
raise ValueError("Secret key must be provided when using JWT authentication")
self.configure_jwt(secret)
elif method == 'session':
secret = auth_config.get('secret')
if not secret:
raise ValueError("Secret key must be provided when using session authentication")
self.configure_session(secret)

def to_dict(self):
"""Convert current configuration to dictionary format.

Returns:
dict: Configuration dictionary in the same format as the YAML file
"""
config = {
'auth': {
'method': self.auth_method.value
}
}

if self.auth_method == AuthMethod.API_KEY and self.api_key:
config['auth']['api_key'] = self.api_key
elif self.auth_method == AuthMethod.JWT and self.jwt_secret:
config['auth']['secret'] = self.jwt_secret
elif self.auth_method == AuthMethod.SESSION and self.session_secret:
config['auth']['secret'] = self.session_secret

return config
33 changes: 18 additions & 15 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from routes.docs import docs_bp
from routes.notes import notes_bp
from routes.auth import auth_bp, init_auth_routes
from middleware.auth_middleware import AuthMiddleware
from middleware.auth_middleware import AuthMiddleware, set_auth_middleware_instance
from utils.config import load_config, load_initial_todos, load_initial_users
from utils.auth import setup_auth_config
from services.auth_service import init_auth_service, add_user
Expand All @@ -13,27 +13,27 @@

def create_app(auth_config):
"""Create and configure the Flask application.

This function:
1. Creates a new Flask instance
2. Configures app settings and secrets
3. Sets up authentication middleware
4. Registers blueprints with their URL prefixes

Args:
auth_config: Authentication configuration object

Returns:
Flask: Configured Flask application instance
"""
app = Flask(__name__)

# Configure application settings
app.config['JSONIFY_PRETTYPRINT_REGULAR'] = True
app.config['SECRET_KEY'] = secrets.token_hex(32) # Generate secure random secret key
app.config['auth_config'] = auth_config
app.config['initial_todos'] = load_initial_todos() # Load initial todos from config file

# Configure Swagger
template = {
"swagger": "2.0",
Expand All @@ -43,26 +43,29 @@ def create_app(auth_config):
"version": "1.0.0"
}
}

app.config['SWAGGER'] = {
'title': 'Todo API',
'uiversion': 3,
'specs_route': '/',
'url_prefix': '/swagger'
}

Swagger(app, template=template)

# Set up authentication
init_auth_routes(auth_config)
auth_middleware = AuthMiddleware(auth_config)


# Store the middleware instance globally for runtime updates
set_auth_middleware_instance(auth_middleware)

# Define routes that require authentication
protected_blueprints = {
todos_bp: "/todos", # Todo management endpoints
notes_bp: "/notes" # Note management endpoints
}

# Register protected routes with authentication middleware
for blueprint, url_prefix in protected_blueprints.items():
auth_middleware.protect_blueprint(blueprint)
Expand All @@ -84,17 +87,17 @@ def seed_users():
try:
# Load authentication configuration from config file
auth_method, secret = load_config()

# Set up authentication based on configuration
auth_config = setup_auth_config(auth_method, secret)
seed_users()

# Initialize auth service
init_auth_service(auth_config)

# Create and configure the application
app = create_app(auth_config)

# Start the server
app.run(host="0.0.0.0", port=8000) # Listen on all interfaces, port 8000
except ValueError as e:
Expand Down
33 changes: 30 additions & 3 deletions app/middleware/auth_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,38 @@ def _validate_session(self):
"""Validate session authentication"""
if not session.get("authenticated"):
return jsonify({"error": "Valid session required"}), 401

# Check if session has been invalidated
current_session = request.cookies.get('session')
if current_session and current_session in invalidated_sessions:
session.clear()
return jsonify({"error": "Session has been invalidated"}), 401

return None

return None

# Global middleware instance for runtime updates
_global_middleware_instance = None

def reset_auth_middleware(new_config: AuthConfig):
"""Reset the global auth middleware instance with new configuration.

This function updates the middleware configuration that's used
by all protected blueprints. Due to Flask's blueprint registration
mechanics, we update the global instance that blueprints reference.

Args:
new_config: New AuthConfig instance
"""
global _global_middleware_instance
if _global_middleware_instance:
_global_middleware_instance.config = new_config
print(f"Auth middleware reset with new configuration: {new_config.auth_method.value}")

def get_auth_middleware_instance():
"""Get the global middleware instance."""
return _global_middleware_instance

def set_auth_middleware_instance(instance):
"""Set the global middleware instance."""
global _global_middleware_instance
_global_middleware_instance = instance
Loading