kdrive fix
This commit is contained in:
parent
34d6c2b83d
commit
9e08953c44
5 changed files with 311 additions and 98 deletions
|
|
@ -136,14 +136,20 @@ export async function createConnection(
|
||||||
/**
|
/**
|
||||||
* Connect to a service (initiate OAuth)
|
* Connect to a service (initiate OAuth)
|
||||||
* Endpoint: POST /api/connections/{connectionId}/connect
|
* Endpoint: POST /api/connections/{connectionId}/connect
|
||||||
|
*
|
||||||
|
* @param reauth If true, forces the OAuth provider to re-show the consent screen.
|
||||||
|
* Required when newly added scopes (e.g. Calendar/Contacts after a
|
||||||
|
* feature rollout) need to be granted on top of the existing token.
|
||||||
*/
|
*/
|
||||||
export async function connectService(
|
export async function connectService(
|
||||||
request: ApiRequestFunction,
|
request: ApiRequestFunction,
|
||||||
connectionId: string
|
connectionId: string,
|
||||||
|
reauth: boolean = false
|
||||||
): Promise<ConnectResponse> {
|
): Promise<ConnectResponse> {
|
||||||
return await request({
|
return await request({
|
||||||
url: `/api/connections/${connectionId}/connect`,
|
url: `/api/connections/${connectionId}/connect`,
|
||||||
method: 'post'
|
method: 'post',
|
||||||
|
data: reauth ? { reauth: true } : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -221,3 +227,28 @@ export async function refreshGoogleToken(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit an Infomaniak Personal Access Token (kdrive + mail) for an existing
|
||||||
|
* UserConnection. The backend validates the token via /1/profile and stores it
|
||||||
|
* as the connection's data-access bearer token.
|
||||||
|
* Endpoint: POST /api/infomaniak/connections/{connectionId}/token
|
||||||
|
*/
|
||||||
|
export async function submitInfomaniakToken(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
connectionId: string,
|
||||||
|
token: string
|
||||||
|
): Promise<{
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
type: string;
|
||||||
|
externalUsername: string;
|
||||||
|
externalEmail?: string | null;
|
||||||
|
lastChecked: number;
|
||||||
|
}> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/infomaniak/connections/${connectionId}/token`,
|
||||||
|
method: 'post',
|
||||||
|
data: { token }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,8 @@ const _SERVICE_ICONS: Record<string, string> = {
|
||||||
clickup: '\uD83D\uDCCB',
|
clickup: '\uD83D\uDCCB',
|
||||||
kdrive: '\uD83D\uDCC2',
|
kdrive: '\uD83D\uDCC2',
|
||||||
mail: '\uD83D\uDCE7',
|
mail: '\uD83D\uDCE7',
|
||||||
|
calendar: '\uD83D\uDCC5',
|
||||||
|
contact: '\uD83D\uDC64',
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ─── Source colors & icons ──────────────────────────────────────────── */
|
/* ─── Source colors & icons ──────────────────────────────────────────── */
|
||||||
|
|
@ -166,6 +168,10 @@ const _SOURCE_COLORS: Record<string, string> = {
|
||||||
kdrive: '#0098FF',
|
kdrive: '#0098FF',
|
||||||
mailFolder: '#0098FF',
|
mailFolder: '#0098FF',
|
||||||
mail: '#0098FF',
|
mail: '#0098FF',
|
||||||
|
calendarFolder: '#0098FF',
|
||||||
|
calendar: '#0098FF',
|
||||||
|
contactFolder: '#0098FF',
|
||||||
|
contact: '#0098FF',
|
||||||
};
|
};
|
||||||
|
|
||||||
function _getSourceColor(sourceType: string): string {
|
function _getSourceColor(sourceType: string): string {
|
||||||
|
|
@ -199,6 +205,8 @@ const _SERVICE_TO_SOURCE_TYPE: Record<string, string> = {
|
||||||
clickup: 'clickup',
|
clickup: 'clickup',
|
||||||
kdrive: 'kdriveFolder',
|
kdrive: 'kdriveFolder',
|
||||||
mail: 'mailFolder',
|
mail: 'mailFolder',
|
||||||
|
calendar: 'calendarFolder',
|
||||||
|
contact: 'contactFolder',
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ─── Tree helpers ───────────────────────────────────────────────────── */
|
/* ─── Tree helpers ───────────────────────────────────────────────────── */
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
updateConnection as updateConnectionApi,
|
updateConnection as updateConnectionApi,
|
||||||
refreshMicrosoftToken as refreshMicrosoftTokenApi,
|
refreshMicrosoftToken as refreshMicrosoftTokenApi,
|
||||||
refreshGoogleToken as refreshGoogleTokenApi,
|
refreshGoogleToken as refreshGoogleTokenApi,
|
||||||
|
submitInfomaniakToken as submitInfomaniakTokenApi,
|
||||||
type Connection,
|
type Connection,
|
||||||
type AttributeDefinition,
|
type AttributeDefinition,
|
||||||
type PaginationParams,
|
type PaginationParams,
|
||||||
|
|
@ -138,10 +139,12 @@ export function useConnections() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connect to a service (initiate OAuth)
|
// Connect to a service (initiate OAuth). Pass reauth=true to force the
|
||||||
const connectService = async (connectionId: string): Promise<ConnectResponse> => {
|
// provider's consent screen so newly added scopes (e.g. Calendar/Contacts)
|
||||||
|
// actually land on the access token instead of being silently skipped.
|
||||||
|
const connectService = async (connectionId: string, reauth: boolean = false): Promise<ConnectResponse> => {
|
||||||
try {
|
try {
|
||||||
const data = await connectServiceApi(request, connectionId);
|
const data = await connectServiceApi(request, connectionId, reauth);
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error connecting service:', error);
|
console.error('Error connecting service:', error);
|
||||||
|
|
@ -237,13 +240,13 @@ export function useConnections() {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connect with popup (OAuth flow)
|
// Connect with popup (OAuth flow)
|
||||||
const connectWithPopup = async (connectionId: string): Promise<void> => {
|
const connectWithPopup = async (connectionId: string, reauth: boolean = false): Promise<void> => {
|
||||||
setIsConnecting(true);
|
setIsConnecting(true);
|
||||||
setConnectError(null);
|
setConnectError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the OAuth URL from backend
|
// Get the OAuth URL from backend
|
||||||
const response = await connectService(connectionId);
|
const response = await connectService(connectionId, reauth);
|
||||||
if (!response.authUrl) {
|
if (!response.authUrl) {
|
||||||
throw new Error('No OAuth URL received from backend');
|
throw new Error('No OAuth URL received from backend');
|
||||||
}
|
}
|
||||||
|
|
@ -295,8 +298,7 @@ export function useConnections() {
|
||||||
if (
|
if (
|
||||||
event.data.type === 'msft_connection_success' ||
|
event.data.type === 'msft_connection_success' ||
|
||||||
event.data.type === 'google_connection_success' ||
|
event.data.type === 'google_connection_success' ||
|
||||||
event.data.type === 'clickup_connection_success' ||
|
event.data.type === 'clickup_connection_success'
|
||||||
event.data.type === 'infomaniak_connection_success'
|
|
||||||
) {
|
) {
|
||||||
// Clean up
|
// Clean up
|
||||||
clearInterval(checkClosed);
|
clearInterval(checkClosed);
|
||||||
|
|
@ -310,8 +312,7 @@ export function useConnections() {
|
||||||
} else if (
|
} else if (
|
||||||
event.data.type === 'msft_connection_error' ||
|
event.data.type === 'msft_connection_error' ||
|
||||||
event.data.type === 'google_connection_error' ||
|
event.data.type === 'google_connection_error' ||
|
||||||
event.data.type === 'clickup_connection_error' ||
|
event.data.type === 'clickup_connection_error'
|
||||||
event.data.type === 'infomaniak_connection_error'
|
|
||||||
) {
|
) {
|
||||||
// Handle error
|
// Handle error
|
||||||
clearInterval(checkClosed);
|
clearInterval(checkClosed);
|
||||||
|
|
@ -497,82 +498,24 @@ export function useConnections() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create Infomaniak connection and open OAuth popup
|
// Infomaniak uses Personal Access Tokens (no OAuth). Two-step flow:
|
||||||
const createInfomaniakConnectionAndAuth = async (): Promise<void> => {
|
// 1. createInfomaniakConnection() - creates a PENDING UserConnection row
|
||||||
if (isConnecting) return;
|
// 2. submitInfomaniakToken(connectionId, pat) - validates the PAT against
|
||||||
setIsConnecting(true);
|
// /1/profile, persists it as the connection's bearer token, and flips
|
||||||
try {
|
// the row to ACTIVE.
|
||||||
const newConnection = await createConnection({
|
const createInfomaniakConnection = async (): Promise<Connection> => {
|
||||||
type: 'infomaniak',
|
return await createConnection({
|
||||||
authority: 'infomaniak',
|
type: 'infomaniak',
|
||||||
});
|
authority: 'infomaniak',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const connectResponse = await connectServiceApi(request, newConnection.id);
|
const submitInfomaniakToken = async (
|
||||||
|
connectionId: string,
|
||||||
if (!connectResponse.authUrl) {
|
token: string
|
||||||
throw new Error('No OAuth URL received from backend');
|
): Promise<void> => {
|
||||||
}
|
await submitInfomaniakTokenApi(request, connectionId, token);
|
||||||
|
await fetchConnections();
|
||||||
const apiBaseUrl = getApiBaseUrl();
|
|
||||||
let authUrl = connectResponse.authUrl;
|
|
||||||
if (authUrl.startsWith('/')) {
|
|
||||||
authUrl = `${apiBaseUrl}${authUrl}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await new Promise<void>((resolve, reject) => {
|
|
||||||
const popup = window.open(
|
|
||||||
authUrl,
|
|
||||||
'infomaniak-connection',
|
|
||||||
'width=500,height=600,scrollbars=yes,resizable=yes'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!popup) {
|
|
||||||
setIsConnecting(false);
|
|
||||||
reject(new Error('Popup was blocked. Please allow popups and try again.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkClosed = setInterval(() => {
|
|
||||||
if (popup.closed) {
|
|
||||||
clearInterval(checkClosed);
|
|
||||||
window.removeEventListener('message', messageListener);
|
|
||||||
setIsConnecting(false);
|
|
||||||
console.log('Infomaniak OAuth popup closed');
|
|
||||||
fetchConnections();
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
const messageListener = (event: MessageEvent) => {
|
|
||||||
const apiUrl = new URL(apiBaseUrl);
|
|
||||||
if (event.origin !== apiUrl.origin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.data.type === 'infomaniak_connection_success') {
|
|
||||||
clearInterval(checkClosed);
|
|
||||||
window.removeEventListener('message', messageListener);
|
|
||||||
popup.close();
|
|
||||||
setIsConnecting(false);
|
|
||||||
console.log('Infomaniak connection successful');
|
|
||||||
fetchConnections();
|
|
||||||
resolve();
|
|
||||||
} else if (event.data.type === 'infomaniak_connection_error') {
|
|
||||||
clearInterval(checkClosed);
|
|
||||||
window.removeEventListener('message', messageListener);
|
|
||||||
popup.close();
|
|
||||||
setIsConnecting(false);
|
|
||||||
reject(new Error(event.data.error || 'Infomaniak connection failed'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('message', messageListener);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
setIsConnecting(false);
|
|
||||||
console.error('Error creating Infomaniak connection:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create Microsoft connection and open OAuth popup
|
// Create Microsoft connection and open OAuth popup
|
||||||
|
|
@ -781,7 +724,8 @@ export function useConnections() {
|
||||||
createGoogleConnectionAndAuth,
|
createGoogleConnectionAndAuth,
|
||||||
createMicrosoftConnectionAndAuth,
|
createMicrosoftConnectionAndAuth,
|
||||||
createClickupConnectionAndAuth,
|
createClickupConnectionAndAuth,
|
||||||
createInfomaniakConnectionAndAuth,
|
createInfomaniakConnection,
|
||||||
|
submitInfomaniakToken,
|
||||||
isLoading,
|
isLoading,
|
||||||
loading: isLoading, // Alias for FormGenerator compatibility
|
loading: isLoading, // Alias for FormGenerator compatibility
|
||||||
isConnecting,
|
isConnecting,
|
||||||
|
|
@ -807,13 +751,13 @@ export function useOAuthConnect() {
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
const [connectError, setConnectError] = useState<string | null>(null);
|
const [connectError, setConnectError] = useState<string | null>(null);
|
||||||
|
|
||||||
const connectWithPopup = async (connectionId: string): Promise<void> => {
|
const connectWithPopup = async (connectionId: string, reauth: boolean = false): Promise<void> => {
|
||||||
setIsConnecting(true);
|
setIsConnecting(true);
|
||||||
setConnectError(null);
|
setConnectError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the OAuth URL from backend
|
// Get the OAuth URL from backend
|
||||||
const response = await connectService(connectionId);
|
const response = await connectService(connectionId, reauth);
|
||||||
if (!response.authUrl) {
|
if (!response.authUrl) {
|
||||||
throw new Error('No OAuth URL received from backend');
|
throw new Error('No OAuth URL received from backend');
|
||||||
}
|
}
|
||||||
|
|
@ -866,8 +810,7 @@ export function useOAuthConnect() {
|
||||||
if (
|
if (
|
||||||
event.data.type === 'msft_connection_success' ||
|
event.data.type === 'msft_connection_success' ||
|
||||||
event.data.type === 'google_connection_success' ||
|
event.data.type === 'google_connection_success' ||
|
||||||
event.data.type === 'clickup_connection_success' ||
|
event.data.type === 'clickup_connection_success'
|
||||||
event.data.type === 'infomaniak_connection_success'
|
|
||||||
) {
|
) {
|
||||||
// Clean up - IMPORTANT: clear the checkClosed interval first
|
// Clean up - IMPORTANT: clear the checkClosed interval first
|
||||||
clearInterval(checkClosed);
|
clearInterval(checkClosed);
|
||||||
|
|
@ -881,8 +824,7 @@ export function useOAuthConnect() {
|
||||||
} else if (
|
} else if (
|
||||||
event.data.type === 'msft_connection_error' ||
|
event.data.type === 'msft_connection_error' ||
|
||||||
event.data.type === 'google_connection_error' ||
|
event.data.type === 'google_connection_error' ||
|
||||||
event.data.type === 'clickup_connection_error' ||
|
event.data.type === 'clickup_connection_error'
|
||||||
event.data.type === 'infomaniak_connection_error'
|
|
||||||
) {
|
) {
|
||||||
// Handle error - also clear the checkClosed interval
|
// Handle error - also clear the checkClosed interval
|
||||||
clearInterval(checkClosed);
|
clearInterval(checkClosed);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import React, { useState, useMemo, useEffect } from 'react';
|
||||||
import { useConnections, type Connection } from '../../hooks/useConnections';
|
import { useConnections, type Connection } from '../../hooks/useConnections';
|
||||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||||
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
||||||
import { FaSync, FaGoogle, FaMicrosoft, FaLink, FaRedo, FaShieldAlt, FaTasks, FaCloud } from 'react-icons/fa';
|
import { FaSync, FaGoogle, FaMicrosoft, FaLink, FaRedo, FaShieldAlt, FaTasks, FaCloud, FaSyncAlt } from 'react-icons/fa';
|
||||||
import { getApiBaseUrl } from '../../../config/config';
|
import { getApiBaseUrl } from '../../../config/config';
|
||||||
import styles from '../admin/Admin.module.css';
|
import styles from '../admin/Admin.module.css';
|
||||||
|
|
||||||
|
|
@ -35,7 +35,8 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
createGoogleConnectionAndAuth,
|
createGoogleConnectionAndAuth,
|
||||||
createMicrosoftConnectionAndAuth,
|
createMicrosoftConnectionAndAuth,
|
||||||
createClickupConnectionAndAuth,
|
createClickupConnectionAndAuth,
|
||||||
createInfomaniakConnectionAndAuth,
|
createInfomaniakConnection,
|
||||||
|
submitInfomaniakToken,
|
||||||
connectWithPopup,
|
connectWithPopup,
|
||||||
refreshMicrosoftToken,
|
refreshMicrosoftToken,
|
||||||
refreshGoogleToken,
|
refreshGoogleToken,
|
||||||
|
|
@ -45,8 +46,18 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
const [editingConnection, setEditingConnection] = useState<Connection | null>(null);
|
const [editingConnection, setEditingConnection] = useState<Connection | null>(null);
|
||||||
const [deletingConnections, setDeletingConnections] = useState<Set<string>>(new Set());
|
const [deletingConnections, setDeletingConnections] = useState<Set<string>>(new Set());
|
||||||
const [refreshingConnections, setRefreshingConnections] = useState<Set<string>>(new Set());
|
const [refreshingConnections, setRefreshingConnections] = useState<Set<string>>(new Set());
|
||||||
|
const [reconnectingConnections, setReconnectingConnections] = useState<Set<string>>(new Set());
|
||||||
const [adminConsentPending, setAdminConsentPending] = useState(false);
|
const [adminConsentPending, setAdminConsentPending] = useState(false);
|
||||||
|
|
||||||
|
// Infomaniak PAT modal: holds the pending connectionId (created up-front so the
|
||||||
|
// user only commits if they actually paste a valid token; on cancel we delete it).
|
||||||
|
const [infomaniakModal, setInfomaniakModal] = useState<{
|
||||||
|
connectionId: string;
|
||||||
|
token: string;
|
||||||
|
submitting: boolean;
|
||||||
|
error: string | null;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
// Initial fetch
|
// Initial fetch
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refetch();
|
refetch();
|
||||||
|
|
@ -172,6 +183,24 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle reconnect (full OAuth re-consent so newly added scopes -- e.g.
|
||||||
|
// Calendar/Contacts -- are actually granted on top of existing tokens).
|
||||||
|
const handleReconnect = async (connection: Connection) => {
|
||||||
|
setReconnectingConnections(prev => new Set(prev).add(connection.id));
|
||||||
|
try {
|
||||||
|
await connectWithPopup(connection.id, true);
|
||||||
|
refetch();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reconnecting:', error);
|
||||||
|
} finally {
|
||||||
|
setReconnectingConnections(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(connection.id);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Guards prevent double-trigger while the OAuth popup is open, which would
|
// Guards prevent double-trigger while the OAuth popup is open, which would
|
||||||
// otherwise create additional orphan PENDING connections on every click.
|
// otherwise create additional orphan PENDING connections on every click.
|
||||||
const handleCreateGoogle = async () => {
|
const handleCreateGoogle = async () => {
|
||||||
|
|
@ -205,15 +234,57 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateInfomaniak = async () => {
|
const handleCreateInfomaniak = async () => {
|
||||||
if (isConnecting) return;
|
if (isConnecting || infomaniakModal) return;
|
||||||
try {
|
try {
|
||||||
await createInfomaniakConnectionAndAuth();
|
const newConnection = await createInfomaniakConnection();
|
||||||
|
setInfomaniakModal({
|
||||||
|
connectionId: newConnection.id,
|
||||||
|
token: '',
|
||||||
|
submitting: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
refetch();
|
refetch();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating Infomaniak connection:', error);
|
console.error('Error creating Infomaniak connection:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInfomaniakCancel = async () => {
|
||||||
|
if (!infomaniakModal) return;
|
||||||
|
const { connectionId, submitting } = infomaniakModal;
|
||||||
|
if (submitting) return;
|
||||||
|
setInfomaniakModal(null);
|
||||||
|
try {
|
||||||
|
await deleteConnection(connectionId);
|
||||||
|
refetch();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rolling back pending Infomaniak connection:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInfomaniakSubmit = async () => {
|
||||||
|
if (!infomaniakModal) return;
|
||||||
|
const trimmed = infomaniakModal.token.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
setInfomaniakModal({ ...infomaniakModal, error: t('Bitte Personal Access Token einfügen') });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInfomaniakModal({ ...infomaniakModal, submitting: true, error: null });
|
||||||
|
try {
|
||||||
|
await submitInfomaniakToken(infomaniakModal.connectionId, trimmed);
|
||||||
|
setInfomaniakModal(null);
|
||||||
|
refetch();
|
||||||
|
} catch (error: any) {
|
||||||
|
const detail =
|
||||||
|
error?.response?.data?.detail ||
|
||||||
|
error?.message ||
|
||||||
|
t('Token konnte nicht gespeichert werden');
|
||||||
|
setInfomaniakModal((prev) =>
|
||||||
|
prev ? { ...prev, submitting: false, error: String(detail) } : prev
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Open Microsoft Admin Consent flow in a popup
|
// Open Microsoft Admin Consent flow in a popup
|
||||||
const handleAdminConsent = () => {
|
const handleAdminConsent = () => {
|
||||||
setAdminConsentPending(true);
|
setAdminConsentPending(true);
|
||||||
|
|
@ -360,9 +431,19 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
icon: <FaRedo />,
|
icon: <FaRedo />,
|
||||||
onClick: handleRefresh,
|
onClick: handleRefresh,
|
||||||
title: t('Token aktualisieren'),
|
title: t('Token aktualisieren'),
|
||||||
visible: (row: Connection) => row.status === 'active',
|
visible: (row: Connection) => row.status === 'active' && (row.authority === 'msft' || row.authority === 'google'),
|
||||||
loading: (row: Connection) => refreshingConnections.has(row.id),
|
loading: (row: Connection) => refreshingConnections.has(row.id),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'reconnect',
|
||||||
|
icon: <FaSyncAlt />,
|
||||||
|
onClick: handleReconnect,
|
||||||
|
title: t('Erneut verbinden (neue Scopes erteilen)'),
|
||||||
|
visible: (row: Connection) =>
|
||||||
|
row.status === 'active' &&
|
||||||
|
(row.authority === 'msft' || row.authority === 'google' || row.authority === 'clickup'),
|
||||||
|
loading: (row: Connection) => reconnectingConnections.has(row.id),
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
hookData={{
|
hookData={{
|
||||||
|
|
@ -411,6 +492,143 @@ export const ConnectionsPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Infomaniak Personal Access Token Modal */}
|
||||||
|
{infomaniakModal && (
|
||||||
|
<div className={styles.modalOverlay}>
|
||||||
|
<div className={styles.modal} style={{ maxWidth: 640 }}>
|
||||||
|
<div className={styles.modalHeader}>
|
||||||
|
<h2 className={styles.modalTitle}>{t('Infomaniak verbinden')}</h2>
|
||||||
|
<button
|
||||||
|
className={styles.modalClose}
|
||||||
|
onClick={handleInfomaniakCancel}
|
||||||
|
disabled={infomaniakModal.submitting}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.modalContent}>
|
||||||
|
<p style={{ marginTop: 0 }}>
|
||||||
|
{t(
|
||||||
|
'Infomaniak nutzt für kDrive und kSuite keine OAuth-Anmeldung, sondern ein persönliches API-Token (PAT). Erstelle das Token einmalig im Infomaniak Manager und füge es unten ein.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<ol style={{ paddingLeft: 20 }}>
|
||||||
|
<li>
|
||||||
|
{t('Öffne den Infomaniak-Manager:')}{' '}
|
||||||
|
<a
|
||||||
|
href="https://manager.infomaniak.com/v3/ng/accounts/token/list"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
manager.infomaniak.com – API-Tokens
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t('Klicke auf')} <code>{t('Token erstellen')}</code>{' '}
|
||||||
|
{t('und vergib einen aussagekräftigen Namen, z. B.')}{' '}
|
||||||
|
<code>PowerOn</code>.{' '}
|
||||||
|
{t('Application bleibt auf')} <code>Default application</code>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t('Suche im Scope-Feld nach')}{' '}
|
||||||
|
<strong>{t('allen fünf')}</strong>{' '}
|
||||||
|
{t('Berechtigungen und kreuze sie an:')}
|
||||||
|
<ul style={{ marginTop: 6, marginBottom: 6, paddingLeft: 20 }}>
|
||||||
|
<li>
|
||||||
|
<code>accounts</code> —{' '}
|
||||||
|
{t(
|
||||||
|
'Account-Discovery (Pflicht, sonst findet kDrive deinen Drive nicht)'
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>drive</code> — {t('kDrive (Pflicht, heute aktiv)')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>workspace:calendar</code> —{' '}
|
||||||
|
{t('Kalender (Pflicht, heute aktiv)')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>workspace:contact</code> —{' '}
|
||||||
|
{t('Kontakte (heute aktiv)')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>workspace:mail</code> —{' '}
|
||||||
|
{t('Mail (in Vorbereitung, Scope schon mitnehmen)')}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<em>
|
||||||
|
{t(
|
||||||
|
'Nicht "All" auswählen und nicht user_info — beides wird nicht benötigt.'
|
||||||
|
)}
|
||||||
|
</em>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t(
|
||||||
|
'Kopiere das Token sofort (Infomaniak zeigt es nur einmal) und füge es hier ein:'
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={infomaniakModal.token}
|
||||||
|
onChange={(e) =>
|
||||||
|
setInfomaniakModal((prev) =>
|
||||||
|
prev ? { ...prev, token: e.target.value, error: null } : prev
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder={t('Personal Access Token einfügen')}
|
||||||
|
disabled={infomaniakModal.submitting}
|
||||||
|
autoFocus
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '8px 10px',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 13,
|
||||||
|
border: '1px solid var(--border, #ccc)',
|
||||||
|
borderRadius: 4,
|
||||||
|
marginBottom: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{infomaniakModal.error && (
|
||||||
|
<div className={styles.errorMessage} style={{ marginBottom: 12 }}>
|
||||||
|
{infomaniakModal.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p style={{ fontSize: 12, color: 'var(--text-secondary, #666)' }}>
|
||||||
|
{t(
|
||||||
|
'Das Token wird verschlüsselt gespeichert und nur für API-Aufrufe an Infomaniak verwendet.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: 8,
|
||||||
|
marginTop: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.secondaryButton}
|
||||||
|
onClick={handleInfomaniakCancel}
|
||||||
|
disabled={infomaniakModal.submitting}
|
||||||
|
>
|
||||||
|
{t('Abbrechen')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.primaryButton}
|
||||||
|
onClick={handleInfomaniakSubmit}
|
||||||
|
disabled={infomaniakModal.submitting || !infomaniakModal.token.trim()}
|
||||||
|
>
|
||||||
|
{infomaniakModal.submitting ? t('Prüfen…') : t('Verbinden')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,20 @@ export const FilesPage: React.FC = () => {
|
||||||
maxWidth: 250,
|
maxWidth: 250,
|
||||||
displayField: 'sysCreatedByLabel',
|
displayField: 'sysCreatedByLabel',
|
||||||
} as any);
|
} as any);
|
||||||
|
// sysModifiedAt is marked frontend_visible=false in PowerOnModel so it
|
||||||
|
// never reaches us via the /api/attributes endpoint - declare type
|
||||||
|
// explicitly so the FormGenerator renders it as a timestamp.
|
||||||
|
cols.push({
|
||||||
|
key: 'sysModifiedAt',
|
||||||
|
label: t('Geaendert am'),
|
||||||
|
type: 'timestamp',
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
searchable: false,
|
||||||
|
width: 170,
|
||||||
|
minWidth: 130,
|
||||||
|
maxWidth: 220,
|
||||||
|
} as any);
|
||||||
return resolveColumnTypes(cols, attributes || []);
|
return resolveColumnTypes(cols, attributes || []);
|
||||||
}, [attributes, t]);
|
}, [attributes, t]);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue