480 lines
No EOL
13 KiB
TypeScript
480 lines
No EOL
13 KiB
TypeScript
import { useState } from 'react';
|
|
|
|
import { useMsal } from '@azure/msal-react';
|
|
import api from '../api';
|
|
import { useApiRequest } from './useApi';
|
|
|
|
// Regular authentication
|
|
interface LoginResponse {
|
|
accessToken: string;
|
|
tokenType: string;
|
|
label?: any;
|
|
fieldLabels?: any;
|
|
}
|
|
|
|
export function useAuth() {
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const login = async (username: string, password: string): Promise<LoginResponse> => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// Create the form data in the exact format FastAPI expects
|
|
const params = new URLSearchParams();
|
|
params.append('username', username);
|
|
params.append('password', password);
|
|
params.append('grant_type', 'password');
|
|
params.append('scope', '');
|
|
params.append('client_id', '');
|
|
params.append('client_secret', '');
|
|
|
|
// Generate a simple CSRF token (in production, this should come from the server)
|
|
const csrfToken = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
|
|
// Use the existing api instance with custom headers for this request
|
|
const response = await api.post('/api/local/login', params, {
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-CSRF-Token': csrfToken
|
|
}
|
|
});
|
|
|
|
// Normalize the response structure to match what the frontend expects
|
|
let normalizedAuthData;
|
|
if (response.data.token_data) {
|
|
// Backend returns token_data with tokenAccess field, normalize to accessToken
|
|
normalizedAuthData = {
|
|
accessToken: response.data.token_data.tokenAccess || response.data.access_token,
|
|
tokenType: response.data.token_data.tokenType || 'bearer',
|
|
userId: response.data.token_data.userId,
|
|
expiresAt: response.data.token_data.expiresAt,
|
|
createdAt: response.data.token_data.createdAt
|
|
};
|
|
} else {
|
|
// Fallback to old structure if needed
|
|
normalizedAuthData = {
|
|
accessToken: response.data.access_token,
|
|
tokenType: response.data.token_type || 'bearer'
|
|
};
|
|
}
|
|
|
|
// Store the normalized auth response
|
|
localStorage.setItem('auth_data', JSON.stringify(normalizedAuthData));
|
|
|
|
return {
|
|
accessToken: normalizedAuthData.accessToken,
|
|
tokenType: normalizedAuthData.tokenType
|
|
};
|
|
} catch (error: any) {
|
|
let errorMessage = 'An error occurred during login';
|
|
|
|
if (error.response) {
|
|
errorMessage = error.response.data?.detail || 'Invalid username or password';
|
|
} else if (error.request) {
|
|
errorMessage = 'No response received from server';
|
|
} else {
|
|
errorMessage = error.message;
|
|
}
|
|
|
|
setError(errorMessage);
|
|
throw error;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return {
|
|
login,
|
|
error,
|
|
isLoading
|
|
};
|
|
}
|
|
|
|
// Microsoft Authentication
|
|
interface MsalAuthResponse {
|
|
accessToken: string;
|
|
tokenType: string;
|
|
user: {
|
|
username: string;
|
|
email: string;
|
|
fullName: string;
|
|
mandateId: number;
|
|
};
|
|
}
|
|
|
|
export function useMsalAuth() {
|
|
const [msalError, setMsalError] = useState<string | null>(null);
|
|
const [isMsalLoading, setIsMsalLoading] = useState(false);
|
|
|
|
const loginWithMsal = async (): Promise<MsalAuthResponse> => {
|
|
setIsMsalLoading(true);
|
|
setMsalError(null);
|
|
|
|
try {
|
|
return new Promise((resolve, reject) => {
|
|
// Open popup to backend Microsoft login route
|
|
const popup = window.open(
|
|
`${import.meta.env.VITE_API_BASE_URL}/api/msft/login?state=login`,
|
|
'msft-login',
|
|
'width=500,height=600,scrollbars=yes,resizable=yes'
|
|
);
|
|
|
|
if (!popup) {
|
|
setMsalError('Popup was blocked. Please allow popups and try again.');
|
|
setIsMsalLoading(false);
|
|
reject(new Error('Popup was blocked'));
|
|
return;
|
|
}
|
|
|
|
// Listen for messages from the popup
|
|
const messageListener = (event: MessageEvent) => {
|
|
// Verify origin for security
|
|
const apiUrl = new URL(import.meta.env.VITE_API_BASE_URL);
|
|
if (event.origin !== apiUrl.origin) {
|
|
return;
|
|
}
|
|
|
|
if (event.data.type === 'msft_auth_success') {
|
|
// Store the auth data with normalized field names
|
|
if (event.data.token_data) {
|
|
const normalizedTokenData = {
|
|
accessToken: event.data.token_data.tokenAccess, // Convert tokenAccess to accessToken
|
|
tokenType: event.data.token_data.tokenType,
|
|
userId: event.data.token_data.userId,
|
|
expiresAt: event.data.token_data.expiresAt,
|
|
createdAt: event.data.token_data.createdAt
|
|
};
|
|
localStorage.setItem('auth_data', JSON.stringify(normalizedTokenData));
|
|
}
|
|
|
|
// Clean up
|
|
window.removeEventListener('message', messageListener);
|
|
popup.close();
|
|
setIsMsalLoading(false);
|
|
|
|
// Resolve with the token data
|
|
resolve({
|
|
accessToken: event.data.token_data.tokenAccess,
|
|
tokenType: event.data.token_data.tokenType || 'bearer',
|
|
user: {
|
|
username: '', // Will be populated by the backend
|
|
email: '',
|
|
fullName: '',
|
|
mandateId: 0
|
|
}
|
|
});
|
|
} else if (event.data.type === 'msft_connection_error') {
|
|
// Handle error
|
|
window.removeEventListener('message', messageListener);
|
|
popup.close();
|
|
setIsMsalLoading(false);
|
|
setMsalError(event.data.error || 'Microsoft authentication failed');
|
|
reject(new Error(event.data.error || 'Microsoft authentication failed'));
|
|
}
|
|
};
|
|
|
|
// Add message listener
|
|
window.addEventListener('message', messageListener);
|
|
|
|
// Handle popup closing without completing auth
|
|
const checkClosed = setInterval(() => {
|
|
if (popup.closed) {
|
|
clearInterval(checkClosed);
|
|
window.removeEventListener('message', messageListener);
|
|
setIsMsalLoading(false);
|
|
setMsalError('Authentication was cancelled');
|
|
reject(new Error('Authentication was cancelled'));
|
|
}
|
|
}, 1000);
|
|
});
|
|
} catch (error: any) {
|
|
setMsalError(error.message || 'Microsoft authentication failed');
|
|
setIsMsalLoading(false);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
return {
|
|
loginWithMsal,
|
|
error: msalError,
|
|
isLoading: isMsalLoading
|
|
};
|
|
}
|
|
|
|
// Registration
|
|
interface RegisterData {
|
|
username: string;
|
|
password: string;
|
|
email: string;
|
|
fullName: string;
|
|
language?: string;
|
|
enabled?: boolean;
|
|
privilege?: string;
|
|
}
|
|
|
|
interface RegisterResponse {
|
|
success: boolean;
|
|
message?: string;
|
|
user?: {
|
|
id: string;
|
|
username: string;
|
|
email: string;
|
|
fullName: string;
|
|
language: string;
|
|
enabled: boolean;
|
|
privilege: string;
|
|
};
|
|
}
|
|
|
|
export function useRegister() {
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const register = async (userData: RegisterData): Promise<RegisterResponse> => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// Prepare data to match backend expectations
|
|
// Backend expects userData as object and password as embedded field
|
|
const dataToSend = {
|
|
userData: {
|
|
username: userData.username,
|
|
email: userData.email,
|
|
fullName: userData.fullName,
|
|
language: userData.language || 'de',
|
|
enabled: userData.enabled !== undefined ? userData.enabled : true,
|
|
privilege: userData.privilege || 'user'
|
|
},
|
|
password: userData.password
|
|
};
|
|
|
|
const response = await api.post('/api/local/register', dataToSend, {
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
message: 'Registration successful',
|
|
user: response.data
|
|
};
|
|
} catch (error: any) {
|
|
let errorMessage = 'An error occurred during registration';
|
|
|
|
if (error.response) {
|
|
// Handle validation errors from FastAPI
|
|
if (error.response.data?.detail) {
|
|
if (Array.isArray(error.response.data.detail)) {
|
|
// Handle FastAPI validation errors array
|
|
errorMessage = error.response.data.detail.map((err: any) => err.msg).join(', ');
|
|
} else {
|
|
errorMessage = error.response.data.detail;
|
|
}
|
|
} else {
|
|
errorMessage = 'Registration failed';
|
|
}
|
|
} else if (error.request) {
|
|
errorMessage = 'No response received from server';
|
|
} else {
|
|
errorMessage = error.message;
|
|
}
|
|
|
|
setError(errorMessage);
|
|
throw error;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return {
|
|
register,
|
|
error,
|
|
isLoading
|
|
};
|
|
}
|
|
|
|
// Microsoft Registration
|
|
interface MsalRegisterData {
|
|
username: string;
|
|
email: string;
|
|
fullName: string;
|
|
language?: string;
|
|
}
|
|
|
|
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
|
|
const response = await request({
|
|
url: '/api/msft/register',
|
|
method: 'post',
|
|
data: userData,
|
|
additionalConfig: {
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
}
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
message: 'Registration successful',
|
|
user: response
|
|
};
|
|
} catch (error: any) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
return {
|
|
registerWithMsal,
|
|
error,
|
|
isLoading
|
|
};
|
|
}
|
|
|
|
// Username availability check
|
|
export function useUsernameAvailability() {
|
|
const [isChecking, setIsChecking] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const checkAvailability = async (username: string, authenticationAuthority: string = 'local'): Promise<{
|
|
username: string;
|
|
authenticationAuthority: string;
|
|
available: boolean;
|
|
message: string;
|
|
}> => {
|
|
setIsChecking(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await api.get('/api/local/available', {
|
|
params: {
|
|
username,
|
|
authenticationAuthority
|
|
}
|
|
});
|
|
|
|
return response.data;
|
|
} catch (error: any) {
|
|
let errorMessage = 'Failed to check username availability';
|
|
|
|
if (error.response) {
|
|
errorMessage = error.response.data?.detail || errorMessage;
|
|
}
|
|
|
|
setError(errorMessage);
|
|
throw error;
|
|
} finally {
|
|
setIsChecking(false);
|
|
}
|
|
};
|
|
|
|
return {
|
|
checkAvailability,
|
|
isChecking,
|
|
error
|
|
};
|
|
}
|
|
|
|
// Logout function
|
|
export function useLogout() {
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const logout = async (): Promise<void> => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
await api.post('/api/local/logout');
|
|
|
|
// Clear local storage
|
|
localStorage.removeItem('auth_data');
|
|
|
|
// Redirect to login page
|
|
window.location.href = '/login';
|
|
} catch (error: any) {
|
|
let errorMessage = 'Logout failed';
|
|
|
|
if (error.response) {
|
|
errorMessage = error.response.data?.detail || errorMessage;
|
|
}
|
|
|
|
setError(errorMessage);
|
|
throw error;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return {
|
|
logout,
|
|
isLoading,
|
|
error
|
|
};
|
|
}
|
|
|
|
// Get current user
|
|
export function useCurrentUser() {
|
|
const [user, setUser] = useState<any>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const getCurrentUser = async (): Promise<any> => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await api.get('/api/local/me');
|
|
setUser(response.data);
|
|
return response.data;
|
|
} catch (error: any) {
|
|
let errorMessage = 'Failed to get current user';
|
|
|
|
if (error.response) {
|
|
errorMessage = error.response.data?.detail || errorMessage;
|
|
}
|
|
|
|
setError(errorMessage);
|
|
throw error;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return {
|
|
user,
|
|
getCurrentUser,
|
|
isLoading,
|
|
error
|
|
};
|
|
}
|