diff --git a/.env.example b/.env.example index 8dfca77..7325424 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,15 @@ # Google Gemini API Key # Get your API key from: https://makersuite.google.com/app/apikey -VITE_GEMINI_API_KEY=your_key_here \ No newline at end of file +VITE_GEMINI_API_KEY=AIzaSyCMKLSJJYEx4c6-TtBACnjdULrLzsr_fts + +# Microsoft SSO Configuration Local Development Credentials +# Client ID and Tenant ID from Azure AD App Registration +# VITE_MSAL_CLIENT_ID=15c0c4e2-bac0-4564-a3a6-c2717f00a6d9 +# VITE_MSAL_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385 +# VITE_MSAL_REDIRECT_URI=http://localhost:3000 + + +# Redirect URI - CONFIGURE FOR PRODUCTION DEPLOYMENT +VITE_MSAL_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef +VITE_MSAL_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385 +VITE_MSAL_REDIRECT_URI=https://ai-sandbox.oliver.solutions/prompt/index.html \ No newline at end of file diff --git a/.gitignore b/.gitignore index 546f16e..3fad84d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,10 +11,15 @@ node_modules dist dist-ssr *.local +.claude/ -# Environment variables + +# Environment variables (NEVER commit these - contain API keys and secrets) .env .env.local +.env.development +.env.production +.env*.local # Editor directories and files .vscode/* @@ -30,3 +35,20 @@ dist-ssr # Test files and folders TESTS/ TESTS/* + +# Backup files +*.bak +*.backup +*.old +*.orig +*.save +*.swp +*~ + +# Security and sensitive files +*.key +*.pem +*.p12 +*.pfx +credentials.json +secrets.json diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..8cdaa93 --- /dev/null +++ b/.htaccess @@ -0,0 +1,122 @@ +# ============================================ +# CinePrompt Studio - Apache Security Configuration +# Safe version that works on most servers +# ============================================ + +# ------------------------------ +# 1. PREVENT DIRECTORY LISTING +# ------------------------------ + + Options -Indexes + + +# ------------------------------ +# 2. BLOCK ACCESS TO SENSITIVE FILES +# ------------------------------ + +# Block .env files (contains API keys) + + + Require all denied + + + Order allow,deny + Deny from all + + + + + + Require all denied + + + Order allow,deny + Deny from all + + + +# Block .git directory + + + Require all denied + + + Order allow,deny + Deny from all + + + + + + Require all denied + + + Order allow,deny + Deny from all + + + +# Block configuration files + + + Require all denied + + + Order allow,deny + Deny from all + + + + + + Require all denied + + + Order allow,deny + Deny from all + + + +# ------------------------------ +# 3. SINGLE PAGE APPLICATION ROUTING (OPTIONAL) +# Only enable if deploying built app (npm run build) +# Comment out if causing issues +# ------------------------------ + + + RewriteEngine On + + # Don't rewrite for actual files and directories + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + + # Redirect everything else to index.html + RewriteRule . /index.html [L] + + +# ------------------------------ +# 4. BASIC SECURITY HEADERS (OPTIONAL) +# Only if mod_headers is enabled +# ------------------------------ + + + # Prevent clickjacking + Header set X-Frame-Options "SAMEORIGIN" + + # Prevent MIME sniffing + Header set X-Content-Type-Options "nosniff" + + # XSS Protection + Header set X-XSS-Protection "1; mode=block" + + +# ------------------------------ +# 5. GZIP COMPRESSION (OPTIONAL) +# Only if mod_deflate is enabled +# ------------------------------ + + + AddOutputFilterByType DEFLATE text/html text/plain text/css + AddOutputFilterByType DEFLATE application/javascript text/javascript + AddOutputFilterByType DEFLATE application/json + diff --git a/package-lock.json b/package-lock.json index f8eaa32..9ab5fd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "cineprompt-studio", "version": "0.0.0", "dependencies": { + "@azure/msal-browser": "^4.27.0", + "@azure/msal-react": "^3.0.23", "@google/generative-ai": "^0.24.1", "lucide-react": "^0.555.0", "react": "^19.2.0", @@ -41,6 +43,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@azure/msal-browser": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.27.0.tgz", + "integrity": "sha512-bZ8Pta6YAbdd0o0PEaL1/geBsPrLEnyY/RDWqvF1PP9RUH8EMLvUMGoZFYS6jSlUan6KZ9IMTLCnwpWWpQRK/w==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.13.3" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "15.13.3", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.3.tgz", + "integrity": "sha512-shSDU7Ioecya+Aob5xliW9IGq1Ui8y4EVSdWGyI1Gbm4Vg61WpP95LuzcY214/wEjSn6w4PZYD4/iVldErHayQ==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-react": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@azure/msal-react/-/msal-react-3.0.23.tgz", + "integrity": "sha512-tHvq441nwlJD9QfQP4ZStiw6xb2hQoujNHZhZb+wpUbImb3wyr2FF6/umhX/p+yzc/aq0Lee7mbdDDpzRZzxcA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@azure/msal-browser": "^4.27.0", + "react": "^16.8.0 || ^17 || ^18 || ^19.2.1" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", diff --git a/package.json b/package.json index 1083a4b..d9737e4 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "dependencies": { + "@azure/msal-browser": "^4.27.0", + "@azure/msal-react": "^3.0.23", "@google/generative-ai": "^0.24.1", "lucide-react": "^0.555.0", "react": "^19.2.0", diff --git a/src/App.jsx b/src/App.jsx index e248eea..efa15b1 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,7 +1,12 @@ import CinePromptStudio from './components/CinePromptStudio'; +import AuthGate from './components/AuthGate'; function App() { - return ; + return ( + + + + ); } export default App; \ No newline at end of file diff --git a/src/components/AuthError.jsx b/src/components/AuthError.jsx new file mode 100644 index 0000000..1cdb46c --- /dev/null +++ b/src/components/AuthError.jsx @@ -0,0 +1,58 @@ +import { AlertCircle, RefreshCw, LogOut } from 'lucide-react'; + +function AuthError({ error, onRetry, onLogout }) { + return ( +
+
+
+
+ +
+
+ +

