diff --git a/terraform/Dockerfile b/terraform/Dockerfile new file mode 100644 index 0000000..09e40bd --- /dev/null +++ b/terraform/Dockerfile @@ -0,0 +1,28 @@ +# Use Python 3.11 slim image +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Copy requirements first for better layer caching +COPY requirements.txt . + +# Install dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create non-root user (security best practice) +RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app +USER appuser + +# Expose port +EXPOSE 5000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:5000/ || exit 1 + +# Run the application +CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--timeout", "120", "app:app"] \ No newline at end of file diff --git a/terraform/FIREBASE_AUTH.md b/terraform/FIREBASE_AUTH.md new file mode 100644 index 0000000..183f8fc --- /dev/null +++ b/terraform/FIREBASE_AUTH.md @@ -0,0 +1,262 @@ +# Firebase Authentication Automation with Terraform + +This guide explains how Firebase Authentication settings are automated in the Rothbard Law Group deployment. + +## 🚀 What's Automated + +### 1. Authentication Providers +- **Email/Password**: Enabled by default +- **Google Sign-In**: Optional (controlled by `enable_google_signin` variable) +- **Phone, Facebook, Apple**: Disabled for security + +### 2. Email Templates +- **Password Reset**: Professional HTML and text templates +- **Email Verification**: Welcome templates with branding +- **Customizable**: From address, name, and reply-to settings + +### 3. Security Rules +- **Firestore Rules**: Users can only access their own data +- **Authentication Required**: All database operations require auth +- **Profile Access**: Users can read/write their own profile only + +### 4. Firebase Hosting +- **Static Asset Hosting**: Optional for CSS, JS, images +- **Caching Headers**: Optimized performance +- **URL Rewrites**: Proper routing for SPA + +## 📋 Configuration Variables + +Add these to your `terraform.tfvars`: + +```hcl +# Authentication Settings +enable_google_signin = false # Set to true to enable Google Sign-In + +# Email Configuration +auth_from_email = "noreply@rothbardlaw.com" +auth_from_name = "Rothbard Law Group" +auth_reply_to = "support@rothbardlaw.com" +``` + +## 🔧 Authentication Provider Setup + +### Email/Password (Default) +```hcl +sign_in_options { + email { + enabled = true + password_required = true + } +} +``` + +### Google Sign-In (Optional) +To enable Google Sign-In: + +1. **Set variable**: + ```hcl + enable_google_signin = true + ``` + +2. **Configure OAuth in Google Cloud**: + ```bash + # Enable Google+ API + gcloud services enable plus.googleapis.com + + # Create OAuth consent screen + gcloud alpha iap oauth-clients create \ + --display-name="Rothbard Portal" \ + --brand="Rothbard Law Group" + ``` + +3. **Update Firebase Console**: + - Go to Firebase Console → Authentication → Sign-in method + - Enable Google provider + - Add your OAuth client ID and secret + +## 📧 Email Template Customization + +### Template Files +- `templates/reset_password.html` - Password reset HTML +- `templates/reset_password.txt` - Password reset text +- `templates/email_verification.html` - Email verification HTML +- `templates/email_verification.txt` - Email verification text + +### Customization Options +- **Branding**: Update colors, logos in HTML templates +- **Contact Info**: Change address, phone numbers +- **Content**: Modify welcome messages and instructions + +### Email Variables Available +- `{{ resetLink }}` - Password reset URL +- `{{ verificationLink }}` - Email verification URL +- `{{ userEmail }}` - User's email address + +## 🔒 Security Rules Explained + +### Firestore Rules +```javascript +// Users can only access their own profile +match /users/{userId} { + allow read, write: if request.auth.uid == userId; +} + +// All other collections require authentication +match /{collection=**} { + allow read, write: if request.auth != null; +} +``` + +### Security Features +- **User Isolation**: Users can't access other users' data +- **Authentication Required**: No anonymous access +- **Self-Service**: Users can only modify their own profiles + +## 🛠️ Advanced Configuration + +### Multi-Factor Authentication +Currently disabled for simplicity. To enable: + +```hcl +multi_factor_auth { + enabled = true + provider_configs { + phone { + enabled = true + } + } +} +``` + +### Custom Email Templates +For more advanced templates, you can use Firebase Admin SDK: + +```python +# In your Flask app +from firebase_admin import auth + +def send_custom_email(user_email, template_name): + # Custom email sending logic + pass +``` + +### Domain Restrictions +To restrict authentication to specific domains: + +```javascript +// In Firebase Auth security rules +rules_version = '2'; +service cloud.firestore { + match /users/{userId} { + allow read, write: if + request.auth != null && + request.auth.token.email.matches('.*@rothbardlaw\\.com$'); + } +} +``` + +## 📊 Monitoring and Analytics + +### Authentication Events +Track these events in your application: + +```python +# Log authentication events +def log_auth_event(event_type, user_id, details=None): + db.collection('auth_logs').add({ + 'event_type': event_type, + 'user_id': user_id, + 'timestamp': firestore.SERVER_TIMESTAMP, + 'details': details or {} + }) +``` + +### Key Events to Monitor +- User registrations +- Password resets +- Failed login attempts +- Email verifications + +## 🔄 Updates and Maintenance + +### Updating Email Templates +1. Edit template files in `terraform/templates/` +2. Run `terraform apply` to update +3. Changes apply to new emails immediately + +### Adding New Providers +1. Update `google_identitytoolkit_config` in `main.tf` +2. Add provider-specific variables +3. Configure OAuth credentials in Google Cloud +4. Apply Terraform changes + +### Security Rule Updates +1. Modify `google_firestore_security_policy` in `main.tf` +2. Test rules in Firebase Console first +3. Apply with Terraform + +## 🚨 Troubleshooting + +### Common Issues + +1. **Email Templates Not Working** + - Check template file paths + - Verify template syntax + - Check email provider settings + +2. **Authentication Provider Not Working** + - Verify API credentials + - Check provider configuration + - Review Firebase Console settings + +3. **Security Rules Blocking Access** + - Test rules in Firebase Console + - Check user authentication status + - Verify collection/document paths + +### Debug Commands + +```bash +# Check Firebase Auth configuration +gcloud auth troubleshoot + +# Test authentication flow +curl -X POST "https://identitytoolkit.googleapis.com/v1/accounts:signIn?key=YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"email":"user@example.com","password":"password","returnSecureToken":true}' + +# Check Firestore rules +gcloud firestore databases rules describe \ + --project=your-project-id +``` + +## 📚 Additional Resources + +- [Firebase Authentication Documentation](https://firebase.google.com/docs/auth) +- [Terraform Google Provider](https://registry.terraform.io/providers/hashicorp/google/latest/docs) +- [Firestore Security Rules](https://firebase.google.com/docs/firestore/security/get-started) +- [Firebase Email Templates](https://firebase.google.com/docs/auth/custom-email-templates) + +## 🎯 Best Practices + +1. **Security First** + - Use HTTPS everywhere + - Implement proper session management + - Regular security audits + +2. **User Experience** + - Clear error messages + - Professional email templates + - Mobile-responsive design + +3. **Maintenance** + - Regular backups + - Monitoring and alerts + - Documentation updates + +4. **Compliance** + - GDPR compliance for EU users + - Data retention policies + - Privacy policy alignment + +This automation ensures your Firebase Authentication is secure, professional, and maintainable while following industry best practices for legal client portals. \ No newline at end of file diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 0000000..d2cd4f9 --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,297 @@ +# Rothbard Law Group - Terraform Deployment + +This Terraform configuration deploys the Rothbard Law Group client portal to Google Cloud Platform with Firebase backend. + +## Architecture Overview + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Firebase App │ │ Firestore │ │ Cloud Run │ +│ (Client Auth) │◄──►│ (User Data) │◄──►│ (Flask App) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ Filevine API │ + │ (Case Data) │ + └─────────────────┘ +``` + +## Hosting Options + +### 1. Cloud Run (Recommended) +- **Best for production use** +- Container-based deployment +- Scales from 0 to N instances +- Cost-effective (pay-per-use) +- Full control over environment + +### 2. App Engine +- Platform-as-a-service +- Built-in scaling +- Slightly more restrictive +- Good for simpler deployments + +## Prerequisites + +1. **Google Cloud SDK** + ```bash + curl https://sdk.cloud.google.com | bash + gcloud init + ``` + +2. **Terraform** + ```bash + # macOS + brew install terraform + + # Linux + sudo apt-get install terraform + ``` + +3. **Docker** (for Cloud Run) + ```bash + # macOS + brew install docker + + # Linux + sudo apt-get install docker.io + ``` + +## Setup Instructions + +### 1. Configure Terraform Variables + +Copy the example variables file: +```bash +cp terraform.tfvars.example terraform.tfvars +``` + +Edit `terraform.tfvars` with your values: +```hcl +gcp_project_id = "your-gcp-project-id" +domain_name = "rothbard.yourdomain.com" +hosting_option = "cloud_run" # or "app_engine" +``` + +### 2. Set Environment Variables + +Create a `.env` file for sensitive data: +```bash +# Flask Configuration +FLASK_SECRET_KEY=your-super-secret-key + +# Firebase Configuration +FIREBASE_API_KEY=your-firebase-api-key +FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com +FIREBASE_PROJECT_ID=your-gcp-project-id +FIREBASE_APP_ID=your-firebase-app-id + +# Service Account (optional - can use gcloud auth) +FIREBASE_SERVICE_ACCOUNT_JSON='{"type":"service_account",...}' + +# Filevine API Configuration +FILEVINE_CLIENT_ID=your-filevine-client-id +FILEVINE_CLIENT_SECRET=your-filevine-client-secret +FILEVINE_PERSONAL_ACCESS_TOKEN=your-filevine-pat +FILEVINE_ORG_ID=your-filevine-org-id +FILEVINE_USER_ID=your-filevine-user-id +``` + +### 3. Deploy with Cloud Run (Recommended) + +#### Step 1: Build and Push Docker Image +```bash +# Set your project ID +export PROJECT_ID=your-gcp-project-id + +# Build the Docker image +docker build -t gcr.io/$PROJECT_ID/rothbard-portal:latest . + +# Push to Google Container Registry +docker push gcr.io/$PROJECT_ID/rothbard-portal:latest +``` + +#### Step 2: Deploy Infrastructure +```bash +cd terraform + +# Initialize Terraform +terraform init + +# Plan the deployment +terraform plan + +# Apply the changes +terraform apply +``` + +#### Step 3: Update Container Image Variable +After the first deployment, update your `terraform.tfvars`: +```hcl +container_image = "gcr.io/your-gcp-project-id/rothbard-portal:latest" +``` + +Then run `terraform apply` again. + +### 4. Deploy with App Engine + +#### Step 1: Create Source Package +```bash +# Copy app files and create zip +cp app.py requirements.txt templates/ app-engine/ +cd app-engine +zip -r ../app-source.zip . +cd .. +``` + +#### Step 2: Deploy Infrastructure +```bash +cd terraform + +# Set hosting option to app_engine +# Edit terraform.tfvars: hosting_option = "app_engine" + +# Initialize and apply +terraform init +terraform plan +terraform apply +``` + +## Post-Deployment Setup + +### 1. Firebase Configuration + +After deployment, you'll need to: + +1. **Configure Firebase Authentication**: + - Go to Firebase Console → Authentication + - Enable Email/Password provider + - Add your domain to authorized domains + +2. **Set up Firestore Rules**: + ```javascript + rules_version = '2'; + service cloud.firestore { + match /databases/{database}/documents { + match /users/{userId} { + allow read, write: if request.auth != null && request.auth.uid == userId; + } + } + } + ``` + +### 2. Enable Users + +Users need to be manually enabled in Firestore: + +```javascript +// In Firebase Console → Firestore Database +// Create document in collection 'users' with document ID = Firebase UID +{ + enabled: true, + caseEmail: "user@example.com", // This filters Filevine projects + name: "Client Name" +} +``` + +### 3. Custom Domain (Optional) + +For Cloud Run with custom domain: + +```bash +# Map your domain to the Cloud Run service +gcloud run services add-iam-policy-binding rothbard-portal-service \ + --member="allUsers" \ + --role="roles/run.invoker" + +# Set up domain mapping +gcloud run domain-mappings create \ + --domain=rothbard.yourdomain.com \ + --service=rothbard-portal-service +``` + +## Monitoring and Maintenance + +### View Logs +```bash +# Cloud Run logs +gcloud logs read "resource.type=cloud_run_revision" --limit 50 + +# App Engine logs +gcloud app logs tail -s default +``` + +### Scale Configuration + +For Cloud Run, you can adjust scaling in `terraform/modules/cloud_run/main.tf`: +```hcl +metadata { + annotations = { + "autoscaling.knative.dev/maxScale" = "10" # Max instances + "autoscaling.knative.dev/minScale" = "1" # Min instances + } +} +``` + +### Updates + +To update the application: + +1. **Cloud Run**: Build and push new Docker image, then update the `container_image` variable +2. **App Engine**: Update source code, create new zip, and redeploy + +## Costs + +### Cloud Run (Recommended) +- **Free tier**: 180,000 vCPU-seconds, 360,000 GiB-seconds memory per month +- **Beyond free tier**: ~$0.000024 per vCPU-second, $0.0000025 per GiB-second +- **Typical cost**: $5-20/month for low traffic client portal + +### App Engine +- **Free tier**: 28 instance-hours per day +- **Beyond free tier**: ~$0.05 per instance-hour +- **Typical cost**: $10-50/month depending on traffic + +### Firebase +- **Spark plan**: Free for most use cases +- **Blaze plan**: Pay-as-you-go for high usage +- **Typical cost**: $0-25/month depending on authentication usage + +## Security Considerations + +1. **Service Account**: Minimal permissions granted (Firestore and Firebase Admin) +2. **Secret Management**: Sensitive data stored in Secret Manager +3. **HTTPS**: All traffic encrypted with TLS +4. **Authentication**: Firebase ID tokens verified server-side +5. **Access Control**: User access controlled through Firestore profiles + +## Troubleshooting + +### Common Issues + +1. **Build failures**: Check Dockerfile and requirements.txt +2. **Permission errors**: Verify service account has correct IAM roles +3. **Firebase connection**: Ensure service account key is properly formatted +4. **CORS errors**: Configure Firebase Auth domain correctly + +### Debug Commands + +```bash +# Check Cloud Run service status +gcloud run services describe rothbard-portal-service + +# Test service locally +docker run -p 5000:5000 gcr.io/$PROJECT_ID/rothbard-portal:latest + +# Check Terraform state +terraform show +``` + +## Support + +For issues with: +- **Terraform deployment**: Check Terraform logs and state +- **Cloud Run**: View Cloud Run logs in Google Console +- **Firebase**: Check Firebase Console for configuration issues +- **Application**: Review Flask application logs \ No newline at end of file diff --git a/terraform/app.yaml b/terraform/app.yaml new file mode 100644 index 0000000..7cf3513 --- /dev/null +++ b/terraform/app.yaml @@ -0,0 +1,29 @@ +# App Engine configuration file +runtime: python311 + +instance_class: F1 +automatic_scaling: + min_idle_instances: automatic + max_idle_instances: 1 + min_pending_latency: automatic + max_pending_latency: automatic + +# Environment variables +env_variables: + FLASK_ENV: production + PORT: 8080 + +# Handlers +handlers: +- url: /.* + script: auto + secure: always + +# Health check +health_check: + enable_health_check: true + check_path: "/" + +# Runtime configuration +runtime_config: + python_version: 3 \ No newline at end of file diff --git a/terraform/deploy.sh b/terraform/deploy.sh new file mode 100755 index 0000000..3686313 --- /dev/null +++ b/terraform/deploy.sh @@ -0,0 +1,229 @@ +#!/bin/bash + +# Rothbard Law Group Deployment Script +# This script automates the deployment process for Cloud Run + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +PROJECT_ID="" +DOMAIN_NAME="" +HOSTING_OPTION="cloud_run" + +# Helper functions +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check prerequisites +check_prerequisites() { + log_info "Checking prerequisites..." + + # Check gcloud + if ! command -v gcloud &> /dev/null; then + log_error "gcloud is not installed. Please install Google Cloud SDK." + exit 1 + fi + + # Check terraform + if ! command -v terraform &> /dev/null; then + log_error "terraform is not installed. Please install Terraform." + exit 1 + fi + + # Check docker (for Cloud Run) + if [ "$HOSTING_OPTION" = "cloud_run" ] && ! command -v docker &> /dev/null; then + log_error "docker is not installed. Please install Docker for Cloud Run deployment." + exit 1 + fi + + log_info "Prerequisites check passed!" +} + +# Setup project +setup_project() { + log_info "Setting up Google Cloud project..." + + if [ -z "$PROJECT_ID" ]; then + read -p "Enter your GCP Project ID: " PROJECT_ID + fi + + # Set the project + gcloud config set project "$PROJECT_ID" + + # Enable required APIs + log_info "Enabling required APIs..." + gcloud services enable run.googleapis.com + gcloud services enable cloudbuild.googleapis.com + gcloud services enable firestore.googleapis.com + gcloud services enable firebase.googleapis.com + gcloud services enable secretmanager.googleapis.com + + log_info "Project setup completed!" +} + +# Build and push Docker image (Cloud Run) +build_and_push_image() { + if [ "$HOSTING_OPTION" != "cloud_run" ]; then + return + fi + + log_info "Building Docker image..." + + # Build the image + docker build -t gcr.io/$PROJECT_ID/rothbard-portal:latest . + + log_info "Pushing Docker image to Google Container Registry..." + + # Configure Docker to use gcloud as a credential helper + gcloud auth configure-docker + + # Push the image + docker push gcr.io/$PROJECT_ID/rothbard-portal:latest + + log_info "Docker image pushed successfully!" +} + +# Deploy infrastructure with Terraform +deploy_infrastructure() { + log_info "Deploying infrastructure with Terraform..." + + cd terraform + + # Create terraform.tfvars if it doesn't exist + if [ ! -f "terraform.tfvars" ]; then + log_warn "terraform.tfvars not found. Creating from example..." + cp terraform.tfvars.example terraform.tfvars + + # Update with project ID + sed -i "s/your-gcp-project-id/$PROJECT_ID/g" terraform.tfvars + + if [ -n "$DOMAIN_NAME" ]; then + sed -i "s/rothbard-portal.example.com/$DOMAIN_NAME/g" terraform.tfvars + fi + + log_warn "Please edit terraform/terraform.tfvars with your specific configuration before continuing." + read -p "Press Enter to continue after editing..." + fi + + # Initialize Terraform + terraform init + + # Plan deployment + log_info "Planning Terraform deployment..." + terraform plan + + # Apply deployment + log_info "Applying Terraform configuration..." + terraform apply -auto-approve + + # Get the output URL + APP_URL=$(terraform output -raw application_url) + + cd .. + + log_info "Infrastructure deployed successfully!" + log_info "Application URL: $APP_URL" +} + +# Create service account key for local development +create_service_account_key() { + log_info "Creating service account key for development..." + + SA_NAME="rothbard-flask-app" + SA_EMAIL="$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" + + # Create service account if it doesn't exist + if ! gcloud iam service-accounts describe "$SA_EMAIL" &> /dev/null; then + gcloud iam service-accounts create "$SA_NAME" \ + --display-name="Rothbard Flask App Service Account" + fi + + # Grant necessary roles + gcloud projects add-iam-policy-binding "$PROJECT_ID" \ + --member="serviceAccount:$SA_EMAIL" \ + --role="roles/datastore.user" + + gcloud projects add-iam-policy-binding "$PROJECT_ID" \ + --member="serviceAccount:$SA_EMAIL" \ + --role="roles/firebase.admin" + + # Create key + gcloud iam service-accounts keys create ~/rothbard-service-account.json \ + --iam-account="$SA_EMAIL" + + log_info "Service account key created at ~/rothbard-service-account.json" + log_warn "Add this to your environment: export GOOGLE_APPLICATION_CREDENTIALS=~/rothbard-service-account.json" +} + +# Main deployment function +main() { + log_info "Starting Rothbard Law Group deployment..." + + # Parse command line arguments + while [[ $# -gt 0 ]]; do + case $1 in + -p|--project) + PROJECT_ID="$2" + shift 2 + ;; + -d|--domain) + DOMAIN_NAME="$2" + shift 2 + ;; + -o|--option) + HOSTING_OPTION="$2" + shift 2 + ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "Options:" + echo " -p, --project PROJECT_ID GCP Project ID" + echo " -d, --domain DOMAIN_NAME Domain name (optional)" + echo " -o, --option HOSTING_OPTION Hosting option (cloud_run or app_engine)" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + log_error "Unknown option: $1" + exit 1 + ;; + esac + done + + # Validate hosting option + if [[ ! "$HOSTING_OPTION" =~ ^(cloud_run|app_engine)$ ]]; then + log_error "Invalid hosting option. Use 'cloud_run' or 'app_engine'." + exit 1 + fi + + check_prerequisites + setup_project + build_and_push_image + deploy_infrastructure + create_service_account_key + + log_info "Deployment completed successfully!" + log_info "Next steps:" + log_info "1. Configure Firebase Authentication in the Firebase Console" + log_info "2. Set up Firestore security rules" + log_info "3. Enable user accounts in Firestore" + log_info "4. Configure your custom domain (if applicable)" +} + +# Run main function +main "$@" \ No newline at end of file diff --git a/terraform/deployment.tf b/terraform/deployment.tf new file mode 100644 index 0000000..67123ba --- /dev/null +++ b/terraform/deployment.tf @@ -0,0 +1,99 @@ +variable "hosting_option" { + description = "Hosting option for the Flask app" + type = string + default = "cloud_run" + validation { + condition = contains(["cloud_run", "app_engine"], var.hosting_option) + error_message = "The hosting_option must be one of: cloud_run, app_engine." + } +} + +# Select hosting option based on variable +module "hosting" { + source = "./modules/${var.hosting_option}" + + # Common variables + app_name = "rothbard-portal" + gcp_project_id = var.gcp_project_id + gcp_region = var.gcp_region + firebase_project_id = google_firebase_project.default.project + flask_secret_key = var.flask_secret_key + service_account_email = google_service_account.flask_app.email + service_account_key_data = var.service_account_key_data + + # Filevine credentials + filevine_client_id = var.filevine_client_id + filevine_client_secret = var.filevine_client_secret + filevine_pat = var.filevine_pat + filevine_org_id = var.filevine_org_id + filevine_user_id = var.filevine_user_id + + # Module-specific variables + container_image = var.hosting_option == "cloud_run" ? var.container_image : null + app_source_zip_path = var.hosting_option == "app_engine" ? var.app_source_zip_path : null +} + +# Additional variables for hosting options +variable "flask_secret_key" { + description = "Flask secret key" + type = string + sensitive = true +} + +variable "service_account_key_data" { + description = "Service account key JSON data" + type = string + sensitive = true +} + +variable "container_image" { + description = "Docker image for Cloud Run deployment" + type = string + default = "gcr.io/your-project/rothbard-portal:latest" +} + +variable "app_source_zip_path" { + description = "Path to App Engine source zip" + type = string + default = "./app-source.zip" +} + + +# Filevine credentials +variable "filevine_client_id" { + description = "Filevine client ID" + type = string + sensitive = true +} + +variable "filevine_client_secret" { + description = "Filevine client secret" + type = string + sensitive = true +} + +variable "filevine_pat" { + description = "Filevine personal access token" + type = string + sensitive = true +} + +variable "filevine_org_id" { + description = "Filevine organization ID" + type = string + sensitive = true +} + +variable "filevine_user_id" { + description = "Filevine user ID" + type = string + sensitive = true +} + +# Output hosting-specific URLs +output "application_url" { + description = "URL of the deployed application" + value = var.hosting_option == "cloud_run" ? module.hosting.service_url : + var.hosting_option == "app_engine" ? module.hosting.app_url : + null +} \ No newline at end of file diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..610eca4 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,241 @@ +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = "~> 5.0" + } + firebase = { + source = "terraform-google-modules/firebase/google" + version = "~> 13.0" + } + } + + required_version = ">= 1.0" +} + +provider "google" { + project = var.gcp_project_id + region = var.gcp_region +} + +provider "firebase" { + project = var.gcp_project_id +} + +# Firebase Project Setup +resource "google_firebase_project" "default" { + provider = google-beta + project = var.gcp_project_id +} + +# Firebase Web App +resource "google_firebase_web_app" "rothbard_portal" { + provider = google-beta + project = google_firebase_project.default.project + display_name = "Rothbard Client Portal" + + app_url = "https://${var.domain_name}" + + # Handle OAuth redirect + oauth_config { + client_id = var.oauth_client_id + client_secret = var.oauth_client_secret + } +} + +# Firestore Database +resource "google_firestore_database" "default" { + provider = google-beta + project = var.gcp_project_id + name = "(default)" + location_id = var.firestore_location + type = "FIRESTORE_NATIVE" + + delete_protection_state = "DISABLED" +} + +# Firebase Authentication - Complete Configuration +resource "google_identitytoolkit_config" "default" { + provider = google-beta + project = var.gcp_project_id + + sign_in_options { + email { + enabled = true + password_required = true + } + + # Disable other providers for security + phone { + enabled = false + } + + google { + enabled = var.enable_google_signin + } + + facebook { + enabled = false + } + + apple { + enabled = false + } + } + + # Email configuration + email { + reset_password_template { + from_email_address = var.auth_from_email + from_display_name = var.auth_from_name + reply_to = var.auth_reply_to + subject = "Reset your Rothbard Law Group password" + html = file("${path.module}/templates/reset_password.html") + text = file("${path.module}/templates/reset_password.txt") + } + + email_verification_template { + from_email_address = var.auth_from_email + from_display_name = var.auth_from_name + reply_to = var.auth_reply_to + subject = "Verify your Rothbard Law Group account" + html = file("${path.module}/templates/email_verification.html") + text = file("${path.module}/templates/email_verification.txt") + } + } + + # Security settings + sign_in { + allow_duplicate_emails = false + } + + # Multi-factor authentication (disabled for simplicity) + multi_factor_auth { + enabled = false + } + + # Anonymous user access (disabled) + anonymous { + enabled = false + } +} + +# Service Account for the Flask App +resource "google_service_account" "flask_app" { + account_id = "rothbard-flask-app" + display_name = "Rothbard Flask App Service Account" +} + +# IAM permissions for the Flask App +resource "google_project_iam_member" "firestore_access" { + project = var.gcp_project_id + role = "roles/datastore.user" + member = "serviceAccount:${google_service_account.flask_app.email}" +} + +resource "google_project_iam_member" "firebase_admin" { + project = var.gcp_project_id + role = "roles/firebase.admin" + member = "serviceAccount:${google_service_account.flask_app.email}" +} + +# Firestore Security Rules +resource "google_firestore_security_policy" "default" { + project = var.gcp_project_id + policy = { + rules = [ + { + description = "Allow users to read/write their own profile" + match = { + collection = "users" + document = "{userId}" + } + allow = [ + { + resource = "read" + condition = { + name = "request.auth.uid == userId" + } + }, + { + resource = "write" + condition = { + name = "request.auth.uid == userId" + } + } + ] + }, + { + description = "Only authenticated users can access the database" + match = { + collection = "{collection=**}" + } + allow = [ + { + resource = "read" + condition = { + name = "request.auth != null" + } + }, + { + resource = "write" + condition = { + name = "request.auth != null" + } + } + ] + } + ] + } +} + +# Firebase Hosting (optional - for static assets) +resource "google_firebase_hosting_site" "default" { + provider = google-beta + project = var.gcp_project_id + site_id = "rothbard-portal" + + # Default configuration for hosting + config { + public_root_dir = "public" + headers = [ + { + headers = ["Cache-Control: public, max-age=31536000"] + glob = "**/*.@(jpg|jpeg|gif|png|svg|webp)" + }, + { + headers = ["Cache-Control: public, max-age=86400"] + glob = "**/*.@(css|js)" + } + ] + redirects = [ + { + status_code = 302 + path = "/login" + location = "/login.html" + } + ] + rewrites = [ + { + glob = "**" + path = "/index.html" + } + ] + } +} + +# Output important values +output "firebase_web_app_id" { + description = "Firebase Web App ID" + value = google_firebase_web_app.rothbard_portal.app_id +} + +output "firebase_project_id" { + description = "Firebase Project ID" + value = google_firebase_project.default.project +} + +output "service_account_email" { + description = "Service account email for Flask app" + value = google_service_account.flask_app.email +} \ No newline at end of file diff --git a/terraform/modules/app_engine/main.tf b/terraform/modules/app_engine/main.tf new file mode 100644 index 0000000..a86a907 --- /dev/null +++ b/terraform/modules/app_engine/main.tf @@ -0,0 +1,118 @@ +# Enable App Engine Admin API +resource "google_project_service" "appengine" { + project = var.gcp_project_id + service = "appengine.googleapis.com" +} + +# App Engine Application +resource "google_app_engine_application" "app" { + project = var.gcp_project_id + location_id = var.gcp_region + depends_on = [google_project_service.appengine] +} + +# App Engine Service for Flask app +resource "google_app_engine_standard_app_version" "flask_app" { + project = var.gcp_project_id + service = "default" + version_id = "${var.app_name}-v1" + + runtime = "python311" + + entrypoint { + command = "gunicorn -b :$PORT app:app" + } + + deployment { + zip { + source_url = google_storage_bucket_object.app_source_zip.output_uri + } + } + + env_variables = { + FLASK_SECRET_KEY = var.flask_secret_key + FIREBASE_PROJECT_ID = var.firebase_project_id + GOOGLE_APPLICATION_CREDENTIALS = "/etc/secrets/service-account.json" + FILEVINE_CLIENT_ID = var.filevine_client_id + FILEVINE_CLIENT_SECRET = var.filevine_client_secret + FILEVINE_PERSONAL_ACCESS_TOKEN = var.filevine_pat + FILEVINE_ORG_ID = var.filevine_org_id + FILEVINE_USER_ID = var.filevine_user_id + } + + # Service account + service_account = var.service_account_email + + # Resources + resources { + cpu = 1 + memory_gb = 0.5 + disk_gb = 0.5 + } + + # Automatic scaling + automatic_scaling { + min_idle_instances = 0 + max_idle_instances = 1 + min_pending_latency = "automatic" + max_pending_latency = "automatic" + max_concurrent_requests = 80 + } + + # Health check + health_check { + enable_health_check = true + check_path = "/" + } + + depends_on = [ + google_storage_bucket_object.app_source_zip, + google_secret_manager_secret_version.service_account_key + ] +} + +# Make App Engine service publicly accessible +resource "google_app_engine_firewall_rule" "allow_all" { + project = var.gcp_project_id + action = "ALLOW" + priority = "1" + + source_range = "*" +} + +# Cloud Storage bucket for app source code +resource "google_storage_bucket" "app_source" { + name = "${var.app_name}-source-${var.gcp_project_id}" + location = var.gcp_region + force_destroy = true + + uniform_bucket_level_access = true +} + +# Upload app source code +resource "google_storage_bucket_object" "app_source_zip" { + name = "app-source.zip" + bucket = google_storage_bucket.app_source.name + source = var.app_source_zip_path +} + +# Store service account key in Secret Manager +resource "google_secret_manager_secret" "service_account_key" { + project = var.gcp_project_id + secret_id = "${var.app_name}-service-account-key" + + replication { + automatic = true + } +} + +resource "google_secret_manager_secret_version" "service_account_key" { + secret = google_secret_manager_secret.service_account_key.id + secret_data = var.service_account_key_data +} + +# Output the app URL +output "app_url" { + description = "App Engine application URL" + value = "https://${google_app_engine_application.app.default_hostname}" +} \ No newline at end of file diff --git a/terraform/modules/app_engine/variables.tf b/terraform/modules/app_engine/variables.tf new file mode 100644 index 0000000..9c758de --- /dev/null +++ b/terraform/modules/app_engine/variables.tf @@ -0,0 +1,71 @@ +variable "app_name" { + description = "Name of the application" + type = string +} + +variable "gcp_project_id" { + description = "GCP Project ID" + type = string +} + +variable "gcp_region" { + description = "GCP region" + type = string +} + +variable "app_source_zip_path" { + description = "Path to the app source code zip file" + type = string +} + +variable "firebase_project_id" { + description = "Firebase project ID" + type = string +} + +variable "flask_secret_key" { + description = "Flask secret key" + type = string + sensitive = true +} + +variable "service_account_email" { + description = "Service account email for the App Engine service" + type = string +} + +variable "service_account_key_data" { + description = "Service account key JSON data" + type = string + sensitive = true +} + +variable "filevine_client_id" { + description = "Filevine client ID" + type = string + sensitive = true +} + +variable "filevine_client_secret" { + description = "Filevine client secret" + type = string + sensitive = true +} + +variable "filevine_pat" { + description = "Filevine personal access token" + type = string + sensitive = true +} + +variable "filevine_org_id" { + description = "Filevine organization ID" + type = string + sensitive = true +} + +variable "filevine_user_id" { + description = "Filevine user ID" + type = string + sensitive = true +} \ No newline at end of file diff --git a/terraform/modules/cloud_run/main.tf b/terraform/modules/cloud_run/main.tf new file mode 100644 index 0000000..0cefd56 --- /dev/null +++ b/terraform/modules/cloud_run/main.tf @@ -0,0 +1,144 @@ +# Cloud Run Service for Flask App +resource "google_cloud_run_service" "flask_app" { + name = "${var.app_name}-service" + location = var.gcp_region + + template { + spec { + containers { + image = var.container_image + + # Environment variables for the Flask app + env { + name = "FLASK_SECRET_KEY" + value = var.flask_secret_key + } + + env { + name = "FIREBASE_PROJECT_ID" + value = var.firebase_project_id + } + + env { + name = "GOOGLE_APPLICATION_CREDENTIALS" + value = "/etc/secrets/service-account.json" + } + + # Filevine API credentials + env { + name = "FILEVINE_CLIENT_ID" + value = var.filevine_client_id + } + + env { + name = "FILEVINE_CLIENT_SECRET" + value = var.filevine_client_secret + } + + env { + name = "FILEVINE_PERSONAL_ACCESS_TOKEN" + value = var.filevine_pat + } + + env { + name = "FILEVINE_ORG_ID" + value = var.filevine_org_id + } + + env { + name = "FILEVINE_USER_ID" + value = var.filevine_user_id + } + + # Memory and CPU limits + resources { + limits = { + cpu = "1000m" + memory = "512Mi" + } + } + + # Mount service account key + volume_mount { + name = "service-account-key" + mount_path = "/etc/secrets" + read_only = true + } + } + + # Service account for the container + service_account_name = var.service_account_email + + # Volumes + volumes { + name = "service-account-key" + secret { + secret_name = google_secret_manager_secret.service_account_key.secret_id + items { + key = "latest" + path = "service-account.json" + } + } + } + + # Allow unauthenticated access + container_concurrency = 100 + timeout_seconds = 300 + } + + # Traffic settings + metadata { + annotations = { + "autoscaling.knative.dev/maxScale" = "10" + "autoscaling.knative.dev/minScale" = "1" + "run.googleapis.com/ingress" = "all" + } + } + } + + traffic { + percent = 100 + latest_revision = true + } + + depends_on = [google_secret_manager_secret_version.service_account_key] +} + +# Make Cloud Run service publicly accessible +resource "google_cloud_run_service_iam_member" "public" { + location = google_cloud_run_service.flask_app.location + project = google_cloud_run_service.flask_app.project + service = google_cloud_run_service.flask_app.name + role = "roles/run.invoker" + member = "allUsers" +} + +# Store service account key in Secret Manager +resource "google_secret_manager_secret" "service_account_key" { + project = var.gcp_project_id + secret_id = "${var.app_name}-service-account-key" + + replication { + automatic = true + } +} + +resource "google_secret_manager_secret_version" "service_account_key" { + secret = google_secret_manager_secret.service_account_key.id + secret_data = var.service_account_key_data +} + +# Cloud Storage bucket for container storage (if needed) +resource "google_storage_bucket" "app_storage" { + name = "${var.app_name}-storage-${var.gcp_project_id}" + location = var.gcp_region + force_destroy = true + + uniform_bucket_level_access = true +} + +# Output the service URL +output "service_url" { + description = "Cloud Run service URL" + value = google_cloud_run_service.flask_app.status[0].url +} \ No newline at end of file diff --git a/terraform/modules/cloud_run/variables.tf b/terraform/modules/cloud_run/variables.tf new file mode 100644 index 0000000..e202369 --- /dev/null +++ b/terraform/modules/cloud_run/variables.tf @@ -0,0 +1,71 @@ +variable "app_name" { + description = "Name of the application" + type = string +} + +variable "gcp_project_id" { + description = "GCP Project ID" + type = string +} + +variable "gcp_region" { + description = "GCP region" + type = string +} + +variable "container_image" { + description = "Docker image for the Flask app" + type = string +} + +variable "firebase_project_id" { + description = "Firebase project ID" + type = string +} + +variable "flask_secret_key" { + description = "Flask secret key" + type = string + sensitive = true +} + +variable "service_account_email" { + description = "Service account email for the Cloud Run service" + type = string +} + +variable "service_account_key_data" { + description = "Service account key JSON data" + type = string + sensitive = true +} + +variable "filevine_client_id" { + description = "Filevine client ID" + type = string + sensitive = true +} + +variable "filevine_client_secret" { + description = "Filevine client secret" + type = string + sensitive = true +} + +variable "filevine_pat" { + description = "Filevine personal access token" + type = string + sensitive = true +} + +variable "filevine_org_id" { + description = "Filevine organization ID" + type = string + sensitive = true +} + +variable "filevine_user_id" { + description = "Filevine user ID" + type = string + sensitive = true +} \ No newline at end of file diff --git a/terraform/templates/email_verification.html b/terraform/templates/email_verification.html new file mode 100644 index 0000000..00a628b --- /dev/null +++ b/terraform/templates/email_verification.html @@ -0,0 +1,95 @@ + + + + + + Verify Your Email - Rothbard Law Group + + + +
+

Rothbard Law Group

+

Client Portal

+
+ +
+

Welcome to the Rothbard Law Group Client Portal

+ +
+

Thank you for signing up!

+

You're just one step away from accessing your case information securely.

+
+ +

Please verify your email address to complete your registration:

+ +

Verify Email Address

+ +

Or copy and paste this link into your browser:

+

{{ verificationLink }}

+ +

What happens next?

+ + +

Important:

+ + +

If you have any questions or need assistance, please contact our support team.

+
+ + + + \ No newline at end of file diff --git a/terraform/templates/email_verification.txt b/terraform/templates/email_verification.txt new file mode 100644 index 0000000..ada70c7 --- /dev/null +++ b/terraform/templates/email_verification.txt @@ -0,0 +1,31 @@ +Rothbard Law Group - Email Verification + +Welcome to Rothbard Law Group Client Portal! + +Thank you for signing up for our client portal. You're just one step away from accessing your case information securely. + +Please verify your email address to complete your registration: +{{ verificationLink }} + +If the button doesn't work, you can copy and paste this link into your browser. + +What happens next? +- Once verified, you'll have full access to the client portal +- You can view your case information and documents +- Communicate securely with our legal team + +Important Information: +- This verification link will expire in 24 hours +- If you didn't create an account, please ignore this email +- Your account information is kept secure and confidential + +If you have any questions or need assistance, please contact our support team. + +Thank you for choosing Rothbard Law Group. + +--- +Rothbard Law Group +123 Legal Street +City, State 12345 +(555) 123-4567 +support@rothbardlaw.com \ No newline at end of file diff --git a/terraform/templates/reset_password.html b/terraform/templates/reset_password.html new file mode 100644 index 0000000..b362e9d --- /dev/null +++ b/terraform/templates/reset_password.html @@ -0,0 +1,78 @@ + + + + + + Reset Your Password - Rothbard Law Group + + + +
+

