azure login und cal sync
This commit is contained in:
parent
2f9218ce0e
commit
b6f35431df
|
|
@ -1,21 +1,20 @@
|
||||||
AZURE_CLIENT_ID=<app-registration-client-id>
|
# ── SERVER ───────────────────────────────────────────────────────────────────
|
||||||
|
PORT=9000
|
||||||
|
DEV_AUTH_BYPASS=false
|
||||||
|
AI_LOCK_EXPIRY_HOURS=168
|
||||||
|
|
||||||
|
# Azure AD — server app registration (validates incoming JWTs)
|
||||||
|
AZURE_CLIENT_ID=<server-app-registration-client-id>
|
||||||
AZURE_TENANT_ID=<azure-ad-tenant-id>
|
AZURE_TENANT_ID=<azure-ad-tenant-id>
|
||||||
|
|
||||||
# Set to true for local dev to skip JWT verification (never use in production)
|
# Graph / OBO — required for calendar integration
|
||||||
# DEV_AUTH_BYPASS=true
|
# App Registration → API permissions → Graph → Calendars.Read (delegated) → grant admin consent
|
||||||
|
# App Registration → Certificates & secrets → New client secret
|
||||||
|
AZURE_CLIENT_SECRET=<client-secret-value>
|
||||||
|
# aud of client token must match this. Only needed if frontend uses a different app registration.
|
||||||
|
AZURE_OBO_CLIENT_ID=<frontend-app-registration-client-id>
|
||||||
|
|
||||||
# AI lock duration in hours (default: 24, use 168 for 7 days)
|
# ── CLIENT (Vite — copy relevant lines to client/.env) ───────────────────────
|
||||||
# AI_LOCK_EXPIRY_HOURS=168
|
# VITE_AZURE_CLIENT_ID=<frontend-app-registration-client-id>
|
||||||
|
# VITE_AZURE_TENANT_ID=<azure-ad-tenant-id>
|
||||||
# Graph API calendar integration (OBO flow)
|
# VITE_DEV_AUTH_BYPASS=true # DEV ONLY — never set in production
|
||||||
# AZURE_CLIENT_SECRET=<client-secret-value>
|
|
||||||
# AZURE_OBO_CLIENT_ID=<client-id of the app registration the frontend uses>
|
|
||||||
# Only needed if VITE_AZURE_CLIENT_ID != AZURE_CLIENT_ID (separate frontend/backend registrations).
|
|
||||||
# The OBO assertion token's audience must match this client_id.
|
|
||||||
# Required: App Registration → API permissions → Microsoft Graph → Calendars.Read (delegated) → Grant admin consent
|
|
||||||
# Then: Certificates & secrets → New client secret → copy value here
|
|
||||||
|
|
||||||
# Client needs VITE_ prefix — create client/.env with:
|
|
||||||
# VITE_AZURE_CLIENT_ID=<same as above>
|
|
||||||
# VITE_AZURE_TENANT_ID=<same as above>
|
|
||||||
# VITE_DEV_AUTH_BYPASS=true ← DEV ONLY: skips MS login in browser (never set in production)
|
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -21,11 +21,17 @@ const oboClientId = process.env.AZURE_OBO_CLIENT_ID ?? process.env.AZURE_CLIENT_
|
||||||
const clientSecret = process.env.AZURE_CLIENT_SECRET ?? '';
|
const clientSecret = process.env.AZURE_CLIENT_SECRET ?? '';
|
||||||
|
|
||||||
async function getOboToken(userToken: string, userId: string): Promise<string> {
|
async function getOboToken(userToken: string, userId: string): Promise<string> {
|
||||||
|
console.log(`[graph] getOboToken userId=${userId} tenantId=${tenantId || '(missing)'} oboClientId=${oboClientId || '(missing)'} secretSet=${!!clientSecret}`);
|
||||||
const cached = tokenCache.get(userId);
|
const cached = tokenCache.get(userId);
|
||||||
if (cached && cached.expiresAt > Date.now() + 60_000) {
|
if (cached && cached.expiresAt > Date.now() + 60_000) {
|
||||||
|
console.log(`[graph] OBO token from cache, expires in ${Math.round((cached.expiresAt - Date.now()) / 1000)}s`);
|
||||||
return cached.accessToken;
|
return cached.accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!tenantId || !oboClientId || !clientSecret) {
|
||||||
|
throw new Error(`OBO config incomplete: tenantId=${!!tenantId} oboClientId=${!!oboClientId} secret=${!!clientSecret}`);
|
||||||
|
}
|
||||||
|
|
||||||
const body = new URLSearchParams({
|
const body = new URLSearchParams({
|
||||||
client_id: oboClientId,
|
client_id: oboClientId,
|
||||||
client_secret: clientSecret,
|
client_secret: clientSecret,
|
||||||
|
|
@ -35,6 +41,7 @@ async function getOboToken(userToken: string, userId: string): Promise<string> {
|
||||||
scope: 'https://graph.microsoft.com/Calendars.Read',
|
scope: 'https://graph.microsoft.com/Calendars.Read',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`[graph] OBO token exchange → https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`);
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
|
`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
|
||||||
{ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body },
|
{ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body },
|
||||||
|
|
@ -42,10 +49,12 @@ async function getOboToken(userToken: string, userId: string): Promise<string> {
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const detail = await res.text();
|
const detail = await res.text();
|
||||||
|
console.error(`[graph] OBO exchange failed ${res.status}: ${detail}`);
|
||||||
throw new Error(`OBO token exchange failed (${res.status}): ${detail}`);
|
throw new Error(`OBO token exchange failed (${res.status}): ${detail}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await res.json() as { access_token: string; expires_in: number };
|
const json = await res.json() as { access_token: string; expires_in: number };
|
||||||
|
console.log(`[graph] OBO token obtained, expires_in=${json.expires_in}s`);
|
||||||
const entry: OboTokenEntry = {
|
const entry: OboTokenEntry = {
|
||||||
accessToken: json.access_token,
|
accessToken: json.access_token,
|
||||||
expiresAt: Date.now() + json.expires_in * 1000,
|
expiresAt: Date.now() + json.expires_in * 1000,
|
||||||
|
|
@ -66,6 +75,7 @@ export async function getCalendarEvents(
|
||||||
date: string,
|
date: string,
|
||||||
): Promise<CalendarEvent[]> {
|
): Promise<CalendarEvent[]> {
|
||||||
|
|
||||||
|
console.log(`[graph] getCalendarEvents userId=${userId} email=${userEmail} date=${date} tokenLen=${userToken.length}`);
|
||||||
const graphToken = await getOboToken(userToken, userId);
|
const graphToken = await getOboToken(userToken, userId);
|
||||||
|
|
||||||
const start = `${date}T00:00:00`;
|
const start = `${date}T00:00:00`;
|
||||||
|
|
@ -87,8 +97,10 @@ export async function getCalendarEvents(
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const detail = await res.text();
|
const detail = await res.text();
|
||||||
|
console.error(`[graph] calendarView failed ${res.status}: ${detail}`);
|
||||||
throw new Error(`Graph calendarView failed (${res.status}): ${detail}`);
|
throw new Error(`Graph calendarView failed (${res.status}): ${detail}`);
|
||||||
}
|
}
|
||||||
|
console.log(`[graph] calendarView OK`);
|
||||||
|
|
||||||
type GraphEvent = {
|
type GraphEvent = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,13 @@ calendar.get('/events', async (c) => {
|
||||||
const auth = c.get('auth');
|
const auth = c.get('auth');
|
||||||
const rawToken = c.req.header('Authorization')?.slice(7) ?? '';
|
const rawToken = c.req.header('Authorization')?.slice(7) ?? '';
|
||||||
|
|
||||||
|
// OBO requires a real MSAL JWT — API keys and dev-bypass tokens don't work
|
||||||
|
const looksLikeJwt = rawToken.startsWith('eyJ') && rawToken.split('.').length === 3;
|
||||||
|
console.log(`[calendar] rawToken prefix="${rawToken.slice(0, 8)}..." looksLikeJwt=${looksLikeJwt} userId=${auth.userId}`);
|
||||||
|
if (!looksLikeJwt) {
|
||||||
|
return c.json({ error: 'graph_unavailable', detail: 'Calendar requires MSAL login (OBO not possible with API key or dev-bypass token)' }, 502);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const events = await getCalendarEvents(rawToken, auth.userId, auth.email, date);
|
const events = await getCalendarEvents(rawToken, auth.userId, auth.email, date);
|
||||||
return c.json(events);
|
return c.json(events);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue