Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
204b794
feat: add BackupModel, PadModel, and UserModel for database schema
atyrode May 2, 2025
dd234bb
feat: introduce base model and refactor existing models for database …
atyrode May 2, 2025
cb0658d
feat: implement repository modules for database operations
atyrode May 2, 2025
a12827f
feat: add service modules for user, pad, and backup management
atyrode May 2, 2025
6bc027c
feat: implement database module with async support
atyrode May 2, 2025
24b7611
refactor: update SQLAlchemy column types in UserModel
atyrode May 2, 2025
8735bc4
refactor: update models to use schema configuration
atyrode May 2, 2025
026c635
refactor: update UUID type usage in BackupModel and PadModel
atyrode May 2, 2025
1274cb3
refactor: centralize schema configuration and update database initial…
atyrode May 2, 2025
5cf9698
refactor: update UserModel and UserRepository to use UUID for user IDs
atyrode May 2, 2025
1ed0774
feat: enhance user model and repository for additional user attributes
atyrode May 2, 2025
eefd4a2
feat: add JWT token handling and user management dependencies
atyrode May 2, 2025
289df9e
feat: add TemplatePadModel for managing template pads
atyrode May 2, 2025
ff53a8a
feat: add TemplatePad repository, service, and router for template pa…
atyrode May 2, 2025
287fd75
feat: add docstrings to user router functions for improved clarity
atyrode May 2, 2025
0c496d9
feat: update requirements for database and file handling
atyrode May 2, 2025
30e1f27
feat: enhance template management and application structure
atyrode May 2, 2025
d96ff3c
feat: refactor authentication and user session management
atyrode May 2, 2025
0d14371
refactor: streamline user session handling and update API endpoints
atyrode May 2, 2025
20923d6
refactor: reorganize workspace routing and implement user email uniqu…
atyrode May 2, 2025
7b57a99
refactor: update pad router and API endpoint consistency
atyrode May 2, 2025
63447ed
refactor: update workspace state management in API and components
atyrode May 2, 2025
de080bb
refactor: remove db.py and reorganize database initialization
atyrode May 2, 2025
d1756da
refactor: integrate CoderAPI into authentication and workspace manage…
atyrode May 2, 2025
01499cc
refactor: migrate CoderAPI configuration to centralized config module
atyrode May 2, 2025
c9dab93
refactor: update authentication URL configuration for Keycloak
atyrode May 2, 2025
4f91532
refactor: enhance JWT token handling and session management
atyrode May 2, 2025
e69cb4e
refactor: enhance Redis connection management and backup functionality
atyrode May 2, 2025
8a0abd7
refactor: standardize API endpoint paths and enhance datetime handling
atyrode May 2, 2025
e68d32e
refactor: update CORS middleware configuration
atyrode May 2, 2025
d9620cc
feat: add user synchronization with authentication token data
atyrode May 2, 2025
d773ad6
feat: implement database migration system with Alembic
atyrode May 2, 2025
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
30 changes: 13 additions & 17 deletions src/backend/coder.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
import os
import requests
from dotenv import load_dotenv
from config import CODER_API_KEY, CODER_URL, CODER_TEMPLATE_ID, CODER_DEFAULT_ORGANIZATION, CODER_WORKSPACE_NAME

class CoderAPI:
"""
A class for interacting with the Coder API using credentials from .env file
A class for interacting with the Coder API using credentials from config
"""

def __init__(self):
# Load environment variables from .env file
load_dotenv()
# Get configuration from config
self.api_key = CODER_API_KEY
self.coder_url = CODER_URL
self.template_id = CODER_TEMPLATE_ID
self.default_organization_id = CODER_DEFAULT_ORGANIZATION

# Get configuration from environment variables
self.api_key = os.getenv("CODER_API_KEY")
self.coder_url = os.getenv("CODER_URL")
self.user_id = os.getenv("USER_ID")
self.template_id = os.getenv("CODER_TEMPLATE_ID")
self.default_organization_id = os.getenv("CODER_DEFAULT_ORGANIZATION")

# Check if required environment variables are set
# Check if required configuration variables are set
if not self.api_key or not self.coder_url:
raise ValueError("CODER_API_KEY and CODER_URL must be set in .env file")
raise ValueError("CODER_API_KEY and CODER_URL must be set in environment variables")