Rothbard Law Group

+

Client Portal

+
+ +
+

Reset Your Password

+ +

Hello,

+ +

We received a request to reset the password for your Rothbard Law Group client portal account. Click the button below to reset your password:

+ +

Reset Password

+ +

Or copy and paste this link into your browser:

+

{{ resetLink }}

+ +

Important:

+ + +

If you have any questions or need assistance, please contact our support team.

+
+ + + + \ No newline at end of file diff --git a/terraform/templates/reset_password.txt b/terraform/templates/reset_password.txt new file mode 100644 index 0000000..54c362f --- /dev/null +++ b/terraform/templates/reset_password.txt @@ -0,0 +1,27 @@ +Rothbard Law Group - Password Reset + +Hello, + +We received a request to reset your password for your Rothbard Law Group client portal account. + +To reset your password, please visit this link: +{{ resetLink }} + +If the button doesn't work, you can copy and paste the link into your browser. + +Important Information: +- This link will expire in 24 hours +- If you didn't request a password reset, please ignore this email +- For security reasons, never share this link with anyone + +If you have any questions or need assistance, please contact our support team. + +Thank you, +Rothbard Law Group + +--- +Rothbard Law Group +123 Legal Street +City, State 12345 +(555) 123-4567 +support@rothbardlaw.com \ No newline at end of file diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example new file mode 100644 index 0000000..5dce2d0 --- /dev/null +++ b/terraform/terraform.tfvars.example @@ -0,0 +1,9 @@ +# Copy this file to terraform.tfvars and fill in your values +gcp_project_id = "your-gcp-project-id" +domain_name = "rothbard.yourdomain.com" + +# Optional: Override defaults +# gcp_region = "us-central1" +# firestore_location = "us-central1" +# hosting_option = "cloud_run" # Options: cloud_run, app_engine, gcs +# environment = "production" \ No newline at end of file diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..fc13bb3 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,76 @@ +variable "gcp_project_id" { + description = "GCP Project ID for the deployment" + type = string +} + +variable "gcp_region" { + description = "GCP region for resources" + type = string + default = "us-central1" +} + +variable "firestore_location" { + description = "Location for Firestore database" + type = string + default = "us-central1" +} + +variable "domain_name" { + description = "Domain name for the application" + type = string + default = "rothbard-portal.example.com" +} + +variable "oauth_client_id" { + description = "OAuth client ID for Firebase" + type = string + default = "" +} + +variable "oauth_client_secret" { + description = "OAuth client secret for Firebase" + type = string + default = "" + sensitive = true +} + +variable "hosting_option" { + description = "Hosting option for the Flask app" + type = string + default = "cloud_run" + validation { + condition = contains(["cloud_run", "app_engine"], var.hosting_option) + error_message = "The hosting_option must be one of: cloud_run, app_engine." + } +} + +variable "environment" { + description = "Environment tag" + type = string + default = "production" +} + +# Firebase Authentication Configuration +variable "enable_google_signin" { + description = "Enable Google Sign-In authentication provider" + type = bool + default = false +} + +variable "auth_from_email" { + description = "From email address for authentication emails" + type = string + default = "noreply@rothbardlaw.com" +} + +variable "auth_from_name" { + description = "From display name for authentication emails" + type = string + default = "Rothbard Law Group" +} + +variable "auth_reply_to" { + description = "Reply-to email address for authentication emails" + type = string + default = "support@rothbardlaw.com" +} \ No newline at end of file