mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 09:06:40 +02:00
Backend-driven Authorization Code Flow with @azure/msal-node: - EntraIdService: MSAL ConfidentialClientApplication, auth URL generation, token exchange - SsoController: /auth/sso/microsoft (initiate) + /auth/sso/microsoft/callback (callback) - AuthService.loginViaSso(): User provisioning (find by OID, auto-link by email, or create new) - CSRF protection via state parameter stored in Redis - SSO status endpoint for frontend feature detection Frontend: - "Mit Microsoft anmelden" button on login page (shown only when SSO is configured) - SsoCallbackPage: handles redirect from backend, sets token, loads user profile - AuthContext.loginWithToken(): new method for SSO token handling Configuration: - AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_REDIRECT_URI env vars - docker-compose.yml updated to pass Azure vars to core service Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
160 lines
3.8 KiB
TypeScript
160 lines
3.8 KiB
TypeScript
import {
|
|
createContext,
|
|
useContext,
|
|
useState,
|
|
useCallback,
|
|
useEffect,
|
|
type ReactNode,
|
|
} from 'react';
|
|
import api, { setAccessToken } from '../api/client';
|
|
|
|
interface User {
|
|
id: string;
|
|
email: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
avatar?: string | null;
|
|
phone?: string | null;
|
|
mobile?: string | null;
|
|
street?: string | null;
|
|
postalCode?: string | null;
|
|
city?: string | null;
|
|
role: string;
|
|
twoFactorEnabled: boolean;
|
|
}
|
|
|
|
interface AuthContextType {
|
|
user: User | null;
|
|
isAuthenticated: boolean;
|
|
isLoading: boolean;
|
|
login: (email: string, password: string, totpCode?: string) => Promise<LoginResult>;
|
|
loginWithToken: (accessToken: string) => Promise<void>;
|
|
logout: () => Promise<void>;
|
|
refreshUser: () => Promise<void>;
|
|
}
|
|
|
|
interface LoginResult {
|
|
success: boolean;
|
|
requiresTwoFactor?: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextType | null>(null);
|
|
|
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
// Beim Start: Silent Refresh versuchen
|
|
useEffect(() => {
|
|
const initAuth = async () => {
|
|
try {
|
|
const { data } = await api.post<{ accessToken: string }>(
|
|
'/auth/refresh',
|
|
);
|
|
setAccessToken(data.accessToken);
|
|
|
|
// User-Profil laden
|
|
const profileResponse = await api.get<User>('/users/me');
|
|
setUser(profileResponse.data);
|
|
} catch {
|
|
// Nicht eingeloggt - normal
|
|
setAccessToken(null);
|
|
setUser(null);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
initAuth();
|
|
}, []);
|
|
|
|
const login = useCallback(
|
|
async (
|
|
email: string,
|
|
password: string,
|
|
totpCode?: string,
|
|
): Promise<LoginResult> => {
|
|
try {
|
|
const { data } = await api.post<{
|
|
accessToken?: string;
|
|
user?: User;
|
|
requiresTwoFactor?: boolean;
|
|
}>('/auth/login', { email, password, totpCode });
|
|
|
|
if (data.requiresTwoFactor) {
|
|
return { success: false, requiresTwoFactor: true };
|
|
}
|
|
|
|
if (data.accessToken && data.user) {
|
|
setAccessToken(data.accessToken);
|
|
setUser(data.user);
|
|
return { success: true };
|
|
}
|
|
|
|
return { success: false, error: 'Unerwartete Antwort vom Server' };
|
|
} catch (err: unknown) {
|
|
const error = err as { response?: { data?: { message?: string } } };
|
|
return {
|
|
success: false,
|
|
error: error.response?.data?.message ?? 'Login fehlgeschlagen',
|
|
};
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
|
|
/**
|
|
* SSO-Login: Access-Token direkt setzen und User-Profil laden.
|
|
* Wird von SsoCallbackPage aufgerufen.
|
|
*/
|
|
const loginWithToken = useCallback(async (token: string) => {
|
|
setAccessToken(token);
|
|
const { data } = await api.get<User>('/users/me');
|
|
setUser(data);
|
|
}, []);
|
|
|
|
const refreshUser = useCallback(async () => {
|
|
try {
|
|
const { data } = await api.get<User>('/users/me');
|
|
setUser(data);
|
|
} catch {
|
|
// Fehler ignorieren - User bleibt unverändert
|
|
}
|
|
}, []);
|
|
|
|
const logout = useCallback(async () => {
|
|
try {
|
|
await api.post('/auth/logout');
|
|
} catch {
|
|
// Fehler ignorieren
|
|
} finally {
|
|
setAccessToken(null);
|
|
setUser(null);
|
|
}
|
|
}, []);
|
|
|
|
return (
|
|
<AuthContext.Provider
|
|
value={{
|
|
user,
|
|
isAuthenticated: !!user,
|
|
isLoading,
|
|
login,
|
|
loginWithToken,
|
|
logout,
|
|
refreshUser,
|
|
}}
|
|
>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useAuth(): AuthContextType {
|
|
const context = useContext(AuthContext);
|
|
if (!context) {
|
|
throw new Error('useAuth muss innerhalb von AuthProvider verwendet werden');
|
|
}
|
|
return context;
|
|
}
|