Runtime-Error im /init und /callback weil Nitro auto-import nur server/utils/
scannt, nicht server/db/. Die Helper-Funktionen (createOauthPendingState,
consumeOauthPendingState, upsertOauthMicrosoftConnection, countMailConnections)
sowie ms-oauth-utility-Functions (exchangeCodeForTokens, extractEmailFromId
Token, generate*-Helpers, MS_*-Konstanten) wurden im Code implizit referenziert
aber nicht explizit importiert.
pnpm build:backend hat das nicht gefangen (Nitro Bundle-Step ist toleranter als
strict tsc). Erster Symptom auf staging:
ReferenceError: createOauthPendingState is not defined
at .../routes/api/mail/oauth/microsoft/init.post.mjs:36:3
Fix: explizite import-Statements in beiden Endpoints.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
import { createOauthPendingState } from "../../../../db/mail";
|
|
import {
|
|
MS_AUTH_BASE,
|
|
MS_REDIRECT_URI,
|
|
MS_OAUTH_SCOPES,
|
|
generateCodeVerifier,
|
|
computeCodeChallenge,
|
|
generateStateId,
|
|
} from "../../../../utils/ms-oauth";
|
|
|
|
/**
|
|
* POST /api/mail/oauth/microsoft/init
|
|
*
|
|
* Step 1 of the Microsoft OAuth PKCE flow.
|
|
* Generates a PKCE code_verifier + code_challenge, persists the state in
|
|
* oauth_pending_states (TTL 10 min), and returns the authorization URL.
|
|
*
|
|
* The native app opens this URL via expo-web-browser (WebBrowser.openAuthSessionAsync).
|
|
* After login + consent, Microsoft redirects to rebreak://auth/mail-oauth-callback?code=…&state=…
|
|
* The app deep-link handler calls POST /api/mail/oauth/microsoft/callback with { code, state }.
|
|
*
|
|
* Body (optional):
|
|
* email?: string — pre-fills login_hint + prompt logic
|
|
*
|
|
* Response:
|
|
* 200: { authorizationUrl: string }
|
|
* 401: not authenticated
|
|
*/
|
|
export default defineEventHandler(async (event) => {
|
|
const user = await requireUser(event);
|
|
const config = useRuntimeConfig(event);
|
|
const clientId = config.msOauthClientId as string;
|
|
|
|
if (!clientId) {
|
|
throw createError({
|
|
statusCode: 500,
|
|
data: { error: "MS_OAUTH_CLIENT_ID not configured" },
|
|
});
|
|
}
|
|
|
|
const body = await readBody(event).catch(() => ({})) as { email?: string };
|
|
const hintEmail = body?.email?.trim() || null;
|
|
|
|
// ── PKCE ─────────────────────────────────────────────────────────────────
|
|
const codeVerifier = generateCodeVerifier();
|
|
const codeChallenge = computeCodeChallenge(codeVerifier);
|
|
const stateId = generateStateId();
|
|
|
|
// ── Persist state (TTL enforced at read time in callback) ─────────────────
|
|
// Clean up stale entries for this user (older than 10 min) before inserting.
|
|
await createOauthPendingState({
|
|
stateId,
|
|
userId: user.id,
|
|
codeVerifier,
|
|
email: hintEmail,
|
|
});
|
|
|
|
// ── Build authorization URL ───────────────────────────────────────────────
|
|
const params = new URLSearchParams({
|
|
client_id: clientId,
|
|
response_type: "code",
|
|
redirect_uri: MS_REDIRECT_URI,
|
|
response_mode: "query",
|
|
scope: MS_OAUTH_SCOPES,
|
|
state: stateId,
|
|
code_challenge: codeChallenge,
|
|
code_challenge_method: "S256",
|
|
// prompt=consent: always show consent screen so user sees what scopes are requested.
|
|
// Alternative: 'select_account' for re-connect flows (less friction but skips
|
|
// scope confirmation). Using 'consent' is the safer DSGVO-aligned default.
|
|
prompt: "consent",
|
|
});
|
|
|
|
if (hintEmail) {
|
|
params.set("login_hint", hintEmail);
|
|
}
|
|
|
|
const authorizationUrl = `${MS_AUTH_BASE}?${params.toString()}`;
|
|
|
|
return { authorizationUrl };
|
|
});
|