Merge pull request #78 from valueonag/feat/demo-system-readieness

dns switch poweron-center to poweron.swiss
This commit is contained in:
Patrick Motsch 2026-05-08 13:22:30 +02:00 committed by GitHub
commit 3477126d9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 68 additions and 609 deletions

View file

@ -27,7 +27,7 @@ jobs:
- name: Copy integration environment file - name: Copy integration environment file
run: | run: |
cp config/.env.int .env cp config/env-poweron-nyla-int.env .env
- name: Install dependencies - name: Install dependencies
run: | run: |

View file

@ -27,7 +27,7 @@ jobs:
- name: Copy production environment file - name: Copy production environment file
run: | run: |
cp config/.env.prod .env cp config/env-poweron-nyla-prod.env .env
- name: Install dependencies - name: Install dependencies
run: | run: |

6
.gitignore vendored
View file

@ -30,7 +30,5 @@ dist-ssr
.cursorignore .cursorignore
# Keep environment template files in config/ # Keep environment files in config/ (naming: env-<workflow>.env)
!config/.env.dev !config/env-*.env
!config/.env.int
!config/.env.prod

View file

@ -5,9 +5,9 @@
```mermaid ```mermaid
graph TB graph TB
%% Environment Files %% Environment Files
ENV_DEV[".env.dev<br/>Development"] ENV_DEV["env-poweron-nyla-dev.env<br/>Development"]
ENV_PROD[".env.prod<br/>Production"] ENV_PROD["env-poweron-nyla-prod.env<br/>Production"]
ENV_INT[".env.int<br/>Integration"] ENV_INT["env-poweron-nyla-int.env<br/>Integration"]
%% Configuration System %% Configuration System
CONFIG_TS["config.ts<br/>TypeScript Config<br/>(React Frontend)"] CONFIG_TS["config.ts<br/>TypeScript Config<br/>(React Frontend)"]
@ -114,30 +114,25 @@ The app uses a **dual configuration system** to handle environment variables acr
- **Used by:** Express servers and build scripts - **Used by:** Express servers and build scripts
### Environment Files ### Environment Files
- **`config/.env.dev`** - Development environment variables
- **Why:** Separate config for local development with debug settings
- **How:** Copied to root `.env` by `npm run dev` command
- **Contains:** Local API URLs, debug flags, dev-specific settings
- **`config/.env.prod`** - Production environment variables Naming convention: `env-<workflow-name>.env` — matches the GitHub Actions workflow that uses it.
- **Why:** Production-specific settings (live API URLs, optimized settings)
- **How:** Copied to root `.env` by GitHub Actions workflow
- **Contains:** Production API URLs, security settings, performance configs
- **`config/.env.int`** - Integration environment variables - **`config/env-poweron-nyla-dev.env`** — Local development (localhost gateway)
- **Why:** Testing environment that mirrors production but with test data - **`config/env-poweron-nyla-int.env`** — Integration (used by `poweron_nyla_int` workflow)
- **How:** Copied to root `.env` by integration deployment workflow - **`config/env-poweron-nyla-prod.env`** — Production (used by `poweron_nyla_main` workflow)
- **Contains:** Staging API URLs, test user credentials, integration settings
Each env is copied to root `.env` at build time (by CI or manually for local dev).
### Usage ### Usage
```bash ```bash
# Development (loads .env.dev) # Local development — copy env then start Vite
cp config/env-poweron-nyla-dev.env .env
npm run dev npm run dev
# Production build (loads .env.prod) # Production build (CI copies env-poweron-nyla-prod.env → .env)
npm run build:prod npm run build:prod
# Integration build (loads .env.int) # Integration build (CI copies env-poweron-nyla-int.env → .env)
npm run build:int npm run build:int
``` ```

View file

@ -1,34 +0,0 @@
# Development Environment Configuration
# Frontend Nyla - Development
# API Configuration
VITE_API_BASE_URL="http://localhost:8000/"
VITE_API_TIMEOUT=10000
# Microsoft Entra ID Configuration
VITE_MICROSOFT_CLIENT_ID=24cd6c8a-b592-4905-a5ba-d5fa9f911154
VITE_MICROSOFT_TENANT_ID=6a51aaeb-2467-4186-9504-2a05aedc591f
VITE_ENTRA_CLIENT_SECRET=2iw8Q~jwqG1iacxHopBt5pstu6R45UC1gIQabcbD
VITE_ENTRA_AUTHORITY=https://login.microsoftonline.com/6a51aaeb-2467-4186-9504-2a05aedc591f
VITE_ENTRA_REDIRECT_PATH=/auth/callback/
VITE_ENTRA_REDIRECT_URI=http://localhost:8000/api/msft/auth/callback/
# Application Configuration
VITE_APP_NAME=PowerOn Nyla dev
VITE_APP_VERSION=0.0.0
VITE_APP_ENVIRONMENT=development
# Debug Configuration
VITE_DEBUG=true
VITE_LOG_LEVEL=debug
VITE_ENABLE_CONSOLE_LOGS=true
# Feature Flags
VITE_ENABLE_ANALYTICS=false
VITE_ENABLE_ERROR_REPORTING=false
VITE_ENABLE_PERFORMANCE_MONITORING=false
# Development Server
VITE_DEV_SERVER_PORT=5176
VITE_DEV_SERVER_HOST=localhost
VITE_DEV_SERVER_HTTPS=false

View file

@ -1,33 +0,0 @@
# Integration/Test Environment Configuration
# Frontend Nyla - Integration
# API Configuration
VITE_API_BASE_URL=https://gateway-int.poweron-center.net
VITE_API_TIMEOUT=12000
# Microsoft Entra ID Configuration
VITE_MICROSOFT_CLIENT_ID=24cd6c8a-b592-4905-a5ba-d5fa9f911154
VITE_MICROSOFT_TENANT_ID=6a51aaeb-2467-4186-9504-2a05aedc591f
VITE_ENTRA_CLIENT_SECRET=2iw8Q~jwqG1iacxHopBt5pstu6R45UC1gIQabcbD
VITE_ENTRA_AUTHORITY=https://login.microsoftonline.com/6a51aaeb-2467-4186-9504-2a05aedc591f
VITE_ENTRA_REDIRECT_PATH=/auth/callback/
VITE_ENTRA_REDIRECT_URI=https://gateway-int.poweron-center.net/api/msft/auth/callback/
# Application Configuration
VITE_APP_NAME=Poweron Nyla int
VITE_APP_VERSION=0.0.0
VITE_APP_ENVIRONMENT=integration
# Debug Configuration
VITE_DEBUG=true
VITE_LOG_LEVEL=info
VITE_ENABLE_CONSOLE_LOGS=true
# Feature Flags
VITE_ENABLE_ANALYTICS=true
VITE_ENABLE_ERROR_REPORTING=true
VITE_ENABLE_PERFORMANCE_MONITORING=true
# Test Configuration
VITE_ENABLE_MOCK_DATA=false
VITE_ENABLE_TEST_MODE=true

View file

@ -1,33 +0,0 @@
# Production Environment Configuration
# Frontend Nyla - Production
# API Configuration
VITE_API_BASE_URL=https://gateway-prod.poweron-center.net
VITE_API_TIMEOUT=15000
# Microsoft Entra ID Configuration
VITE_MICROSOFT_CLIENT_ID=24cd6c8a-b592-4905-a5ba-d5fa9f911154
VITE_MICROSOFT_TENANT_ID=6a51aaeb-2467-4186-9504-2a05aedc591f
VITE_ENTRA_CLIENT_SECRET=2iw8Q~jwqG1iacxHopBt5pstu6R45UC1gIQabcbD
VITE_ENTRA_AUTHORITY=https://login.microsoftonline.com/6a51aaeb-2467-4186-9504-2a05aedc591f
VITE_ENTRA_REDIRECT_PATH=/auth/callback/
VITE_ENTRA_REDIRECT_URI=https://gateway-prod.poweron-center.net/api/msft/auth/callback/
# Application Configuration
VITE_APP_NAME=PowerOn Nyla
VITE_APP_VERSION=0.0.0
VITE_APP_ENVIRONMENT=production
# Debug Configuration
VITE_DEBUG=false
VITE_LOG_LEVEL=error
VITE_ENABLE_CONSOLE_LOGS=false
# Feature Flags
VITE_ENABLE_ANALYTICS=true
VITE_ENABLE_ERROR_REPORTING=true
VITE_ENABLE_PERFORMANCE_MONITORING=true
# Security Configuration
VITE_ENABLE_HTTPS=true
VITE_ENABLE_CSP=true

View file

@ -0,0 +1,6 @@
# Environment: poweron-nyla-dev (local development)
# Consumed by: Vite build (title) + SPA runtime (getApiBaseUrl / getAppName)
# Auth and secrets live on the gateway — never in frontend env.
VITE_API_BASE_URL="http://localhost:8000/"
VITE_APP_NAME=PowerOn Nyla dev

View file

@ -0,0 +1,6 @@
# Environment: poweron-nyla-int (integration)
# Consumed by: Vite build (title) + SPA runtime (getApiBaseUrl / getAppName)
# Auth and secrets live on the gateway — never in frontend env.
VITE_API_BASE_URL=https://gateway-int.poweron.swiss
VITE_APP_NAME=Poweron Nyla int

View file

@ -0,0 +1,6 @@
# Environment: poweron-nyla-prod (production)
# Consumed by: Vite build (title) + SPA runtime (getApiBaseUrl / getAppName)
# Auth and secrets live on the gateway — never in frontend env.
VITE_API_BASE_URL=https://gateway-prod.poweron.swiss
VITE_APP_NAME=PowerOn Nyla

15
env.d.ts vendored
View file

@ -1,15 +1,6 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_API_URL: string readonly VITE_API_BASE_URL?: string
readonly VITE_MICROSOFT_CLIENT_ID: string readonly VITE_APP_NAME?: string
readonly VITE_MICROSOFT_TENANT_ID: string }
readonly VITE_ENTRA_CLIENT_SECRET: string
readonly VITE_ENTRA_AUTHORITY: string
readonly VITE_ENTRA_REDIRECT_PATH: string
readonly VITE_ENTRA_REDIRECT_URI: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

36
package-lock.json generated
View file

@ -8,8 +8,6 @@
"name": "frontend_nyla_new", "name": "frontend_nyla_new",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@azure/msal-browser": "^4.12.0",
"@azure/msal-react": "^3.0.12",
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@types/leaflet": "^1.9.21", "@types/leaflet": "^1.9.21",
"@xstate/react": "^5.0.0", "@xstate/react": "^5.0.0",
@ -101,40 +99,6 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true "dev": true
}, },
"node_modules/@azure/msal-browser": {
"version": "4.16.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.16.0.tgz",
"integrity": "sha512-yF8gqyq7tVnYftnrWaNaxWpqhGQXoXpDfwBtL7UCGlIbDMQ1PUJF/T2xCL6NyDNHoO70qp1xU8GjjYTyNIefkw==",
"license": "MIT",
"dependencies": {
"@azure/msal-common": "15.9.0"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-common": {
"version": "15.9.0",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.9.0.tgz",
"integrity": "sha512-lbz/D+C9ixUG3hiZzBLjU79a0+5ZXCorjel3mwXluisKNH0/rOS/ajm8yi4yI9RP5Uc70CAcs9Ipd0051Oh/kA==",
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-react": {
"version": "3.0.16",
"resolved": "https://registry.npmjs.org/@azure/msal-react/-/msal-react-3.0.16.tgz",
"integrity": "sha512-fIFc3z9UrHoOCG4rApNWMRr83DnQlo+CHfLSPNBQa4rndIkr+XYBpdYDqlzqtmikRf3A+CYNVOQ+lQX6jM0zdw==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@azure/msal-browser": "^4.16.0",
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",

View file

@ -18,8 +18,6 @@
"test:coverage": "vitest run --coverage" "test:coverage": "vitest run --coverage"
}, },
"dependencies": { "dependencies": {
"@azure/msal-browser": "^4.12.0",
"@azure/msal-react": "^3.0.12",
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@types/leaflet": "^1.9.21", "@types/leaflet": "^1.9.21",
"@xstate/react": "^5.0.0", "@xstate/react": "^5.0.0",

View file

@ -25,7 +25,6 @@ import Reset from './pages/Reset';
import { InvitePage } from './pages/InvitePage'; import { InvitePage } from './pages/InvitePage';
// Providers // Providers
import { AuthProvider } from './providers/auth/AuthProvider';
import { ProtectedRoute } from './providers/auth/ProtectedRoute'; import { ProtectedRoute } from './providers/auth/ProtectedRoute';
import { LanguageProvider } from './providers/language/LanguageContext'; import { LanguageProvider } from './providers/language/LanguageContext';
import { ToastProvider } from './contexts/ToastContext'; import { ToastProvider } from './contexts/ToastContext';
@ -71,7 +70,6 @@ function App() {
return ( return (
<LanguageProvider> <LanguageProvider>
<AuthProvider>
<ToastProvider> <ToastProvider>
<VoiceCatalogProvider> <VoiceCatalogProvider>
<WorkflowSelectionProvider> <WorkflowSelectionProvider>
@ -240,7 +238,6 @@ function App() {
</WorkflowSelectionProvider> </WorkflowSelectionProvider>
</VoiceCatalogProvider> </VoiceCatalogProvider>
</ToastProvider> </ToastProvider>
</AuthProvider>
</LanguageProvider> </LanguageProvider>
); );
} }

View file

@ -1,15 +1,12 @@
import { useState } from 'react'; import { useState } from 'react';
import { useMsal } from '@azure/msal-react';
import api from '../api'; import api from '../api';
import { useApiRequest } from './useApi';
import { getApiBaseUrl } from '../../config/config'; import { getApiBaseUrl } from '../../config/config';
import { setUserDataCache, clearUserDataCache, type CachedUserData } from '../utils/userCache'; import { setUserDataCache, clearUserDataCache, type CachedUserData } from '../utils/userCache';
import { import {
loginApi, loginApi,
fetchCurrentUserApi, fetchCurrentUserApi,
registerApi, registerApi,
registerWithMsalApi,
checkUsernameAvailabilityApi, checkUsernameAvailabilityApi,
logoutApi, logoutApi,
requestPasswordResetApi, requestPasswordResetApi,
@ -18,7 +15,6 @@ import {
type RegisterResponse, type RegisterResponse,
type UsernameAvailabilityResponse, type UsernameAvailabilityResponse,
type RegisterData, type RegisterData,
type MsalRegisterData,
type PasswordResetRequestResponse, type PasswordResetRequestResponse,
type PasswordResetResponse type PasswordResetResponse
} from '../api/authApi'; } from '../api/authApi';
@ -408,48 +404,6 @@ export function useGoogleAuth() {
}; };
} }
// Microsoft Registration
export function useMsalRegister() {
const { instance, accounts } = useMsal();
const { request, isLoading, error } = useApiRequest<MsalRegisterData, any>();
const registerWithMsal = async (): Promise<RegisterResponse> => {
try {
if (!accounts || accounts.length === 0) {
// If not signed in with Microsoft, sign in first
await instance.loginPopup({
scopes: ['user.read']
});
}
// Get the current account
const currentAccount = instance.getAllAccounts()[0];
if (!currentAccount) {
throw new Error('No Microsoft account found');
}
// Prepare user data from Microsoft account
const userData: MsalRegisterData = {
username: currentAccount.username,
email: currentAccount.username,
fullName: currentAccount.name || currentAccount.username,
language: 'de'
};
// Register the user through our backend
return await registerWithMsalApi(request, userData);
} catch (error: any) {
throw error;
}
};
return {
registerWithMsal,
error,
isLoading
};
}
// Username availability check // Username availability check
export function useUsernameAvailability() { export function useUsernameAvailability() {
const [isChecking, setIsChecking] = useState(false); const [isChecking, setIsChecking] = useState(false);
@ -568,145 +522,32 @@ export function useLogout() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const _clearLocalState = () => {
clearUserDataCache();
localStorage.removeItem('authToken');
sessionStorage.clear();
document.cookie.split(";").forEach((c) => {
const name = c.split("=")[0].trim();
if (name) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
}
});
};
const logout = async (): Promise<void> => { const logout = async (): Promise<void> => {
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
try { try {
// Call logout endpoint to clear JWT tokens on server
await logoutApi(); await logoutApi();
// Give browser time to process Set-Cookie headers from logout response
// CRITICAL: Wait for browser to process Set-Cookie headers from logout response
// This gives the browser time to clear httpOnly cookies before redirect
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
} catch (err: any) {
// Clear user data cache from sessionStorage setError(err.response?.data?.detail || 'Logout failed');
clearUserDataCache();
// Clear auth authority from sessionStorage
sessionStorage.removeItem('auth_authority');
// Clear MSAL cache tokens from localStorage
// MSAL stores tokens with keys starting with 'msal.'
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && (
key.startsWith('msal.') ||
key === 'auth_token' ||
key === 'refresh_token' ||
key.includes('token') ||
key.includes('auth') ||
key.includes('msal')
)) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => {
localStorage.removeItem(key);
});
// Clear ALL MSAL cache data (including account keys, token keys, version)
const msalKeysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('msal.')) {
msalKeysToRemove.push(key);
}
}
msalKeysToRemove.forEach(key => {
localStorage.removeItem(key);
});
// Clear sessionStorage as well (CSRF tokens, etc.)
sessionStorage.clear();
// Clear cookies as backup (in case backend doesn't clear them properly)
// Note: This only works for cookies that are accessible to JavaScript
const cookies = document.cookie.split(";");
cookies.forEach(function(c) {
const cookieName = c.split("=")[0].trim();
if (cookieName === 'auth_token' || cookieName === 'refresh_token' || cookieName.includes('token') || cookieName.includes('msal')) {
document.cookie = cookieName + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
});
// Redirect to login page
window.location.href = '/login?logout=true';
} catch (error: any) {
let errorMessage = 'Logout failed';
if (error.response) {
errorMessage = error.response.data?.detail || errorMessage;
}
setError(errorMessage);
// Even if logout fails on server, clear local data and redirect
clearUserDataCache();
sessionStorage.removeItem('auth_authority');
// Clear MSAL cache tokens from localStorage
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && (
key.startsWith('msal.') ||
key === 'auth_token' ||
key === 'refresh_token' ||
key.includes('token') ||
key.includes('auth') ||
key.includes('msal')
)) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => {
localStorage.removeItem(key);
});
// Clear ALL MSAL cache data (including account keys, token keys, version)
const msalKeysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('msal.')) {
msalKeysToRemove.push(key);
}
}
msalKeysToRemove.forEach(key => {
localStorage.removeItem(key);
});
// Clear sessionStorage as well
sessionStorage.clear();
// Clear cookies as backup (in case backend doesn't clear them properly)
document.cookie.split(";").forEach(function(c) {
const cookieName = c.split("=")[0].trim();
if (cookieName === 'auth_token' || cookieName === 'refresh_token' || cookieName.includes('token') || cookieName.includes('msal')) {
document.cookie = cookieName + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
});
window.location.href = '/login?logout=true';
} finally { } finally {
_clearLocalState();
setIsLoading(false); setIsLoading(false);
window.location.href = '/login?logout=true';
} }
}; };

View file

@ -172,36 +172,14 @@ export function useCurrentUser() {
// Clear auth authority from sessionStorage // Clear auth authority from sessionStorage
sessionStorage.removeItem('auth_authority'); sessionStorage.removeItem('auth_authority');
// Optional: clear MSAL browser cache only (PowerOn JWT lives in httpOnly cookies + backend). localStorage.removeItem('authToken');
// Do not call msal.logoutRedirect — that signs the user out of Microsoft globally.
for (let i = localStorage.length - 1; i >= 0; i--) {
const key = localStorage.key(i);
if (key && key.startsWith('msal.')) {
localStorage.removeItem(key);
}
}
// Clear cookies as backup (in case backend doesn't clear them properly) document.cookie.split(";").forEach((c) => {
// Note: This only works for cookies that are accessible to JavaScript const name = c.split("=")[0].trim();
console.log('🍪 Checking cookies for cleanup...'); if (name) {
console.log('🍪 All cookies:', document.cookie); document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
const cookies = document.cookie.split(";");
console.log('🍪 Cookie count:', cookies.length);
cookies.forEach(function(c) {
const cookieName = c.split("=")[0].trim();
console.log('🍪 Checking cookie:', cookieName);
if (cookieName === 'auth_token' || cookieName === 'refresh_token' || cookieName.includes('token') || cookieName.includes('msal')) {
console.log('🗑️ Clearing cookie:', cookieName);
document.cookie = cookieName + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
} }
}); });
console.log('🍪 Cookies after cleanup attempt:', document.cookie);
console.log('✅ Cleanup completed');
// Redirect to login or home page // Redirect to login or home page
console.log('🔄 Redirecting to login page...'); console.log('🔄 Redirecting to login page...');

View file

@ -3,7 +3,7 @@ import { useNavigate, useLocation } from 'react-router-dom';
import { FaEnvelopeOpenText } from 'react-icons/fa'; import { FaEnvelopeOpenText } from 'react-icons/fa';
import styles from './Register.module.css'; import styles from './Register.module.css';
import { useRegister, useMsalRegister, useUsernameAvailability } from '../hooks/useAuthentication'; import { useRegister, useUsernameAvailability } from '../hooks/useAuthentication';
import { generateAndStoreCSRFToken } from '../utils/csrfUtils'; import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
import { PENDING_INVITATION_KEY } from './InvitePage'; import { PENDING_INVITATION_KEY } from './InvitePage';
import { LanguageSelector } from '../components/UiComponents/LanguageSelector'; import { LanguageSelector } from '../components/UiComponents/LanguageSelector';
@ -21,7 +21,6 @@ function Register() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { register, error: registerError, isLoading } = useRegister(); const { register, error: registerError, isLoading } = useRegister();
const { error: msalError } = useMsalRegister();
const { checkAvailability, isChecking, error: availabilityError } = useUsernameAvailability(); const { checkAvailability, isChecking, error: availabilityError } = useUsernameAvailability();
const invitationUsername = (location.state as any)?.invitationUsername || ''; const invitationUsername = (location.state as any)?.invitationUsername || '';
const invitationEmail = (location.state as any)?.invitationEmail || ''; const invitationEmail = (location.state as any)?.invitationEmail || '';
@ -118,7 +117,6 @@ function Register() {
const _getErrorMessage = () => { const _getErrorMessage = () => {
if (validationError) return validationError; if (validationError) return validationError;
if (registerError) return typeof registerError === 'string' ? registerError : t('Registrierung fehlgeschlagen'); if (registerError) return typeof registerError === 'string' ? registerError : t('Registrierung fehlgeschlagen');
if (msalError) return typeof msalError === 'string' ? msalError : t('Microsoft-Registrierung fehlgeschlagen');
if (availabilityError) return typeof availabilityError === 'string' ? availabilityError : t('Benutzernamen-Prüfung fehlgeschlagen'); if (availabilityError) return typeof availabilityError === 'string' ? availabilityError : t('Benutzernamen-Prüfung fehlgeschlagen');
return null; return null;
}; };

View file

@ -1,99 +0,0 @@
import {
AuthenticationResult,
EventType,
PublicClientApplication
} from "@azure/msal-browser";
import { msalConfig } from "./authConfig";
import { MsalProvider } from "@azure/msal-react";
import { ReactNode, useEffect, useState } from "react";
import { useLanguage } from "../language/LanguageContext";
interface AuthProviderProps {
children: ReactNode;
}
export const AuthProvider = ({ children }: AuthProviderProps) => {
const { t } = useLanguage();
const [msalInstance, setMsalInstance] = useState<PublicClientApplication | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
const msalApp = new PublicClientApplication(msalConfig);
const initializeMsal = async () => {
try {
// Set event handlers first, so we catch all events
msalApp.addEventCallback((event) => {
if (event.eventType === EventType.LOGIN_SUCCESS) {
const payload = event?.payload as AuthenticationResult;
if (payload?.account) {
msalApp.setActiveAccount(payload.account);
console.log("MSAL login successful");
// Store authentication authority for backend communication
if (payload.account?.environment) {
sessionStorage.setItem('auth_authority', payload.account.environment);
}
console.log('✅ MSAL login successful - tokens will be set in httpOnly cookies by backend');
}
} else if (event.eventType === EventType.LOGIN_FAILURE) {
console.error("MSAL login failed:", event.error);
}
});
// Initialize MSAL
await msalApp.initialize();
msalApp.enableAccountStorageEvents();
// Handle any redirect response
const response = await msalApp.handleRedirectPromise();
if (response) {
// If we have a response, we've completed a redirect flow
console.log("MSAL redirect completed successfully");
if (response.account) {
msalApp.setActiveAccount(response.account);
// Store authentication authority
if (response.account.environment) {
sessionStorage.setItem('auth_authority', response.account.environment);
}
console.log('✅ MSAL redirect completed - tokens will be set in httpOnly cookies by backend');
}
}
// Check for accounts
const accounts = msalApp.getAllAccounts();
if (accounts.length > 0) {
msalApp.setActiveAccount(accounts[0]);
// Store authentication authority for existing accounts
if (accounts[0].environment) {
sessionStorage.setItem('auth_authority', accounts[0].environment);
}
console.log('✅ MSAL account found - tokens will be set in httpOnly cookies by backend');
}
setMsalInstance(msalApp);
setIsInitialized(true);
} catch (err) {
console.error("MSAL initialization failed", err);
}
};
initializeMsal();
}, []);
if (!isInitialized || !msalInstance) {
return <div>{t('Authentifizierung wird geladen…')}</div>;
}
return <MsalProvider instance={msalInstance}>{children}</MsalProvider>;
};
export function useAuthProvider() {
return { AuthProvider };
}

