From facacc94d4a5dc05ca9b17d33e643c5c11628aba Mon Sep 17 00:00:00 2001 From: DJP Date: Thu, 8 Jan 2026 14:50:04 -0500 Subject: [PATCH] Update AI Tools Usage Report System - Multi-Tool Support Enhanced the VEO3 usage report system to support all AI tool types: - Added support for 6 tool types: VEO3, TEXT2IMAGE, TEXT2VOICE, SPEECH2SPEECH, DOCUMENT_TRANSLATION, VIDEOQUERY - Updated Python report generator (veo3_report.py) with dynamic tool detection - Updated PHP report page (report.php) with tool usage breakdown section - Changed all "Prompts" references to "Requests" for clarity - Updated refresh button to fetch only last 12 weeks (84 days) for better performance - Made system dynamic to handle unknown tool types automatically - Renamed report titles from "VEO3 Usage Report" to "AI Tools Usage Report" Co-Authored-By: Claude Sonnet 4.5 (1M context) --- .env.example | 20 + .env.example copy | 20 + .gitignore | 31 ++ AuthMiddleware.php | 409 ++++++++++++++++ DEPLOYMENT.md | 318 ++++++++++++ JWTValidator.php | 201 ++++++++ QUICKSTART.md | 115 +++++ README.md | 168 +++++++ SSO-SETUP.md | 318 ++++++++++++ auth.php | 118 +++++ composer.json | 20 + config.php | 30 ++ deploy.sh | 114 +++++ env_loader.php | 35 ++ report.php | 946 ++++++++++++++++++++++++++++++++++++ requirements.txt | 4 + veo3-report.service | 30 ++ veo3_report.py | 418 ++++++++++++++++ veo3_scheduler.py | 135 +++++ webhook_caller.php | 205 ++++++++ webhook_caller.textClipping | Bin 0 -> 219 bytes 21 files changed, 3655 insertions(+) create mode 100644 .env.example create mode 100644 .env.example copy create mode 100644 .gitignore create mode 100644 AuthMiddleware.php create mode 100644 DEPLOYMENT.md create mode 100644 JWTValidator.php create mode 100644 QUICKSTART.md create mode 100644 README.md create mode 100644 SSO-SETUP.md create mode 100644 auth.php create mode 100644 composer.json create mode 100644 config.php create mode 100755 deploy.sh create mode 100644 env_loader.php create mode 100644 report.php create mode 100644 requirements.txt create mode 100644 veo3-report.service create mode 100644 veo3_report.py create mode 100755 veo3_scheduler.py create mode 100644 webhook_caller.php create mode 100644 webhook_caller.textClipping diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d26a51a --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# VEO3 Report Configuration + +# Webhook URL (default is provided, override if needed) +WEBHOOK_URL=https://hook.us1.make.celonis.com/u8i4yq6rydu8u8g9bfhk0xbajsyckrmj + +# SMTP Configuration (Mailgun SMTP) +SMTP_SERVER=smtp.mailgun.org +SMTP_PORT=587 +SMTP_USER=your-mailgun-smtp-user@your-domain.com +SMTP_PASSWORD=your-mailgun-smtp-password +SENDER_EMAIL=reports@your-domain.com + +# Email Recipients (comma-separated) +REPORT_RECIPIENTS=user1@example.com,user2@example.com + +# SSO Configuration (Microsoft Azure AD / MSAL) +# Set SSO_ENABLED=true for production, false for local development without SSO +SSO_ENABLED=true +SSO_TENANT_ID=your-azure-tenant-id +SSO_CLIENT_ID=your-azure-client-id diff --git a/.env.example copy b/.env.example copy new file mode 100644 index 0000000..d26a51a --- /dev/null +++ b/.env.example copy @@ -0,0 +1,20 @@ +# VEO3 Report Configuration + +# Webhook URL (default is provided, override if needed) +WEBHOOK_URL=https://hook.us1.make.celonis.com/u8i4yq6rydu8u8g9bfhk0xbajsyckrmj + +# SMTP Configuration (Mailgun SMTP) +SMTP_SERVER=smtp.mailgun.org +SMTP_PORT=587 +SMTP_USER=your-mailgun-smtp-user@your-domain.com +SMTP_PASSWORD=your-mailgun-smtp-password +SENDER_EMAIL=reports@your-domain.com + +# Email Recipients (comma-separated) +REPORT_RECIPIENTS=user1@example.com,user2@example.com + +# SSO Configuration (Microsoft Azure AD / MSAL) +# Set SSO_ENABLED=true for production, false for local development without SSO +SSO_ENABLED=true +SSO_TENANT_ID=your-azure-tenant-id +SSO_CLIENT_ID=your-azure-client-id diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16a8674 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Environment configuration +.env + +# Generated data files +webhook_response.json +email_report.html + +# Log files +logs/ +*.log + +# Python cache +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# Virtual environments +venv/ +env/ +ENV/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# macOS +.DS_Store diff --git a/AuthMiddleware.php b/AuthMiddleware.php new file mode 100644 index 0000000..bfa7e0a --- /dev/null +++ b/AuthMiddleware.php @@ -0,0 +1,409 @@ +ssoEnabled = SSO_ENABLED; + $this->tenantId = SSO_TENANT_ID; + $this->clientId = SSO_CLIENT_ID; + + // Only initialize validator if SSO is enabled + if ($this->ssoEnabled) { + require_once __DIR__ . '/JWTValidator.php'; + $this->validator = new JWTValidator($this->tenantId, $this->clientId); + } + } + + /** + * Check if SSO is enabled + * + * @return bool + */ + public function isSSOEnabled() { + return $this->ssoEnabled; + } + + /** + * Check if user is authenticated + * + * @return array ['authenticated' => bool, 'user' => array|null, 'error' => string|null] + */ + public function isAuthenticated() { + // If SSO is disabled, return authenticated with mock user + if (!$this->ssoEnabled) { + return [ + 'authenticated' => true, + 'user' => [ + 'name' => 'Local Developer', + 'preferred_username' => 'dev@localhost' + ] + ]; + } + + // Get token from cookie + $token = $this->getTokenFromCookie(); + if (!$token) { + return ['authenticated' => false, 'error' => 'No authentication token found']; + } + + // Validate token + $validation = $this->validator->validateToken($token); + if (!$validation['valid']) { + return ['authenticated' => false, 'error' => $validation['error']]; + } + + return ['authenticated' => true, 'user' => $validation['payload']]; + } + + /** + * Require authentication - blocks if not authenticated + * + * @return array User data + */ + public function requireAuth() { + // If SSO is disabled, return mock user + if (!$this->ssoEnabled) { + return [ + 'name' => 'Local Developer', + 'preferred_username' => 'dev@localhost' + ]; + } + + // Check authentication + $auth = $this->isAuthenticated(); + + if (!$auth['authenticated']) { + $this->handleUnauthorized($auth['error'] ?? 'Authentication required'); + exit; + } + + return $auth['user']; + } + + /** + * Set authentication token (after login) + * + * @param string $token JWT token from MSAL + * @return array ['success' => bool, 'user' => array|null, 'error' => string|null] + */ + public function setAuthToken($token) { + if (!$this->ssoEnabled) { + return ['success' => false, 'error' => 'SSO is disabled']; + } + + // Validate token + $validation = $this->validator->validateToken($token); + if (!$validation['valid']) { + return ['success' => false, 'error' => $validation['error']]; + } + + // Set httpOnly cookie with security options + $cookieOptions = [ + 'expires' => time() + (24 * 60 * 60), // 24 hours + 'path' => '/', + 'domain' => '', + 'secure' => isset($_SERVER['HTTPS']), // Only over HTTPS in production + 'httponly' => true, + 'samesite' => 'Lax' + ]; + + setcookie('auth_token', $token, $cookieOptions); + + return ['success' => true, 'user' => $validation['payload']]; + } + + /** + * Clear authentication token (logout) + */ + public function clearAuthToken() { + setcookie('auth_token', '', [ + 'expires' => time() - 3600, + 'path' => '/', + 'httponly' => true + ]); + } + + /** + * Get token from cookie + * + * @return string|null + */ + private function getTokenFromCookie() { + return isset($_COOKIE['auth_token']) ? $_COOKIE['auth_token'] : null; + } + + /** + * Handle unauthorized access + * + * @param string $error Error message + */ + private function handleUnauthorized($error) { + if ($this->isAjaxRequest()) { + // For AJAX requests, return JSON + header('Content-Type: application/json'); + http_response_code(401); + echo json_encode([ + 'error' => 'Authentication required', + 'message' => $error, + 'requiresAuth' => true + ]); + } else { + // For page requests, show login interface + $this->showLoginPage($error); + } + } + + /** + * Check if request is AJAX + * + * @return bool + */ + private function isAjaxRequest() { + return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && + strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'; + } + + /** + * Show login page with MSAL integration + * + * @param string $error Optional error message + */ + public function showLoginPage($error = '') { + ?> + + + + + + Sign In - VEO3 Usage Report + + + + + + + + + + + + + tenantId = $tenantId; + $this->clientId = $clientId; + } + + /** + * Validate a JWT token + * + * @param string $token The JWT token to validate + * @return array ['valid' => bool, 'payload' => array|null, 'error' => string|null] + */ + public function validateToken($token) { + if (empty($token)) { + return ['valid' => false, 'error' => 'Token is empty']; + } + + try { + // Get public keys from Azure AD + $jwks = $this->getJWKS(); + if (!$jwks) { + return ['valid' => false, 'error' => 'Could not retrieve public keys from Azure AD']; + } + + // Convert JWKS to Key objects + $keys = JWK::parseKeySet($jwks); + + // Decode and validate the JWT + $decoded = JWT::decode($token, $keys); + $payload = (array) $decoded; + + // Validate claims + $validation = $this->validateClaims($payload); + if (!$validation['valid']) { + return $validation; + } + + return ['valid' => true, 'payload' => $payload]; + + } catch (\Firebase\JWT\ExpiredException $e) { + return ['valid' => false, 'error' => 'Token has expired']; + } catch (\Firebase\JWT\SignatureInvalidException $e) { + return ['valid' => false, 'error' => 'Token signature is invalid']; + } catch (\Firebase\JWT\BeforeValidException $e) { + return ['valid' => false, 'error' => 'Token is not yet valid']; + } catch (Exception $e) { + return ['valid' => false, 'error' => 'JWT validation failed: ' . $e->getMessage()]; + } + } + + /** + * Validate JWT claims + * + * @param array $payload The decoded JWT payload + * @return array ['valid' => bool, 'error' => string|null] + */ + private function validateClaims($payload) { + $now = time(); + + // Check expiration (exp claim) + if (isset($payload['exp']) && $payload['exp'] < $now) { + return ['valid' => false, 'error' => 'Token has expired']; + } + + // Check not-before (nbf claim) + if (isset($payload['nbf']) && $payload['nbf'] > $now) { + return ['valid' => false, 'error' => 'Token is not yet valid']; + } + + // Validate audience (aud claim) - must be our client ID or Microsoft Graph + if (isset($payload['aud'])) { + $validAudiences = [ + $this->clientId, + '00000003-0000-0000-c000-000000000000', // Microsoft Graph + 'https://graph.microsoft.com' + ]; + + if (!in_array($payload['aud'], $validAudiences)) { + return ['valid' => false, 'error' => 'Invalid audience: ' . $payload['aud']]; + } + } + + // Validate issuer (iss claim) - must be from Azure AD tenant + if (isset($payload['iss'])) { + $validIssuers = [ + "https://login.microsoftonline.com/{$this->tenantId}/v2.0", + "https://login.microsoftonline.com/{$this->tenantId}/", + "https://sts.windows.net/{$this->tenantId}/", + "https://login.microsoftonline.com/common/v2.0" + ]; + + if (!in_array($payload['iss'], $validIssuers)) { + return ['valid' => false, 'error' => 'Invalid issuer: ' . $payload['iss']]; + } + } + + return ['valid' => true]; + } + + /** + * Get JWKS (JSON Web Key Set) from Azure AD + * + * @return array|null + */ + private function getJWKS() { + // Return cached JWKS if still valid + if ($this->jwksCache && (time() - $this->jwksCacheTime) < $this->jwksCacheDuration) { + return $this->jwksCache; + } + + // Get OpenID configuration from Azure AD + $configUrl = "https://login.microsoftonline.com/{$this->tenantId}/v2.0/.well-known/openid-configuration"; + + $config = $this->fetchJson($configUrl); + if (!$config || !isset($config['jwks_uri'])) { + error_log("Failed to get OpenID configuration from: $configUrl"); + return null; + } + + // Fetch JWKS from jwks_uri + $jwks = $this->fetchJson($config['jwks_uri']); + + if (!$jwks || !isset($jwks['keys'])) { + error_log("Failed to get JWKS from: " . $config['jwks_uri']); + return null; + } + + // Ensure all keys have the 'alg' parameter + foreach ($jwks['keys'] as &$key) { + if (!isset($key['alg'])) { + // Default to RS256 for RSA keys + if (isset($key['kty']) && $key['kty'] === 'RSA') { + $key['alg'] = 'RS256'; + } + } + } + + // Cache the JWKS + $this->jwksCache = $jwks; + $this->jwksCacheTime = time(); + + return $jwks; + } + + /** + * Fetch JSON from a URL + * + * @param string $url + * @return array|null + */ + private function fetchJson($url) { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + if ($curlError) { + error_log("CURL Error fetching $url: $curlError"); + return null; + } + + if ($httpCode !== 200) { + error_log("HTTP Error $httpCode fetching $url"); + return null; + } + + $json = json_decode($response, true); + if (json_last_error() !== JSON_ERROR_NONE) { + error_log("JSON decode error fetching $url: " . json_last_error_msg()); + return null; + } + + return $json; + } +} diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..1d7eccf --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,115 @@ +# VEO3 Report - Quick Start Guide + +## Deploy to Server in 3 Steps + +### Step 1: Prepare Files +```bash +# On your local machine, ensure .env is configured +cp .env.example .env +nano .env # Add your SMTP credentials +``` + +### Step 2: Upload to Server +```bash +scp veo3_report.py veo3_scheduler.py requirements.txt .env \ + veo3-report.service deploy.sh \ + user@your-server.com:/tmp/veo3-deploy/ +``` + +### Step 3: Deploy +```bash +# SSH into server +ssh user@your-server.com + +# Run deployment +cd /tmp/veo3-deploy +chmod +x deploy.sh +sudo ./deploy.sh +``` + +**Done!** The service is now running and will send reports daily at 7:00 PM EST. + +--- + +## Common Commands + +```bash +# Check if service is running +sudo systemctl status veo3-report + +# View live logs +sudo journalctl -u veo3-report -f + +# Restart service +sudo systemctl restart veo3-report + +# Test immediately (without waiting for 7pm) +sudo systemctl stop veo3-report +cd /var/www/veo3-report +sudo -u www-data ./venv/bin/python veo3_report.py +sudo systemctl start veo3-report +``` + +--- + +## Configuration Files + +| File | Location | Purpose | +|------|----------|---------| +| `.env` | `/var/www/veo3-report/.env` | SMTP credentials & recipients | +| `veo3_scheduler.py` | `/var/www/veo3-report/` | Schedule time & timezone | +| `veo3_report.py` | `/var/www/veo3-report/` | Cost per video setting | + +--- + +## Changing Settings + +### Change Email Recipients +```bash +sudo nano /var/www/veo3-report/.env +# Edit REPORT_RECIPIENTS line +sudo systemctl restart veo3-report +``` + +### Change Schedule Time +```bash +sudo nano /var/www/veo3-report/veo3_scheduler.py +# Find: CronTrigger(hour=19, minute=0, ...) +# Change hour (24-hour format: 19 = 7pm) +sudo systemctl restart veo3-report +``` + +### Change Cost Per Video +```bash +sudo nano /var/www/veo3-report/veo3_report.py +# Find: COST_PER_VIDEO = 3.20 +# Change value +sudo systemctl restart veo3-report +``` + +--- + +## Troubleshooting + +**Service won't start?** +```bash +sudo journalctl -u veo3-report -n 50 +``` + +**Emails not sending?** +```bash +# Test manually +cd /var/www/veo3-report +sudo -u www-data ./venv/bin/python veo3_report.py +# Check output for SMTP errors +``` + +**Wrong time?** +```bash +# Service uses EST timezone explicitly in code +# Check: sudo journalctl -u veo3-report | grep "Next scheduled run" +``` + +--- + +For detailed information, see [DEPLOYMENT.md](DEPLOYMENT.md) diff --git a/README.md b/README.md new file mode 100644 index 0000000..939a573 --- /dev/null +++ b/README.md @@ -0,0 +1,168 @@ +# VEO3 Usage Report System + +Automated reporting system for VEO3 video generation usage analytics with Microsoft Azure AD Single Sign-On. + +## Features + +- **PHP Web Interface**: Interactive dashboard for viewing usage reports +- **SSO Authentication**: Microsoft Azure AD (MSAL) integration for secure access +- **Python Automation**: Automated daily/weekly/monthly email reports via SMTP (Mailgun) +- **Granular Analytics**: + - Last 24 hours activity + - Last 7 days trends + - Last 30 days overview + - Top 25 users per period +- **Professional Styling**: Montserrat font, custom yellow (#FFC407) branding + +## Setup + +### PHP Web Reports with SSO + +1. **Install PHP dependencies:** + ```bash + composer install + ``` + +2. **Configure SSO (see [SSO-SETUP.md](SSO-SETUP.md)):** + ```bash + cp .env.example .env + nano .env # Add Azure AD tenant and client IDs + ``` + +3. **For local development (disable SSO):** + ```bash + # In .env + SSO_ENABLED=false + ``` + +4. **Start PHP development server:** + ```bash + php -S localhost:8000 + ``` + +5. **Access the interfaces:** + - `http://localhost:8000/webhook_caller.php` - Fetch data from webhook + - `http://localhost:8000/report.php` - View full report + +### Python Email Reports + +1. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +2. Configure environment variables: + ```bash + cp .env.example .env + # Edit .env with your SMTP credentials + ``` + +3. Run the report generator: + ```bash + python veo3_report.py + ``` + +## Configuration (.env) + +```bash +# Webhook URL +WEBHOOK_URL=https://hook.us1.make.celonis.com/... + +# SMTP Settings (Mailgun SMTP) +SMTP_SERVER=smtp.mailgun.org +SMTP_PORT=587 +SMTP_USER=your-mailgun-smtp-user@your-domain.com +SMTP_PASSWORD=your-smtp-password +SENDER_EMAIL=reports@your-domain.com + +# Recipients (comma-separated) +REPORT_RECIPIENTS=user1@example.com,user2@example.com +``` + +## Scheduling Automated Reports + +### Production Server Deployment (Recommended) + +For production servers, use the included systemd service instead of cron: + +1. **Deploy to server:** + ```bash + # Upload files to server + scp veo3_report.py veo3_scheduler.py requirements.txt .env \ + veo3-report.service deploy.sh user@your-server:/tmp/veo3-deploy/ + + # Run deployment script + ssh user@your-server + cd /tmp/veo3-deploy + chmod +x deploy.sh + sudo ./deploy.sh + ``` + +2. **Service will run automatically at 7:00 PM EST daily** + +**Benefits:** +- āœ… Auto-restart on failure +- āœ… Starts on server boot +- āœ… Better logging with journalctl +- āœ… No cron dependency +- āœ… Easy status monitoring + +See **[DEPLOYMENT.md](DEPLOYMENT.md)** for full deployment guide. + +### Alternative: Manual Scheduling + +#### macOS/Linux (cron) + +```bash +# Daily report at 9 AM +0 9 * * * cd /path/to/VEO3-REPORT && /usr/bin/python3 veo3_report.py +``` + +#### Windows (Task Scheduler) + +Create a scheduled task that runs: +``` +python C:\path\to\VEO3-REPORT\veo3_report.py +``` + +## Report Output + +The Python script: +- Fetches data from webhook +- Analyzes three time periods (24h, 7d, 30d) +- Generates HTML report saved as `email_report.html` +- Sends email via SMTP to configured recipients + +## Files + +### Web Interface +- `webhook_caller.php` - Web interface for fetching webhook data +- `report.php` - Interactive web dashboard with collapsible sections + +### Python Automation +- `veo3_report.py` - Automated email report generator +- `veo3_scheduler.py` - Scheduler daemon service (runs continuously) +- `requirements.txt` - Python dependencies + +### Deployment +- `deploy.sh` - Automated server deployment script +- `veo3-report.service` - systemd service configuration +- `DEPLOYMENT.md` - Complete deployment guide + +### Configuration +- `.env.example` - Configuration template +- `.env` - Your configuration (not in git) +- `config.php` - SSO and session configuration +- `env_loader.php` - Environment variable loader + +### SSO Files +- `AuthMiddleware.php` - Main authentication class +- `JWTValidator.php` - JWT token validator +- `auth.php` - Authentication API endpoint +- `composer.json` - PHP dependencies +- `SSO-SETUP.md` - Complete SSO setup guide + +### Generated Files +- `webhook_response.json` - Cached webhook data (auto-generated) +- `email_report.html` - Latest email report (auto-generated) +- `logs/` - Application logs (auto-generated) diff --git a/SSO-SETUP.md b/SSO-SETUP.md new file mode 100644 index 0000000..1210dc0 --- /dev/null +++ b/SSO-SETUP.md @@ -0,0 +1,318 @@ +# SSO Setup Guide - Microsoft Azure AD (MSAL) + +The VEO3 Report System uses Microsoft Azure AD for Single Sign-On authentication. This protects both the web dashboard and webhook caller pages. + +## Prerequisites + +- Microsoft Azure AD tenant +- Permission to register applications in Azure AD +- PHP 7.4 or higher +- Composer (PHP package manager) + +## Setup Steps + +### 1. Install PHP Dependencies + +```bash +cd /path/to/veo3-report +composer install +``` + +This installs the `firebase/php-jwt` library required for token validation. + +### 2. Register Application in Azure AD + +1. **Login to Azure Portal**: https://portal.azure.com +2. **Navigate to**: Azure Active Directory → App registrations → New registration + +3. **Register the application:** + - **Name**: VEO3 Usage Report + - **Supported account types**: Accounts in this organizational directory only + - **Redirect URI**: Web → `https://your-domain.com/report.php` (and add `/webhook_caller.php`) + - Click **Register** + +4. **Note these values:** + - **Application (client) ID**: Copy this + - **Directory (tenant) ID**: Copy this + +5. **Configure Authentication:** + - Go to **Authentication** in left menu + - Under **Implicit grant and hybrid flows**: + - āœ… Check **ID tokens** + - āœ… Check **Access tokens** + - Click **Save** + +6. **Configure API Permissions:** + - Go to **API permissions** in left menu + - Should already have **Microsoft Graph** → **User.Read** (delegated) + - This is sufficient for SSO + +### 3. Configure Environment Variables + +Edit your `.env` file: + +```bash +# SSO Configuration +SSO_ENABLED=true +SSO_TENANT_ID=your-tenant-id-here +SSO_CLIENT_ID=your-client-id-here +``` + +**For Local Development (disable SSO):** +```bash +SSO_ENABLED=false +``` + +When SSO is disabled, you'll be logged in as "Local Developer" automatically. + +### 4. Deploy to Server + +If deploying with the automated script: + +```bash +# Ensure .env is configured +nano .env + +# Run deployment +sudo ./deploy.sh +``` + +The deployment script will: +- Copy SSO files +- Install Composer dependencies +- Set up the systemd service + +### 5. Configure Web Server + +#### Apache (.htaccess) + +```apache +# Enable PHP +AddHandler application/x-httpd-php .php + +# Security headers +Header always set X-Frame-Options "SAMEORIGIN" +Header always set X-Content-Type-Options "nosniff" +Header always set X-XSS-Protection "1; mode=block" + +# Deny access to sensitive files + + Require all denied + + + + Require all denied + + + + Require all denied + +``` + +#### Nginx + +```nginx +location ~ \.php$ { + fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; +} + +# Deny access to sensitive files +location ~ /\. { + deny all; +} + +location ~ composer\.(json|lock)$ { + deny all; +} + +# Security headers +add_header X-Frame-Options "SAMEORIGIN" always; +add_header X-Content-Type-Options "nosniff" always; +add_header X-XSS-Protection "1; mode=block" always; +``` + +## Testing SSO + +### 1. Test Locally (SSO Disabled) + +```bash +# In .env +SSO_ENABLED=false + +# Start PHP server +php -S localhost:8000 + +# Visit http://localhost:8000/report.php +# Should show reports without login +``` + +### 2. Test with SSO Enabled + +```bash +# In .env +SSO_ENABLED=true +SSO_TENANT_ID=your-tenant-id +SSO_CLIENT_ID=your-client-id + +# Visit https://your-domain.com/report.php +# Should redirect to Microsoft login +``` + +## How It Works + +### Authentication Flow + +1. **User visits protected page** (report.php or webhook_caller.php) +2. **AuthMiddleware checks** for valid authentication cookie +3. **If not authenticated:** + - Shows Microsoft login page + - User authenticates with Azure AD + - MSAL returns JWT token + - Token sent to `auth.php` for validation + - Valid token stored in secure cookie + - Page reloads with authenticated session +4. **If authenticated:** + - JWT token validated on each request + - User information available in `$user` variable + +### Security Features + +- āœ… **JWT validation** with Azure AD public keys +- āœ… **HttpOnly cookies** (not accessible to JavaScript) +- āœ… **Token expiration** checked (24-hour lifetime) +- āœ… **Audience validation** (ensures token is for this app) +- āœ… **Issuer validation** (ensures token from correct Azure tenant) +- āœ… **SameSite cookie** attribute (CSRF protection) +- āœ… **Secure cookies** over HTTPS in production + +### File Structure + +``` +veo3-report/ +ā”œā”€ā”€ AuthMiddleware.php # Main authentication class +ā”œā”€ā”€ JWTValidator.php # JWT token validator +ā”œā”€ā”€ auth.php # Authentication API endpoint +ā”œā”€ā”€ config.php # SSO configuration +ā”œā”€ā”€ env_loader.php # .env file loader +ā”œā”€ā”€ report.php # Protected: requires auth +ā”œā”€ā”€ webhook_caller.php # Protected: requires auth +└── composer.json # PHP dependencies +``` + +## Troubleshooting + +### "Could not retrieve public keys from Azure AD" + +**Cause**: Network issue or invalid tenant ID +**Fix**: +- Verify `SSO_TENANT_ID` in `.env` +- Check server can reach `login.microsoftonline.com` +- Check PHP error logs: `tail -f /var/log/apache2/error.log` + +### "Token signature is invalid" + +**Cause**: Token from wrong tenant or client +**Fix**: +- Verify `SSO_CLIENT_ID` matches Azure app registration +- Clear browser cookies and try again +- Ensure redirect URI matches exactly in Azure + +### "Invalid audience" + +**Cause**: Token not intended for this application +**Fix**: +- Verify `SSO_CLIENT_ID` in `.env` +- Check Azure app registration client ID + +### Login popup blocked + +**Cause**: Browser blocking popups +**Fix**: +- Allow popups for your domain +- Try different browser + +### Token expired + +**Cause**: Token older than 24 hours +**Fix**: +- Clear cookies and login again +- Automatic redirect to login should occur + +## Disabling SSO + +To temporarily disable SSO (for maintenance or testing): + +```bash +# In .env +SSO_ENABLED=false + +# Restart web server/PHP-FPM +sudo systemctl restart apache2 +# or +sudo systemctl restart nginx +``` + +When disabled: +- No login required +- Mock user "Local Developer" used +- Full access to all features + +## Adding More Protected Pages + +To protect additional PHP pages: + +```php +requireAuth(); + +// User is authenticated, $user contains: +// - name: Display name +// - preferred_username: Email address +?> +``` + +## User Logout + +Add a logout link: + +```html +Logout + + +``` + +## Production Checklist + +- [ ] Azure app registered with correct redirect URIs +- [ ] `.env` configured with correct tenant/client IDs +- [ ] `SSO_ENABLED=true` in `.env` +- [ ] Composer dependencies installed +- [ ] HTTPS enabled (required for production) +- [ ] Security headers configured in web server +- [ ] Error logs monitored for auth issues +- [ ] Test login/logout flow + +## Support + +For SSO issues: +1. Check PHP error logs +2. Check browser console for JavaScript errors +3. Verify Azure app configuration +4. Test with `SSO_ENABLED=false` to isolate issue diff --git a/auth.php b/auth.php new file mode 100644 index 0000000..4e40467 --- /dev/null +++ b/auth.php @@ -0,0 +1,118 @@ + 'Invalid request - action required']); + exit; +} + +$action = $input['action']; + +// Handle different actions +switch ($action) { + case 'login': + handleLogin($auth, $input); + break; + + case 'logout': + handleLogout($auth); + break; + + case 'status': + handleStatus($auth); + break; + + default: + http_response_code(400); + echo json_encode(['error' => 'Unknown action: ' . $action]); + break; +} + +/** + * Handle login action + */ +function handleLogin($auth, $input) { + if (!$auth->isSSOEnabled()) { + http_response_code(400); + echo json_encode(['error' => 'SSO is disabled']); + return; + } + + // Prefer ID token for validation, fallback to access token + $token = $input['idToken'] ?? $input['accessToken'] ?? null; + + if (!$token) { + http_response_code(400); + echo json_encode(['error' => 'Authentication token is required']); + return; + } + + // Validate and set token + $result = $auth->setAuthToken($token); + + if ($result['success']) { + echo json_encode([ + 'success' => true, + 'message' => 'Authentication successful', + 'user' => [ + 'name' => $result['user']['name'] ?? 'Unknown', + 'email' => $result['user']['preferred_username'] ?? $result['user']['upn'] ?? 'Unknown' + ] + ]); + } else { + http_response_code(401); + echo json_encode([ + 'success' => false, + 'error' => $result['error'] + ]); + } +} + +/** + * Handle logout action + */ +function handleLogout($auth) { + $auth->clearAuthToken(); + echo json_encode([ + 'success' => true, + 'message' => 'Logged out successfully' + ]); +} + +/** + * Handle status check action + */ +function handleStatus($auth) { + $authStatus = $auth->isAuthenticated(); + + if ($authStatus['authenticated']) { + echo json_encode([ + 'authenticated' => true, + 'sso_enabled' => $auth->isSSOEnabled(), + 'user' => [ + 'name' => $authStatus['user']['name'] ?? 'Unknown', + 'email' => $authStatus['user']['preferred_username'] ?? $authStatus['user']['upn'] ?? 'Unknown' + ] + ]); + } else { + http_response_code(401); + echo json_encode([ + 'authenticated' => false, + 'sso_enabled' => $auth->isSSOEnabled(), + 'error' => $authStatus['error'] ?? 'Not authenticated' + ]); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a03a9f7 --- /dev/null +++ b/composer.json @@ -0,0 +1,20 @@ +{ + "name": "veo3-report/usage-analytics", + "description": "VEO3 Usage Report System with MSAL Authentication", + "type": "project", + "require": { + "php": ">=7.4", + "firebase/php-jwt": "^6.0" + }, + "require-dev": {}, + "autoload": { + "classmap": [ + "JWTValidator.php", + "AuthMiddleware.php" + ] + }, + "config": { + "optimize-autoloader": true, + "sort-packages": true + } +} diff --git a/config.php b/config.php new file mode 100644 index 0000000..dff8311 --- /dev/null +++ b/config.php @@ -0,0 +1,30 @@ + /etc/systemd/system/${SERVICE_NAME}.service + +# Reload systemd +echo "Reloading systemd daemon..." +systemctl daemon-reload + +# Enable service (start on boot) +echo "Enabling service to start on boot..." +systemctl enable ${SERVICE_NAME}.service + +# Start service +echo "Starting service..." +systemctl start ${SERVICE_NAME}.service + +# Wait a moment for service to start +sleep 2 + +# Check service status +echo "" +echo "========================================" +echo "Service Status:" +echo "========================================" +systemctl status ${SERVICE_NAME}.service --no-pager + +echo "" +echo "========================================" +echo "Deployment Complete!" +echo "========================================" +echo "" +echo "Service Commands:" +echo " Start: sudo systemctl start ${SERVICE_NAME}" +echo " Stop: sudo systemctl stop ${SERVICE_NAME}" +echo " Restart: sudo systemctl restart ${SERVICE_NAME}" +echo " Status: sudo systemctl status ${SERVICE_NAME}" +echo " Logs: sudo journalctl -u ${SERVICE_NAME} -f" +echo "" +echo "Log Files:" +echo " Service: $LOG_DIR/service.log" +echo " Errors: $LOG_DIR/service-error.log" +echo " Scheduler: $DEPLOY_DIR/logs/veo3_scheduler.log" +echo "" +echo "The report will run automatically at 7:00 PM EST every day." +echo "" diff --git a/env_loader.php b/env_loader.php new file mode 100644 index 0000000..b1c4598 --- /dev/null +++ b/env_loader.php @@ -0,0 +1,35 @@ + 3.20, // $3.20 per 8-second video + 'TEXT2IMAGE' => 0.04, // Average cost per image generation + 'TEXT2VOICE' => 0.30, // Cost per TTS conversion + 'SPEECH2SPEECH' => 0.50, // Cost per speech-to-speech conversion + 'DOCUMENT_TRANSLATION' => 0.10, // Cost per document translation + 'VIDEOQUERY' => 0.25, // Cost per video analysis query +]; + +// Tool display names +$toolNames = [ + 'VEO3' => 'VEO3 Video Generation', + 'TEXT2IMAGE' => 'Text to Image', + 'TEXT2VOICE' => 'Text to Voice', + 'SPEECH2SPEECH' => 'Speech to Speech', + 'DOCUMENT_TRANSLATION' => 'Document Translation', + 'VIDEOQUERY' => 'Video Query Analysis', +]; + +// SSO Authentication +require_once __DIR__ . '/AuthMiddleware.php'; +$auth = new AuthMiddleware(); +$user = $auth->requireAuth(); // This will redirect to login if not authenticated + +// Load and parse JSON +if (!file_exists($responseFile)) { + die("Error: Response file not found. Please run webhook_caller.php first."); +} + +$jsonContent = file_get_contents($responseFile); +$data = json_decode($jsonContent, true); + +if (!$data) { + die("Error: Unable to parse JSON data."); +} + +// Initialize arrays for analysis +$userCounts = []; +$dailyCounts = []; +$monthlyCounts = []; +$promptLengths = []; +$toolCounts = []; +$toolUserCounts = []; +$totalRequests = count($data); +$totalCost = 0; + +// Period-specific analysis +$now = new DateTime('now', new DateTimeZone('UTC')); +$last24Hours = clone $now; +$last24Hours->sub(new DateInterval('P1D')); +$last7Days = clone $now; +$last7Days->sub(new DateInterval('P7D')); +$last30Days = clone $now; +$last30Days->sub(new DateInterval('P30D')); + +$userCounts24h = []; +$userCounts7d = []; +$userCounts30d = []; +$totalPrompts24h = 0; +$totalPrompts7d = 0; +$totalPrompts30d = 0; + +// Process each record +foreach ($data as $record) { + $item = $record['data']; + + // User counts + $user = $item['USER']; + $tool = $item['TOOL'] ?? 'UNKNOWN'; + + if (!isset($userCounts[$user])) { + $userCounts[$user] = 0; + } + $userCounts[$user]++; + + // Tool counts + if (!isset($toolCounts[$tool])) { + $toolCounts[$tool] = 0; + } + $toolCounts[$tool]++; + + // Tool user counts + if (!isset($toolUserCounts[$tool])) { + $toolUserCounts[$tool] = []; + } + if (!isset($toolUserCounts[$tool][$user])) { + $toolUserCounts[$tool][$user] = 0; + } + $toolUserCounts[$tool][$user]++; + + // Calculate cost - use default of 0 for unknown tools + $costPerRequest = $toolCosts[$tool] ?? 0; + $totalCost += $costPerRequest; + + // Date parsing + $date = $item['Date']; + $dateObj = new DateTime($date); + $dateStr = $dateObj->format('Y-m-d'); + $monthStr = $dateObj->format('Y-m'); + + // Period filtering + if ($dateObj >= $last24Hours) { + if (!isset($userCounts24h[$user])) $userCounts24h[$user] = 0; + $userCounts24h[$user]++; + $totalPrompts24h++; + } + if ($dateObj >= $last7Days) { + if (!isset($userCounts7d[$user])) $userCounts7d[$user] = 0; + $userCounts7d[$user]++; + $totalPrompts7d++; + } + if ($dateObj >= $last30Days) { + if (!isset($userCounts30d[$user])) $userCounts30d[$user] = 0; + $userCounts30d[$user]++; + $totalPrompts30d++; + } + + // Daily counts + if (!isset($dailyCounts[$dateStr])) { + $dailyCounts[$dateStr] = 0; + } + $dailyCounts[$dateStr]++; + + // Monthly counts + if (!isset($monthlyCounts[$monthStr])) { + $monthlyCounts[$monthStr] = 0; + } + $monthlyCounts[$monthStr]++; + + // Prompt length analysis (if prompt exists) + if (isset($item['PROMPT']) && $item['PROMPT']) { + $promptLength = strlen($item['PROMPT']); + $promptLengths[] = $promptLength; + } +} + +// Sort tool counts by usage +arsort($toolCounts); + +// Sort period-specific user counts +arsort($userCounts24h); +arsort($userCounts7d); +arsort($userCounts30d); + +// Get top 25 for each period +$topUsers24h = array_slice($userCounts24h, 0, 25, true); +$topUsers7d = array_slice($userCounts7d, 0, 25, true); +$topUsers30d = array_slice($userCounts30d, 0, 25, true); + +// Note: totalCost is already calculated in the loop above +// Calculate period-specific costs (we'll need to recalculate by tool for accuracy) +$cost24h = 0; +$cost7d = 0; +$cost30d = 0; + +// Recalculate period costs +foreach ($data as $record) { + $item = $record['data']; + $tool = $item['TOOL'] ?? 'UNKNOWN'; + $costPerRequest = $toolCosts[$tool] ?? 0; + $dateObj = new DateTime($item['Date']); + + if ($dateObj >= $last24Hours) $cost24h += $costPerRequest; + if ($dateObj >= $last7Days) $cost7d += $costPerRequest; + if ($dateObj >= $last30Days) $cost30d += $costPerRequest; +} + +// Sort data +arsort($userCounts); +ksort($dailyCounts); +ksort($monthlyCounts); + +// Calculate statistics +$uniqueUsers = count($userCounts); +$avgRequestsPerUser = round($totalRequests / $uniqueUsers, 2); +$avgPromptLength = count($promptLengths) > 0 ? round(array_sum($promptLengths) / count($promptLengths), 0) : 0; +$maxPromptLength = count($promptLengths) > 0 ? max($promptLengths) : 0; +$minPromptLength = count($promptLengths) > 0 ? min($promptLengths) : 0; + +// Get date range +$dates = array_keys($dailyCounts); +$startDate = reset($dates); +$endDate = end($dates); + +// Get top 20 users +$topUsers = array_slice($userCounts, 0, 20, true); + +// Get recent daily activity (last 30 days) +$recentDaily = array_slice($dailyCounts, -30, 30, true); +?> + + + + + + AI Tools Usage Report + + + + +
+
+
+

AI Tools Usage Report

+

Data from to

+
+ +
+ +
+
+

Total Requests

+
+
All AI tool requests
+
+
+

Total Cost

+
$
+
All tools combined
+
+
+

Unique Users

+
+
Active accounts
+
+
+

Avg Requests/User

+
+
Mean usage per account
+
+
+ +
+
+

Last 24 Hours

+
requests
+
$
+
+
+

Last 7 Days

+
requests
+
$
+
+
+

Last 30 Days

+
requests
+
$
+
+
+

Avg Prompt Length

+
+
Characters
+
+
+ +
+

Tool Usage Breakdown

+

+ Usage distribution across all AI tools +

+ + + + + + + + + + + $count): + $toolName = $toolNames[$tool] ?? $tool; + $percentage = ($count / $totalRequests) * 100; + + // Get top user for this tool + $topUser = ''; + $topUserCount = 0; + if (isset($toolUserCounts[$tool])) { + arsort($toolUserCounts[$tool]); + $topUserData = array_slice($toolUserCounts[$tool], 0, 1, true); + if (!empty($topUserData)) { + $topUser = array_key_first($topUserData); + $topUserCount = reset($topUserData); + } + } + ?> + + + + + + + + +
ToolRequestsPercentageTop User
% + + () + + - + +
+
+ +
+

