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 (
+
+ );
+ }
+
+ // 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,
+ },
})