| import { NextResponse } from "next/server"; |
| import { loadConfig } from "@/lib/config"; |
|
|
| export const dynamic = "force-dynamic"; |
| export const maxDuration = 10; |
|
|
| const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; |
|
|
| const MISSING_CONFIG_ERROR = |
| "Microsoft OAuth is not configured. The built-in Azure CLI client ID can no longer be used because Microsoft treats it as a first-party application and requires admin pre-authorization. " + |
| "To connect a Microsoft 365 mailbox, register your own Azure AD application and set AZURE_AD_CLIENT_ID (or AZURE_CLIENT_ID / MICROSOFT_CLIENT_ID) and AZURE_AD_TENANT_ID (or 'common'). " + |
| "See the README for step-by-step registration instructions."; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function POST() { |
| let config; |
| try { |
| config = loadConfig(); |
| } catch (e) { |
| console.error("[microsoft/devicecode] loadConfig error:", e); |
| config = { graph: {} }; |
| } |
|
|
| const CLIENT_ID = |
| process.env.AZURE_AD_CLIENT_ID || |
| process.env.AZURE_CLIENT_ID || |
| process.env.MICROSOFT_CLIENT_ID || |
| config.graph?.clientId || |
| ""; |
|
|
| const TENANT = |
| process.env.AZURE_AD_TENANT_ID || |
| process.env.AZURE_TENANT_ID || |
| process.env.MICROSOFT_TENANT_ID || |
| config.graph?.tenantId || |
| "common"; |
|
|
| if (!CLIENT_ID || !UUID_REGEX.test(CLIENT_ID)) { |
| return NextResponse.json({ error: MISSING_CONFIG_ERROR }, { status: 400 }); |
| } |
|
|
| const SCOPES = [ |
| "https://graph.microsoft.com/Mail.Read", |
| "https://graph.microsoft.com/Mail.ReadWrite", |
| "https://graph.microsoft.com/Files.Read", |
| "https://graph.microsoft.com/Files.Read.All", |
| "offline_access", |
| "openid", |
| "profile", |
| ]; |
|
|
| try { |
| const controller = new AbortController(); |
| const timeout = setTimeout(() => controller.abort(), 9000); |
| try { |
| const res = await fetch( |
| `https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/devicecode`, |
| { |
| method: "POST", |
| headers: { "Content-Type": "application/x-www-form-urlencoded" }, |
| body: new URLSearchParams({ |
| client_id: CLIENT_ID, |
| scope: SCOPES.join(" "), |
| }), |
| signal: controller.signal, |
| } |
| ); |
|
|
| const text = await res.text(); |
| const data = text ? JSON.parse(text) : {}; |
|
|
| if (!res.ok) { |
| return NextResponse.json( |
| { error: data.error_description || data.error || "Failed to start device code flow" }, |
| { status: 500 } |
| ); |
| } |
|
|
| return NextResponse.json({ |
| device_code: data.device_code, |
| user_code: data.user_code, |
| verification_uri: data.verification_uri, |
| expires_in: data.expires_in, |
| interval: data.interval || 5, |
| message: data.message || `Go to ${data.verification_uri} and enter code ${data.user_code}`, |
| client_id: CLIENT_ID, |
| tenant_id: TENANT, |
| scopes: SCOPES, |
| }); |
| } finally { |
| clearTimeout(timeout); |
| } |
| } catch (e: any) { |
| return NextResponse.json({ error: e.name === "AbortError" ? "Device code request timed out" : e.message }, { status: 504 }); |
| } |
| } |
|
|