# Set up common headers for API requests
self.headers = {
Expand Down Expand Up @@ -56,9 +52,9 @@ def create_workspace(self, user_id, parameter_values=None):
template_id = self.template_id

if not template_id:
raise ValueError("template_id must be provided or TEMPLATE_ID must be set in .env")
raise ValueError("template_id must be provided or TEMPLATE_ID must be set in environment variables")

name = os.getenv("CODER_WORKSPACE_NAME", "ubuntu")
name = CODER_WORKSPACE_NAME

# Prepare the request data
data = {
Expand Down Expand Up @@ -201,7 +197,7 @@ def get_workspace_status_for_user(self, username):
Returns:
dict: Workspace status data if found, None otherwise
"""
workspace_name = os.getenv("CODER_WORKSPACE_NAME", "ubuntu")
workspace_name = CODER_WORKSPACE_NAME

endpoint = f"{self.coder_url}/api/v2/users/{username}/workspace/{workspace_name}"
response = requests.get(endpoint, headers=self.headers)
Expand Down Expand Up @@ -282,4 +278,4 @@ def stop_workspace(self, workspace_id):
headers['Content-Type'] = 'application/json'
response = requests.post(endpoint, headers=headers, json=data)
response.raise_for_status()
return response.json()
return response.json()
128 changes: 87 additions & 41 deletions src/backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,94 +3,132 @@
import time
import httpx
import redis
from redis import ConnectionPool, Redis
import jwt
from jwt.jwks_client import PyJWKClient
from typing import Optional, Dict, Any, Tuple
from dotenv import load_dotenv

# Load environment variables once
load_dotenv()

# ===== Application Configuration =====
STATIC_DIR = os.getenv("STATIC_DIR")
ASSETS_DIR = os.getenv("ASSETS_DIR")
FRONTEND_URL = os.getenv('FRONTEND_URL')

OIDC_CONFIG = {
'client_id': os.getenv('OIDC_CLIENT_ID'),
'client_secret': os.getenv('OIDC_CLIENT_SECRET'),
'server_url': os.getenv('OIDC_SERVER_URL'),
'realm': os.getenv('OIDC_REALM'),
'redirect_uri': os.getenv('REDIRECT_URI'),
'frontend_url': os.getenv('FRONTEND_URL')
}

# Redis connection
redis_client = redis.Redis(
host=os.getenv('REDIS_HOST', 'localhost'),
password=os.getenv('REDIS_PASSWORD', None),
port=int(os.getenv('REDIS_PORT', 6379)),
MAX_BACKUPS_PER_USER = 10 # Maximum number of backups to keep per user
MIN_INTERVAL_MINUTES = 5 # Minimum interval in minutes between backups
DEFAULT_PAD_NAME = "Untitled" # Default name for new pads
DEFAULT_TEMPLATE_NAME = "default" # Template name to use when a user doesn't have a pad

# ===== PostHog Configuration =====
POSTHOG_API_KEY = os.getenv("VITE_PUBLIC_POSTHOG_KEY")
POSTHOG_HOST = os.getenv("VITE_PUBLIC_POSTHOG_HOST")

# ===== OIDC Configuration =====
OIDC_CLIENT_ID = os.getenv('OIDC_CLIENT_ID')
OIDC_CLIENT_SECRET = os.getenv('OIDC_CLIENT_SECRET')
OIDC_SERVER_URL = os.getenv('OIDC_SERVER_URL')
OIDC_REALM = os.getenv('OIDC_REALM')
OIDC_REDIRECT_URI = os.getenv('REDIRECT_URI')

# ===== Redis Configuration =====
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
REDIS_PASSWORD = os.getenv('REDIS_PASSWORD', None)
REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))

# Create a Redis connection pool
redis_pool = ConnectionPool(
host=REDIS_HOST,
password=REDIS_PASSWORD,
port=REDIS_PORT,
db=0,
decode_responses=True
decode_responses=True,
max_connections=10, # Adjust based on your application's needs
socket_timeout=5.0,
socket_connect_timeout=1.0,
health_check_interval=30
)

# Create a Redis client that uses the connection pool
redis_client = Redis(connection_pool=redis_pool)

def get_redis_client():
"""Get a Redis client from the connection pool"""
return Redis(connection_pool=redis_pool)

# ===== Coder API Configuration =====
CODER_API_KEY = os.getenv("CODER_API_KEY")
CODER_URL = os.getenv("CODER_URL")
CODER_TEMPLATE_ID = os.getenv("CODER_TEMPLATE_ID")
CODER_DEFAULT_ORGANIZATION = os.getenv("CODER_DEFAULT_ORGANIZATION")
CODER_WORKSPACE_NAME = os.getenv("CODER_WORKSPACE_NAME", "ubuntu")

# Cache for JWKS client
_jwks_client = None

# Session management functions
def get_session(session_id: str) -> Optional[Dict[str, Any]]:
"""Get session data from Redis"""
session_data = redis_client.get(f"session:{session_id}")
client = get_redis_client()
session_data = client.get(f"session:{session_id}")
if session_data:
return json.loads(session_data)
return None

def set_session(session_id: str, data: Dict[str, Any], expiry: int) -> None:
"""Store session data in Redis with expiry in seconds"""
redis_client.setex(
client = get_redis_client()
client.setex(
f"session:{session_id}",
expiry,
json.dumps(data)
)

def delete_session(session_id: str) -> None:
"""Delete session data from Redis"""
redis_client.delete(f"session:{session_id}")

provisioning_times = {}
client = get_redis_client()
client.delete(f"session:{session_id}")

def get_auth_url() -> str:
"""Generate the authentication URL for Keycloak login"""
auth_url = f"{OIDC_CONFIG['server_url']}/realms/{OIDC_CONFIG['realm']}/protocol/openid-connect/auth"
auth_url = f"{OIDC_SERVER_URL}/realms/{OIDC_REALM}/protocol/openid-connect/auth"
params = {
'client_id': OIDC_CONFIG['client_id'],
'client_id': OIDC_CLIENT_ID,
'response_type': 'code',
'redirect_uri': OIDC_CONFIG['redirect_uri'],
'redirect_uri': OIDC_REDIRECT_URI,
'scope': 'openid profile email'
}
return f"{auth_url}?{'&'.join(f'{k}={v}' for k,v in params.items())}"

def get_token_url() -> str:
"""Get the token endpoint URL"""
return f"{OIDC_CONFIG['server_url']}/realms/{OIDC_CONFIG['realm']}/protocol/openid-connect/token"
return f"{OIDC_SERVER_URL}/realms/{OIDC_REALM}/protocol/openid-connect/token"

def is_token_expired(token_data: Dict[str, Any], buffer_seconds: int = 30) -> bool:
"""
Check if the access token is expired or about to expire

Args:
token_data: The token data containing the access token
buffer_seconds: Buffer time in seconds to refresh token before it actually expires

Returns:
bool: True if token is expired or about to expire, False otherwise
"""
if not token_data or 'access_token' not in token_data:
return True

try:
# Decode the JWT token without verification to get expiration time
decoded = jwt.decode(token_data['access_token'], options={"verify_signature": False})
# Get the signing key
jwks_client = get_jwks_client()
signing_key = jwks_client.get_signing_key_from_jwt(token_data['access_token'])

# Get expiration time from token
exp_time = decoded.get('exp', 0)
# Decode with verification
decoded = jwt.decode(
token_data['access_token'],
signing_key.key,
algorithms=["RS256"], # Common algorithm for OIDC
audience=OIDC_CLIENT_ID,
)

# Check if token is expired or about to expire (with buffer)
# Check expiration
exp_time = decoded.get('exp', 0)
current_time = time.time()
return current_time + buffer_seconds >= exp_time
except jwt.ExpiredSignatureError:
return True
except Exception as e:
print(f"Error checking token expiration: {str(e)}")
return True
Expand All @@ -115,8 +153,8 @@ async def refresh_token(session_id: str, token_data: Dict[str, Any]) -> Tuple[bo
get_token_url(),
data={
'grant_type': 'refresh_token',
'client_id': OIDC_CONFIG['client_id'],
'client_secret': OIDC_CONFIG['client_secret'],
'client_id': OIDC_CLIENT_ID,
'client_secret': OIDC_CLIENT_SECRET,
'refresh_token': token_data['refresh_token']
}
)
Expand All @@ -136,3 +174,11 @@ async def refresh_token(session_id: str, token_data: Dict[str, Any]) -> Tuple[bo
except Exception as e:
print(f"Error refreshing token: {str(e)}")
return False, token_data

def get_jwks_client():
"""Get or create a PyJWKClient for token verification"""
global _jwks_client
if _jwks_client is None:
jwks_url = f"{OIDC_SERVER_URL}/realms/{OIDC_REALM}/protocol/openid-connect/certs"
_jwks_client = PyJWKClient(jwks_url)
return _jwks_client
31 changes: 31 additions & 0 deletions src/backend/database/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
Database module for the application.

This module provides access to all database components used in the application.
"""

from .database import (
init_db,
get_session,
get_user_repository,
get_pad_repository,
get_backup_repository,
get_template_pad_repository,
get_user_service,
get_pad_service,
get_backup_service,
get_template_pad_service
)

__all__ = [
'init_db',
'get_session',
'get_user_repository',
'get_pad_repository',
'get_backup_repository',
'get_template_pad_repository',
'get_user_service',
'get_pad_service',
'get_backup_service',
'get_template_pad_service',
]
Loading