Daily Usage - Last 30 Days

+
+
+ $count): + $height = ($count / $maxDaily) * 100; + ?> +
+
+
+ requests +
+
+ +
+
+ + +
+
+
+ + + + + + + + + + + $count): + $dateObj = new DateTime($date); + $dayOfWeek = $dateObj->format('l'); + $barWidth = ($count / $maxDailyRecent) * 100; + ?> + + + + + + + + +
DateDay of WeekRequestsActivity Level
+
+
;">
+
+
+
+
+ +
+

Monthly Usage Breakdown

+ + + + + + + + + + $count): + $percentage = ($count / $totalRequests) * 100; + $barWidth = ($count / $maxMonthly) * 100; + ?> + + + + + + + +
MonthRequestsDistribution
(%) +
+
;">
+
+
+
+ +
+

šŸ“… Last 24 Hours - Top 25 Users

+ 0): ?> +

+ requests from users + Cost: $ +

+ + + + + + + + + + + + $count): + $percentage = ($count / $totalPrompts24h) * 100; + $barWidth = ($count / $maxCount24h) * 100; + + $badge = ''; + if ($rank === 1) $badge = 'šŸ„‡ #1'; + elseif ($rank === 2) $badge = '🄈 #2'; + elseif ($rank === 3) $badge = 'šŸ„‰ #3'; + ?> + + + + + + + + + +
RankUserRequestsPercentageActivity
# + + + % +
+
;">
+
+
+ +