+ Authentication Failed +

+ +

+ {error?.message || "An error occurred during authentication. Please try again."} +

+ + {error?.errorCode && ( +
+

Error Code:

+

{error.errorCode}

+
+ )} + +
+ + + {onLogout && ( + + )} +
+ +
+

+ If the problem persists, please contact your administrator or check your network connection. +

+
+
+
+ ); +} + +export default AuthError; diff --git a/src/components/AuthGate.jsx b/src/components/AuthGate.jsx new file mode 100644 index 0000000..4fb9502 --- /dev/null +++ b/src/components/AuthGate.jsx @@ -0,0 +1,191 @@ +import { useEffect, useState, useRef } from 'react'; +import { useMsal, useIsAuthenticated } from '@azure/msal-react'; +import { InteractionStatus } from '@azure/msal-browser'; +import { loginRequest } from '../config/msalConfig'; +import LoginPage from './LoginPage'; +import AuthError from './AuthError'; +import { Loader2 } from 'lucide-react'; + +function AuthGate({ children }) { + const { instance, inProgress } = useMsal(); + const isAuthenticated = useIsAuthenticated(); + const [error, setError] = useState(null); + const [isInitializing, setIsInitializing] = useState(true); + const initializationAttempted = useRef(false); + + useEffect(() => { + const initializeAuth = async () => { + // Prevent multiple initialization attempts + if (initializationAttempted.current) { + return; + } + + // Wait for any ongoing interaction to complete + if (inProgress !== InteractionStatus.None) { + return; + } + + initializationAttempted.current = true; + + try { + // Clear any stale interaction state on mount + const keys = Object.keys(sessionStorage); + keys.forEach(key => { + if (key.includes('interaction.status')) { + sessionStorage.removeItem(key); + } + }); + + // Wait for MSAL to finish initialization + await instance.initialize(); + + // Handle redirect promise (for returning from Microsoft login) + const redirectResponse = await instance.handleRedirectPromise(); + + if (redirectResponse) { + console.log('Login successful via redirect'); + setIsInitializing(false); + return; + } + + // If not authenticated and no interaction in progress, attempt silent SSO + if (!isAuthenticated) { + const accounts = instance.getAllAccounts(); + + if (accounts.length > 0) { + try { + // Attempt silent SSO with existing account + console.log('Attempting silent SSO...'); + await instance.ssoSilent({ + ...loginRequest, + account: accounts[0], + }); + console.log('Silent SSO successful'); + } catch (silentError) { + // Silent SSO failed - user will need to login interactively + console.log('Silent SSO failed:', silentError.errorCode || silentError.message); + // Not setting error here as this is expected for new users + } + } + } + + setIsInitializing(false); + } catch (err) { + console.error('Auth initialization error:', err); + setError(err); + setIsInitializing(false); + } + }; + + initializeAuth(); + }, [instance, isAuthenticated, inProgress]); + + const clearInteractionState = () => { + try { + // Clear any stale interaction state from session storage + const keys = Object.keys(sessionStorage); + keys.forEach(key => { + if (key.includes('interaction.status')) { + sessionStorage.removeItem(key); + console.log('Cleared stale interaction state:', key); + } + }); + } catch (err) { + console.warn('Failed to clear interaction state:', err); + } + }; + + const handleLogin = async () => { + try { + // Clear any stale interaction state first + clearInteractionState(); + + // Give MSAL a moment to update its internal state + await new Promise(resolve => setTimeout(resolve, 100)); + + // Check if interaction is still in progress after clearing + if (inProgress !== InteractionStatus.None) { + console.warn('Authentication still in progress after clearing state. Forcing clear...'); + // Force clear and try again + clearInteractionState(); + await new Promise(resolve => setTimeout(resolve, 200)); + } + + setError(null); + console.log('Starting login redirect...'); + // Use redirect flow for authentication + await instance.loginRedirect(loginRequest); + } catch (err) { + console.error('Login error:', err); + + // If it's an interaction_in_progress error, clear state and set a helpful error + if (err.errorCode === 'interaction_in_progress') { + clearInteractionState(); + setError({ + ...err, + message: 'Authentication was interrupted. Please try again.', + errorCode: 'interaction_in_progress' + }); + } else { + setError(err); + } + } + }; + + const handleRetry = () => { + setError(null); + // Clear interaction state before retrying + clearInteractionState(); + // Wait a bit before retrying + setTimeout(() => { + handleLogin(); + }, 300); + }; + + const handleLogout = async () => { + try { + setError(null); + // Clear interaction state before logout + clearInteractionState(); + await instance.logoutRedirect(); + } catch (err) { + console.error('Logout error:', err); + // Clear all session storage and force page reload + sessionStorage.clear(); + window.location.reload(); + } + }; + + // Show loading state during initialization or authentication + if (isInitializing || inProgress !== InteractionStatus.None) { + return ( +
+
+ +

Authenticating...

+
+
+ ); + } + + // Show error if authentication failed + if (error) { + return ( + + ); + } + + // Show login page if not authenticated + if (!isAuthenticated) { + return ; + } + + // User is authenticated - render the protected content + return children; +} + +export default AuthGate; diff --git a/src/components/LoginPage.jsx b/src/components/LoginPage.jsx new file mode 100644 index 0000000..7c8b801 --- /dev/null +++ b/src/components/LoginPage.jsx @@ -0,0 +1,53 @@ +import { LogIn, Shield } from 'lucide-react'; + +function LoginPage({ onLogin }) { + return ( +
+
+
+
+
+ +
+
+

+ Prompt Studio +

+

+ AI-Powered Cinematography Prompts +

+
+ +
+

+ Welcome +

+

+ Sign in with your Microsoft account to continue +

+ + + +
+
+ + Secured by Microsoft Azure Active Directory +
+
+
+ +

+ By signing in, you agree to use this application in accordance with your organization's policies. +

+
+
+ ); +} + +export default LoginPage; diff --git a/src/config/msalConfig.js b/src/config/msalConfig.js new file mode 100644 index 0000000..98c0e8d --- /dev/null +++ b/src/config/msalConfig.js @@ -0,0 +1,63 @@ +// MSAL Configuration for Microsoft SSO Authentication +// Environment variables are loaded from .env file (must be prefixed with VITE_) + +// Validate required environment variables +const clientId = import.meta.env.VITE_MSAL_CLIENT_ID; +const tenantId = import.meta.env.VITE_MSAL_TENANT_ID; +const redirectUri = import.meta.env.VITE_MSAL_REDIRECT_URI; + +if (!clientId || !tenantId || !redirectUri) { + console.error('MSAL Configuration Error: Missing required environment variables'); + console.error('Required variables:'); + console.error(`- VITE_MSAL_CLIENT_ID: ${clientId ? '✓' : '✗ MISSING'}`); + console.error(`- VITE_MSAL_TENANT_ID: ${tenantId ? '✓' : '✗ MISSING'}`); + console.error(`- VITE_MSAL_REDIRECT_URI: ${redirectUri ? '✓' : '✗ MISSING'}`); + console.error('Please check your .env file and ensure all MSAL variables are set.'); +} + +export const msalConfig = { + auth: { + clientId: clientId || "", + authority: `https://login.microsoftonline.com/${tenantId || ""}`, + redirectUri: redirectUri || window.location.origin, + }, + cache: { + cacheLocation: "sessionStorage", // Store tokens in session storage + storeAuthStateInCookie: true, // Set to true for IE11 or Edge compatibility + }, + system: { + loggerOptions: { + loggerCallback: (level, message, containsPii) => { + if (containsPii) { + return; + } + switch (level) { + case 0: // Error + console.error('[MSAL Error]', message); + break; + case 1: // Warning + console.warn('[MSAL Warning]', message); + break; + case 2: // Info + console.info('[MSAL Info]', message); + break; + case 3: // Verbose + console.debug('[MSAL Debug]', message); + break; + default: + break; + } + }, + }, + }, +}; + +// Scopes for login request +export const loginRequest = { + scopes: ["User.Read"], // Basic authentication - read user profile +}; + +// Optional: Token request for API calls +export const tokenRequest = { + scopes: ["User.Read"], +}; diff --git a/src/main.jsx b/src/main.jsx index b9a1a6d..4c21ee0 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,10 +1,18 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { PublicClientApplication } from '@azure/msal-browser' +import { MsalProvider } from '@azure/msal-react' import './index.css' import App from './App.jsx' +import { msalConfig } from './config/msalConfig' + +// Initialize MSAL instance +const msalInstance = new PublicClientApplication(msalConfig); createRoot(document.getElementById('root')).render( - + + + , ) diff --git a/vite.config.js b/vite.config.js index 8b0f57b..64cb1fb 100644 --- a/vite.config.js +++ b/vite.config.js @@ -4,4 +4,8 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + server: { + port: 3000, + strictPort: true, + }, })