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, Query,
Res, Res,
Logger, Logger,
UseGuards,
Param, Param,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { ApiTags, ApiOperation } from '@nestjs/swagger';
@ -53,15 +52,15 @@ export class IntegrationsController {
/** /**
* GET /api/v1/auth/integrations/microsoft-365 * GET /api/v1/auth/integrations/microsoft-365
* OAuth2-Flow fuer Microsoft 365 starten. * OAuth2-Flow fuer Microsoft 365 vorbereiten.
* User muss eingeloggt sein userId wird im State gespeichert. * 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') @Get('auth/integrations/microsoft-365')
@ApiOperation({ summary: 'Microsoft 365 Integration starten' }) @ApiOperation({ summary: 'Microsoft 365 Integration starten' })
async initM365Integration( async initM365Integration(
@CurrentUser() user: JwtUser, @CurrentUser() user: JwtUser,
@Res() res: Response, ): Promise<{ success: boolean; data: { url: string } }> {
): Promise<void> {
// State = UUID, in Redis mit userId hinterlegen (5 Min TTL) // State = UUID, in Redis mit userId hinterlegen (5 Min TTL)
const state = uuidv4(); const state = uuidv4();
await this.redis.set( await this.redis.set(
@ -74,7 +73,7 @@ export class IntegrationsController {
this.logger.log( this.logger.log(
`M365-Integration OAuth-Flow gestartet fuer User ${user.sub}`, `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), .then((r) => r.data),
/** Gibt die URL zurück, zu der der Browser weitergeleitet werden soll */ /** Ruft die Microsoft OAuth-URL via API ab (JWT-geschützt) und leitet den Browser weiter */
getM365ConnectUrl: (): string => '/api/v1/auth/integrations/microsoft-365', 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) --- // --- 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' }}> <p style={{ color: 'var(--color-text-muted)', fontSize: '0.9375rem', marginBottom: '1rem' }}>
Verbinden Sie Microsoft 365, um Kalendertermine zu diesem Kontakt zu sehen. Verbinden Sie Microsoft 365, um Kalendertermine zu diesem Kontakt zu sehen.
</p> </p>
<a <button
href={integrationsApi.getM365ConnectUrl()} type="button"
onClick={() => integrationsApi.connectM365()}
style={{ style={{
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
@ -41,14 +42,15 @@ export function CalendarTab({ contactId }: Props) {
padding: '0.4375rem 1rem', padding: '0.4375rem 1rem',
background: 'var(--color-primary)', background: 'var(--color-primary)',
color: 'white', color: 'white',
border: 'none',
borderRadius: 'var(--radius-sm)', borderRadius: 'var(--radius-sm)',
fontSize: '0.875rem', fontSize: '0.875rem',
fontWeight: 600, fontWeight: 600,
textDecoration: 'none', cursor: 'pointer',
}} }}
> >
Microsoft 365 verbinden Microsoft 365 verbinden
</a> </button>
</div> </div>
); );
} }

View file

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

View file

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

View file

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