No activity in the last 24 hours

+ +
+ +
+

šŸ“Š Last 7 Days - Top 25 Users

+ 0): ?> +

+ requests from users + Cost: $ +

+ + + + + + + + + + + + $count): + $percentage = ($count / $totalPrompts7d) * 100; + $barWidth = ($count / $maxCount7d) * 100; + + $badge = ''; + if ($rank === 1) $badge = 'šŸ„‡ #1'; + elseif ($rank === 2) $badge = '🄈 #2'; + elseif ($rank === 3) $badge = 'šŸ„‰ #3'; + ?> + + + + + + + + + +
RankUserRequestsPercentageActivity
# + + + % +
+
;">
+
+
+ +

No activity in the last 7 days

+ +
+ +
+

šŸ“ˆ Last 30 Days - Top 25 Users

+ 0): ?> +

+ requests from users + Cost: $ +

+ + + + + + + + + + + + $count): + $percentage = ($count / $totalPrompts30d) * 100; + $barWidth = ($count / $maxCount30d) * 100; + + $badge = ''; + if ($rank === 1) $badge = 'šŸ„‡ #1'; + elseif ($rank === 2) $badge = '🄈 #2'; + elseif ($rank === 3) $badge = 'šŸ„‰ #3'; + ?> + + + + + + + + + +
RankUserRequestsPercentageActivity
# + + + % +
+
;">
+
+
+ +

No activity in the last 30 days

+ +
+ +
+

All Time - Top 20 Users

+ + + + + + + + + + + + $count): + $percentage = ($count / $totalRequests) * 100; + $barWidth = ($count / $maxCount) * 100; + + $badge = ''; + if ($rank === 1) $badge = 'šŸ„‡ #1'; + elseif ($rank === 2) $badge = '🄈 #2'; + elseif ($rank === 3) $badge = 'šŸ„‰ #3'; + ?> + + + + + + + + + +
RankUserPromptsPercentageActivity
# + + + % +
+
;">
+
+
+
+ +
+

+ All Users Summary ( users) + ā–¼ +

+ +
+
+ + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..de4a9a8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +requests>=2.31.0 +python-dotenv>=1.0.0 +apscheduler>=3.10.0 +pytz>=2023.3 diff --git a/veo3-report.service b/veo3-report.service new file mode 100644 index 0000000..573f26b --- /dev/null +++ b/veo3-report.service @@ -0,0 +1,30 @@ +[Unit] +Description=VEO3 Report Scheduler Service +After=network.target +Wants=network-online.target + +[Service] +Type=simple +User=www-data +Group=www-data +WorkingDirectory=/var/www/veo3-report +Environment="PATH=/var/www/veo3-report/venv/bin:/usr/local/bin:/usr/bin:/bin" +ExecStart=/var/www/veo3-report/venv/bin/python3 /var/www/veo3-report/veo3_scheduler.py + +# Restart policy +Restart=always +RestartSec=10 + +# Logging +StandardOutput=append:/var/log/veo3-report/service.log +StandardError=append:/var/log/veo3-report/service-error.log + +# Security settings +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/var/www/veo3-report /var/log/veo3-report + +[Install] +WantedBy=multi-user.target diff --git a/veo3_report.py b/veo3_report.py new file mode 100644 index 0000000..e337bf9 --- /dev/null +++ b/veo3_report.py @@ -0,0 +1,418 @@ +#!/usr/bin/env python3 +""" +AI Tools Usage Report Generator +Fetches usage data from webhook and sends email reports via SMTP +Supports: VEO3, TEXT2IMAGE, TEXT2VOICE, SPEECH2SPEECH, DOCUMENT_TRANSLATION, VIDEOQUERY +""" + +import os +import json +import requests +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from datetime import datetime, timedelta, timezone +from collections import defaultdict +from dotenv import load_dotenv +from typing import Dict, List, Tuple + +# Load environment variables +load_dotenv() + +# Configuration +WEBHOOK_URL = os.getenv('WEBHOOK_URL', 'https://hook.us1.make.celonis.com/u8i4yq6rydu8u8g9bfhk0xbajsyckrmj') +SMTP_SERVER = os.getenv('SMTP_SERVER', 'smtp.mailgun.org') +SMTP_PORT = int(os.getenv('SMTP_PORT', '587')) +SMTP_USER = os.getenv('SMTP_USER') +SMTP_PASSWORD = os.getenv('SMTP_PASSWORD') +SENDER_EMAIL = os.getenv('SENDER_EMAIL') +REPORT_RECIPIENTS = os.getenv('REPORT_RECIPIENTS', '').split(',') + +# Cost configuration per tool type (adjust these as needed) +TOOL_COSTS = { + 'VEO3': 3.20, # $3.20 per 8-second video + 'TEXT2IMAGE': 0.04, # Average cost per image generation + 'TEXT2VOICE': 0.30, # Cost per TTS conversion + 'SPEECH2SPEECH': 0.50, # Cost per speech-to-speech conversion + 'DOCUMENT_TRANSLATION': 0.10, # Cost per document translation + 'VIDEOQUERY': 0.25, # Cost per video analysis query +} + +# Tool display names +TOOL_NAMES = { + 'VEO3': 'VEO3 Video Generation', + 'TEXT2IMAGE': 'Text to Image', + 'TEXT2VOICE': 'Text to Voice', + 'SPEECH2SPEECH': 'Speech to Speech', + 'DOCUMENT_TRANSLATION': 'Document Translation', + 'VIDEOQUERY': 'Video Query Analysis', +} + + +def fetch_webhook_data(start_date: str, end_date: str) -> List[Dict]: + """Fetch data from the webhook""" + print(f"Fetching data from {start_date} to {end_date}...") + + payload = { + 'start_date': start_date, + 'end_date': end_date + } + + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + try: + response = requests.post(WEBHOOK_URL, json=payload, headers=headers) + response.raise_for_status() + data = response.json() + print(f"āœ“ Fetched {len(data)} records") + return data + except Exception as e: + print(f"āœ— Error fetching data: {e}") + return [] + + +def analyze_period_data(data: List[Dict], period_start: datetime, period_end: datetime, period_name: str) -> Dict: + """Analyze data for a specific time period""" + user_counts = defaultdict(int) + daily_counts = defaultdict(int) + tool_counts = defaultdict(int) + tool_user_counts = defaultdict(lambda: defaultdict(int)) + total_requests = 0 + prompt_lengths = [] + total_cost = 0 + + for record in data: + item = record['data'] + record_date = datetime.fromisoformat(item['Date'].replace('Z', '+00:00')) + + # Filter to period + if not (period_start <= record_date <= period_end): + continue + + total_requests += 1 + user = item['USER'] + tool = item['TOOL'] + + user_counts[user] += 1 + tool_counts[tool] += 1 + tool_user_counts[tool][user] += 1 + + date_str = record_date.strftime('%Y-%m-%d') + daily_counts[date_str] += 1 + + if 'PROMPT' in item and item['PROMPT']: + prompt_lengths.append(len(item['PROMPT'])) + + # Calculate cost based on tool type + cost_per_request = TOOL_COSTS.get(tool, 0) + total_cost += cost_per_request + + # Sort users by count + top_users = sorted(user_counts.items(), key=lambda x: x[1], reverse=True)[:25] + + # Sort tools by count + tool_breakdown = sorted(tool_counts.items(), key=lambda x: x[1], reverse=True) + + # Get top users per tool + top_users_by_tool = {} + for tool, users in tool_user_counts.items(): + top_users_by_tool[tool] = sorted(users.items(), key=lambda x: x[1], reverse=True)[:10] + + # Calculate statistics + unique_users = len(user_counts) + avg_requests_per_user = total_requests / unique_users if unique_users > 0 else 0 + avg_prompt_length = sum(prompt_lengths) / len(prompt_lengths) if prompt_lengths else 0 + + return { + 'period_name': period_name, + 'total_requests': total_requests, + 'unique_users': unique_users, + 'avg_requests_per_user': avg_requests_per_user, + 'avg_prompt_length': avg_prompt_length, + 'total_cost': total_cost, + 'top_users': top_users, + 'tool_breakdown': tool_breakdown, + 'top_users_by_tool': top_users_by_tool, + 'daily_counts': dict(sorted(daily_counts.items())), + 'start_date': period_start.strftime('%Y-%m-%d'), + 'end_date': period_end.strftime('%Y-%m-%d') + } + + +def generate_html_report(daily_data: Dict, weekly_data: Dict, monthly_data: Dict) -> str: + """Generate HTML email report""" + + def generate_user_table(users: List[Tuple[str, int]], total: int, period: str) -> str: + """Generate HTML table for top users""" + if not users: + return f"