View file

@ -1,4 +1,3 @@
import { useMsal } from "@azure/msal-react";
import { Navigate, useLocation } from "react-router-dom"; import { Navigate, useLocation } from "react-router-dom";
import { ReactNode, useEffect, useState } from "react"; import { ReactNode, useEffect, useState } from "react";
import { useLanguage } from "../language/LanguageContext"; import { useLanguage } from "../language/LanguageContext";
@ -13,110 +12,36 @@ export const ProtectedRoute = ({
redirectPath = "/login" redirectPath = "/login"
}: ProtectedRouteProps) => { }: ProtectedRouteProps) => {
const { t } = useLanguage(); const { t } = useLanguage();
const { accounts } = useMsal();
const location = useLocation(); const location = useLocation();
const [isChecking, setIsChecking] = useState(true); const [isChecking, setIsChecking] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false); const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => { useEffect(() => {
const checkAuthentication = async () => { const authAuthority = sessionStorage.getItem('auth_authority');
try { setIsAuthenticated(!!authAuthority);
// Check for MSAL authentication setIsChecking(false);
const hasMsalAccount = accounts.length > 0; }, []);
// Check for backend authentication via API call
let hasBackendAuth = false;
try {
// Check for authentication authority (httpOnly cookies are handled automatically)
const authAuthority = sessionStorage.getItem('auth_authority');
console.log('🔍 Checking auth authority:', authAuthority);
if (authAuthority) {
hasBackendAuth = true;
console.log('✅ Authenticated with backend (httpOnly cookies), authority:', authAuthority);
} else {
hasBackendAuth = false;
console.log('❌ No authentication authority found');
}
} catch (error) {
console.log('❌ Backend authentication failed:', error);
hasBackendAuth = false;
}
// User is authenticated if either method is valid
const isAuth = hasMsalAccount || hasBackendAuth;
setIsAuthenticated(isAuth);
console.log('🔐 Authentication status:', {
hasMsalAccount,
hasBackendAuth,
isAuthenticated: isAuth,
authAuthority: sessionStorage.getItem('auth_authority')
});
if (hasBackendAuth) {
console.log('✅ Authenticated with backend cookies');
} else if (hasMsalAccount) {
console.log('✅ Authenticated with MSAL');
} else {
console.log('❌ No valid authentication found');
}
} catch (error) {
console.error('❌ Error checking authentication:', error);
setIsAuthenticated(false);
} finally {
setIsChecking(false);
}
};
// Small delay to ensure MSAL is initialized and localStorage is updated
const timer = setTimeout(() => {
checkAuthentication();
}, 200);
return () => clearTimeout(timer);
}, [accounts]);
// Re-check authentication when component mounts or accounts change
// This handles cases where auth_authority is set after initial mount
useEffect(() => { useEffect(() => {
if (!isChecking) { if (!isChecking) {
// Double-check authentication state periodically when not initially loading
const recheckTimer = setTimeout(() => { const recheckTimer = setTimeout(() => {
const authAuthority = sessionStorage.getItem('auth_authority'); const authAuthority = sessionStorage.getItem('auth_authority');
const hasMsalAccount = accounts.length > 0; const isAuth = !!authAuthority;
const hasBackendAuth = !!authAuthority;
const isAuth = hasMsalAccount || hasBackendAuth;
// Only update if authentication state actually changed
if (isAuth !== isAuthenticated) { if (isAuth !== isAuthenticated) {
console.log('🔄 Authentication state changed, updating...', {
previous: isAuthenticated,
current: isAuth,
authAuthority,
hasMsalAccount,
hasBackendAuth
});
setIsAuthenticated(isAuth); setIsAuthenticated(isAuth);
} }
}, 300); }, 300);
return () => clearTimeout(recheckTimer); return () => clearTimeout(recheckTimer);
} }
}, [isChecking, isAuthenticated, accounts]); }, [isChecking, isAuthenticated]);
// If still checking, show loading
if (isChecking) { if (isChecking) {
return <div>{t('Authentifizierung wird geprüft…')}</div>; return <div>{t('Authentifizierung wird geprüft…')}</div>;
} }
// Check if user is authenticated through either method
if (!isAuthenticated) { if (!isAuthenticated) {
console.log("No valid authentication found, redirecting to login");
return <Navigate to={redirectPath} state={{ from: location }} replace />; return <Navigate to={redirectPath} state={{ from: location }} replace />;
} }
console.log("User is authenticated, rendering protected content");
return <>{children}</>; return <>{children}</>;
}; };

View file

@ -1,45 +0,0 @@
import { LogLevel } from '@azure/msal-browser';
export const msalConfig = {
auth: {
clientId: '24cd6c8a-b592-4905-a5ba-d5fa9f911154',
authority: 'https://login.microsoftonline.com/6a51aaeb-2467-4186-9504-2a05aedc591f/',
redirectUri: '/',
postLogoutRedirectUri: '/',
navigateToLoginRequestUrl: false,
},
cache: {
cacheLocation: 'localStorage',
storeAuthStateInCookie: false,
},
system: {
loggerOptions: {
loggerCallback: (level: any, message: any, containsPii: any) => {
if (containsPii) {
return;
}
switch (level) {
case LogLevel.Error:
console.error(message);
return;
case LogLevel.Info:
console.info(message);
return;
case LogLevel.Verbose:
console.debug(message);
return;
case LogLevel.Warning:
console.warn(message);
return;
default:
return;
}
},
},
},
};
export const loginRequest = {
scopes: ["openid", "profile", "email", "api://24cd6c8a-b592-4905-a5ba-d5fa9f911154/user_impersonation"],
};