fix(ms365): OAuth-Connect via API-Call statt direktem Browser-Link

Problem: <a href="/api/v1/auth/integrations/microsoft-365"> sendet keinen
JWT-Authorization-Header (JWT liegt im Memory, nicht als Cookie).

Lösung:
- Backend: initM365Integration gibt JSON {url} zurück statt server-redirect
- Frontend: integrationsApi.connectM365() ruft Endpoint via Axios ab, dann
  window.location.href zur OAuth-URL
- ProfilePage + EmailsTab + CalendarTab + TasksTab: <a href> → <button onClick>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-12 22:57:20 +01:00
parent 05ccabfdb4
commit 1ecd7dad82
6 changed files with 38 additions and 25 deletions

View file

@ -5,7 +5,6 @@ import {
Query,
Res,
Logger,
UseGuards,
Param,
} from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
@ -53,15 +52,15 @@ export class IntegrationsController {
/**
* GET /api/v1/auth/integrations/microsoft-365
* OAuth2-Flow fuer Microsoft 365 starten.
* User muss eingeloggt sein userId wird im State gespeichert.
* OAuth2-Flow fuer Microsoft 365 vorbereiten.
* Gibt die Microsoft-Authorisierungs-URL als JSON zurueck.
* Das Frontend leitet den Browser dann client-seitig weiter (axios + window.location).
*/
@Get('auth/integrations/microsoft-365')
@ApiOperation({ summary: 'Microsoft 365 Integration starten' })
async initM365Integration(
@CurrentUser() user: JwtUser,
@Res() res: Response,
): Promise<void> {
): Promise<{ success: boolean; data: { url: string } }> {
// State = UUID, in Redis mit userId hinterlegen (5 Min TTL)
const state = uuidv4();
await this.redis.set(
@ -74,7 +73,7 @@ export class IntegrationsController {
this.logger.log(
`M365-Integration OAuth-Flow gestartet fuer User ${user.sub}`,
);
res.redirect(authUrl);
return { success: true, data: { url: authUrl } };
}
/**

View file

@ -771,8 +771,15 @@ export const integrationsApi = {
)
.then((r) => r.data),
/** Gibt die URL zurück, zu der der Browser weitergeleitet werden soll */
getM365ConnectUrl: (): string => '/api/v1/auth/integrations/microsoft-365',
/** Ruft die Microsoft OAuth-URL via API ab (JWT-geschützt) und leitet den Browser weiter */
connectM365: () =>
api
.get<{ success: boolean; data: { url: string } }>(
'/auth/integrations/microsoft-365',
)
.then((r) => {
window.location.href = r.data.data.url;
}),
};
// --- Microsoft Graph Proxy (CRM) ---

View file

@ -32,8 +32,9 @@ export function CalendarTab({ contactId }: Props) {
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.9375rem', marginBottom: '1rem' }}>
Verbinden Sie Microsoft 365, um Kalendertermine zu diesem Kontakt zu sehen.
</p>
<a
href={integrationsApi.getM365ConnectUrl()}
<button
type="button"
onClick={() => integrationsApi.connectM365()}
style={{
display: 'inline-flex',
alignItems: 'center',
@ -41,14 +42,15 @@ export function CalendarTab({ contactId }: Props) {
padding: '0.4375rem 1rem',
background: 'var(--color-primary)',
color: 'white',
border: 'none',
borderRadius: 'var(--radius-sm)',
fontSize: '0.875rem',
fontWeight: 600,
textDecoration: 'none',
cursor: 'pointer',
}}
>
Microsoft 365 verbinden
</a>
</button>
</div>
);
}

View file

@ -31,8 +31,9 @@ export function EmailsTab({ contactId }: Props) {
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.9375rem', marginBottom: '1rem' }}>
Verbinden Sie Microsoft 365, um E-Mails zu diesem Kontakt zu sehen.
</p>
<a
href={integrationsApi.getM365ConnectUrl()}
<button
type="button"
onClick={() => integrationsApi.connectM365()}
style={{
display: 'inline-flex',
alignItems: 'center',
@ -40,14 +41,15 @@ export function EmailsTab({ contactId }: Props) {
padding: '0.4375rem 1rem',
background: 'var(--color-primary)',
color: 'white',
border: 'none',
borderRadius: 'var(--radius-sm)',
fontSize: '0.875rem',
fontWeight: 600,
textDecoration: 'none',
cursor: 'pointer',
}}
>
Microsoft 365 verbinden
</a>
</button>
</div>
);
}

View file

@ -117,8 +117,9 @@ export function TasksTab({ contactId }: Props) {
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.9375rem', marginBottom: '1rem' }}>
Verbinden Sie Microsoft 365, um Aufgaben zu diesem Kontakt zu sehen.
</p>
<a
href={integrationsApi.getM365ConnectUrl()}
<button
type="button"
onClick={() => integrationsApi.connectM365()}
style={{
display: 'inline-flex',
alignItems: 'center',
@ -126,14 +127,15 @@ export function TasksTab({ contactId }: Props) {
padding: '0.4375rem 1rem',
background: 'var(--color-primary)',
color: 'white',
border: 'none',
borderRadius: 'var(--radius-sm)',
fontSize: '0.875rem',
fontWeight: 600,
textDecoration: 'none',
cursor: 'pointer',
}}
>
Microsoft 365 verbinden
</a>
</button>
</div>
);
}

View file

@ -843,9 +843,9 @@ export function ProfilePage() {
Verbinden Sie Ihr Microsoft 365 Konto, um E-Mails, Kalendertermine und Aufgaben
direkt in Kontaktprofilen zu sehen.
</p>
<a
href={integrationsApi.getM365ConnectUrl()}
className={styles.buttonPrimary ?? undefined}
<button
type="button"
onClick={() => integrationsApi.connectM365()}
style={{
display: 'inline-flex',
alignItems: 'center',
@ -853,17 +853,18 @@ export function ProfilePage() {
padding: '0.5rem 1.25rem',
background: 'var(--color-primary)',
color: 'white',
border: 'none',
borderRadius: 'var(--radius-sm)',
fontSize: '0.9375rem',
fontWeight: 600,
textDecoration: 'none',
cursor: 'pointer',
}}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M0 0h7.5v7.5H0zm8.5 0H16v7.5H8.5zM0 8.5h7.5V16H0zm8.5 0H16V16H8.5z" />
</svg>
Microsoft 365 verbinden
</a>
</button>
</div>
)}
</div>