No activity in {period}

" + + html = '' + html += ''' + + + + + + + + + + + ''' + + max_count = users[0][1] if users else 1 + + for rank, (user, count) in enumerate(users, 1): + percentage = (count / total * 100) if total > 0 else 0 + bar_width = (count / max_count * 100) if max_count > 0 else 0 + + badge = '' + rank_color = '#FFC407' + if rank == 1: + badge = 'šŸ„‡ #1' + elif rank == 2: + badge = '🄈 #2' + elif rank == 3: + badge = 'šŸ„‰ #3' + + html += f''' + + + + + + + + ''' + + html += '
RankUserPromptsPercentageActivity
#{rank}{user}{badge}{count:,}{percentage:.2f}% +
+
' + return html + + def generate_period_section(data: Dict, title: str, emoji: str) -> str: + """Generate HTML section for a time period""" + if data['total_requests'] == 0: + return f''' +
+

+ {emoji} {title} +

+

No activity recorded for this period.

+
+ ''' + + # Generate tool breakdown table + tool_breakdown_html = '

Tool Usage Breakdown

' + tool_breakdown_html += '' + tool_breakdown_html += '' + + for tool, count in data['tool_breakdown']: + tool_name = TOOL_NAMES.get(tool, tool) + tool_cost = count * TOOL_COSTS.get(tool, 0) + percentage = (count / data['total_requests'] * 100) if data['total_requests'] > 0 else 0 + tool_breakdown_html += f'' + + tool_breakdown_html += '
ToolRequestsCostPercentage
{tool_name}{count:,}${tool_cost:,.2f}{percentage:.1f}%
' + + return f''' +
+

+ {emoji} {title} +

+

+ {data['start_date']} to {data['end_date']} +

+ +
+
+

Total Requests

+
{data['total_requests']:,}
+
+
+

Total Cost

+
${data['total_cost']:,.2f}
+
All tools combined
+
+
+

Unique Users

+
{data['unique_users']:,}
+
+
+

Avg/User

+
{data['avg_requests_per_user']:.1f}
+
+
+ + {tool_breakdown_html} + +

Top 25 Users

+ {generate_user_table(data['top_users'], data['total_requests'], data['period_name'])} +
+ ''' + + # Build full HTML + html = f''' + + + + + + + + + +
+
+

AI Tools Usage Report

+

Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

+
+ + {generate_period_section(daily_data, 'Last 24 Hours', 'šŸ“…')} + {generate_period_section(weekly_data, 'Last 7 Days', 'šŸ“Š')} + {generate_period_section(monthly_data, 'Last 30 Days', 'šŸ“ˆ')} + +
+

+ This is an automated report. For questions, please contact your administrator. +

+
+
+ + + ''' + + return html + + +def send_email_via_smtp(subject: str, html_content: str, recipients: List[str]) -> bool: + """Send email via SMTP""" + if not SMTP_USER or not SMTP_PASSWORD or not SENDER_EMAIL: + print("āœ— SMTP credentials not configured") + print(f" SMTP_USER: {'āœ“' if SMTP_USER else 'āœ—'}") + print(f" SMTP_PASSWORD: {'āœ“' if SMTP_PASSWORD else 'āœ—'}") + print(f" SENDER_EMAIL: {'āœ“' if SENDER_EMAIL else 'āœ—'}") + return False + + # Filter out empty recipients + recipients = [r.strip() for r in recipients if r.strip()] + + if not recipients: + print("āœ— No valid recipients configured") + return False + + print(f"Sending email to {len(recipients)} recipient(s)...") + print(f" SMTP Server: {SMTP_SERVER}:{SMTP_PORT}") + print(f" From: {SENDER_EMAIL}") + print(f" To: {', '.join(recipients)}") + + try: + # Create message + msg = MIMEMultipart('alternative') + msg['Subject'] = subject + msg['From'] = SENDER_EMAIL + msg['To'] = ', '.join(recipients) + + # Attach HTML content + html_part = MIMEText(html_content, 'html') + msg.attach(html_part) + + # Connect to SMTP server and send + with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server: + server.set_debuglevel(0) # Set to 1 for verbose output + server.starttls() + server.login(SMTP_USER, SMTP_PASSWORD) + server.send_message(msg) + + print("āœ“ Email sent successfully") + return True + except smtplib.SMTPAuthenticationError as e: + print(f"āœ— SMTP Authentication Error: {e}") + print(" Check your SMTP_USER and SMTP_PASSWORD") + return False + except smtplib.SMTPException as e: + print(f"āœ— SMTP Error: {e}") + return False + except Exception as e: + print(f"āœ— Error sending email: {e}") + return False + + +def main(): + """Main execution function""" + print("=" * 60) + print("AI TOOLS USAGE REPORT GENERATOR") + print("=" * 60) + + # Calculate date ranges (timezone-aware) + now = datetime.now(timezone.utc) + + # Last 24 hours + daily_end = now + daily_start = now - timedelta(days=1) + + # Last 7 days + weekly_end = now + weekly_start = now - timedelta(days=7) + + # Last 30 days + monthly_end = now + monthly_start = now - timedelta(days=30) + + # Fetch data (get last 30 days to cover all periods) + data_start = monthly_start.strftime('%Y-%m-%d') + data_end = monthly_end.strftime('%Y-%m-%d') + + data = fetch_webhook_data(data_start, data_end) + + if not data: + print("āœ— No data retrieved, exiting") + return + + print("\nAnalyzing data...") + + # Analyze each period + daily_report = analyze_period_data(data, daily_start, daily_end, "Last 24 Hours") + weekly_report = analyze_period_data(data, weekly_start, weekly_end, "Last 7 Days") + monthly_report = analyze_period_data(data, monthly_start, monthly_end, "Last 30 Days") + + print(f"āœ“ Daily: {daily_report['total_requests']} requests from {daily_report['unique_users']} users") + print(f"āœ“ Weekly: {weekly_report['total_requests']} requests from {weekly_report['unique_users']} users") + print(f"āœ“ Monthly: {monthly_report['total_requests']} requests from {monthly_report['unique_users']} users") + + # Generate HTML report + print("\nGenerating HTML report...") + html_report = generate_html_report(daily_report, weekly_report, monthly_report) + + # Save HTML to file + output_file = 'email_report.html' + with open(output_file, 'w', encoding='utf-8') as f: + f.write(html_report) + print(f"āœ“ Report saved to {output_file}") + + # Send email if configured + if REPORT_RECIPIENTS and REPORT_RECIPIENTS[0]: + subject = f"AI Tools Usage Report - {now.strftime('%Y-%m-%d')}" + send_email_via_smtp(subject, html_report, REPORT_RECIPIENTS) + else: + print("\n⚠ No recipients configured. Skipping email send.") + print(" Set REPORT_RECIPIENTS in .env file to enable email delivery.") + + print("\n" + "=" * 60) + print("REPORT GENERATION COMPLETE") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/veo3_scheduler.py b/veo3_scheduler.py new file mode 100755 index 0000000..87e2935 --- /dev/null +++ b/veo3_scheduler.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +""" +VEO3 Report Scheduler Service +Runs daily at 7:00 PM EST to generate and email VEO3 usage reports +""" + +import os +import sys +import logging +import signal +from datetime import datetime +from pathlib import Path +from apscheduler.schedulers.blocking import BlockingScheduler +from apscheduler.triggers.cron import CronTrigger +import pytz + +# Add the script directory to path so we can import veo3_report +script_dir = Path(__file__).parent +sys.path.insert(0, str(script_dir)) + +# Import the report generation function +from veo3_report import main as generate_report + +# Configure logging +log_dir = script_dir / 'logs' +log_dir.mkdir(exist_ok=True) + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_dir / 'veo3_scheduler.log'), + logging.StreamHandler() + ] +) + +logger = logging.getLogger('VEO3Scheduler') + +# Timezone configuration +EST = pytz.timezone('America/New_York') + + +class VEO3Scheduler: + """Scheduler service for VEO3 reports""" + + def __init__(self): + self.scheduler = BlockingScheduler(timezone=EST) + self.running = False + + # Register signal handlers for graceful shutdown + signal.signal(signal.SIGINT, self._signal_handler) + signal.signal(signal.SIGTERM, self._signal_handler) + + def _signal_handler(self, signum, frame): + """Handle shutdown signals gracefully""" + logger.info(f"Received signal {signum}. Shutting down gracefully...") + self.stop() + + def run_report(self): + """Execute the report generation""" + try: + logger.info("=" * 70) + logger.info("STARTING SCHEDULED REPORT GENERATION") + logger.info("=" * 70) + + # Change to script directory to ensure relative paths work + os.chdir(script_dir) + + # Run the report generation + generate_report() + + logger.info("=" * 70) + logger.info("REPORT GENERATION COMPLETED SUCCESSFULLY") + logger.info("=" * 70) + + except Exception as e: + logger.error(f"Error generating report: {e}", exc_info=True) + logger.error("=" * 70) + logger.error("REPORT GENERATION FAILED") + logger.error("=" * 70) + + def start(self): + """Start the scheduler service""" + logger.info("VEO3 Report Scheduler Service Starting...") + logger.info(f"Working directory: {script_dir}") + logger.info(f"Timezone: {EST}") + + # Schedule daily report at 7:00 PM EST + self.scheduler.add_job( + self.run_report, + trigger=CronTrigger(hour=19, minute=0, timezone=EST), + id='daily_report', + name='Daily VEO3 Report Generation', + replace_existing=True + ) + + # Log scheduling confirmation + logger.info(f"Scheduled to run daily at 7:00 PM EST") + logger.info("Job scheduled successfully") + + # Optional: Run immediately on startup for testing + # Uncomment the line below to run a report when the service starts + # logger.info("Running initial report...") + # self.run_report() + + self.running = True + + try: + logger.info("Scheduler service is now running. Press Ctrl+C to stop.") + self.scheduler.start() + except (KeyboardInterrupt, SystemExit): + self.stop() + + def stop(self): + """Stop the scheduler service""" + if self.running: + logger.info("Stopping scheduler service...") + self.scheduler.shutdown(wait=True) + self.running = False + logger.info("Scheduler service stopped.") + sys.exit(0) + + +def main(): + """Main entry point""" + logger.info("=" * 70) + logger.info("VEO3 REPORT SCHEDULER SERVICE") + logger.info("=" * 70) + + scheduler = VEO3Scheduler() + scheduler.start() + + +if __name__ == "__main__": + main() diff --git a/webhook_caller.php b/webhook_caller.php new file mode 100644 index 0000000..b177157 --- /dev/null +++ b/webhook_caller.php @@ -0,0 +1,205 @@ +requireAuth(); // This will redirect to login if not authenticated + +// Configuration +$webhookUrl = 'https://hook.us1.make.celonis.com/v7ev5x3j50oy1n1iqd6u6462cg0cz4yy'; +$responseFile = 'webhook_response.json'; + +// Initialize variables +$response = null; +$error = null; +$isAjax = isset($_POST['ajax']) && $_POST['ajax'] == '1'; + +// Handle form submission +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['start_date']) && isset($_POST['end_date'])) { + $startDate = $_POST['start_date']; + $endDate = $_POST['end_date']; + + // Prepare the data to send + $postData = [ + 'start_date' => $startDate, + 'end_date' => $endDate + ]; + + // Initialize cURL + $ch = curl_init($webhookUrl); + + // Set cURL options + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData)); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Accept: application/json' + ]); + + // Execute request + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + // Check for errors + if (curl_errno($ch)) { + $error = 'cURL Error: ' . curl_error($ch); + } elseif ($httpCode !== 200) { + $error = "HTTP Error: Received status code $httpCode"; + } else { + // Save response to file + file_put_contents($responseFile, $response); + $error = null; + } + + curl_close($ch); + + // If AJAX request, return JSON + if ($isAjax) { + header('Content-Type: application/json'); + if ($error) { + echo json_encode(['success' => false, 'error' => $error]); + } else { + echo json_encode(['success' => true, 'message' => 'Data refreshed successfully']); + } + exit; + } +} + +// Set default dates (last 30 days) +$defaultEndDate = date('Y-m-d'); +$defaultStartDate = date('Y-m-d', strtotime('-30 days')); +?> + + + + + + Webhook Caller + + + +
+

Webhook Caller

+
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ Error: +
+
+ + + +
+
+ Success! Response received and saved to +
+

Response:

+
+
+
+ +

Formatted JSON:

+
+
+
+
+ + + diff --git a/webhook_caller.textClipping b/webhook_caller.textClipping new file mode 100644 index 0000000000000000000000000000000000000000..e84edfd1218092b39cc3a53089eead9d56dc5ae4 GIT binary patch literal 219 zcmYc)$jK}&F)+Bu$P^J8;;HMBSdw^|nT3^&og-dAB{@G=FR`E?CsnVcC^03oBr`uR zF-JEyFqMZJEpf28@glnt>Ba H!>COF&gwm` literal 0 HcmV?d00001