feat(08-01): add /api/mobile/engagement/user/[userId]/photo proxy route
- Proxies Microsoft Graph user photo bytes to authenticated mobile clients - requireAuth() is first call — unauthenticated requests get 401 before Graph - 503 when MSGRAPH_* env not configured (isMsgraphConfigured gate, D-26) - 400 for malformed userId (path traversal denylist, permissive per VARCHAR(255)) - 404 neutral response when user has no photo (no userId oracle) - 200 with Cache-Control: private, max-age=3600 on success (D-25) - 502 neutral response on Graph upstream errors (no token/user leakage)
This commit is contained in:
parent
3f6b13572e
commit
4978780962
1 changed files with 67 additions and 0 deletions
67
app/api/mobile/engagement/user/[userId]/photo/route.ts
Normal file
67
app/api/mobile/engagement/user/[userId]/photo/route.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth } from '@/lib/auth-utils';
|
||||
import { getMsgraphClient, isMsgraphConfigured } from '@/lib/services/msgraph-factory';
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ userId: string }> }
|
||||
) {
|
||||
// 1. Auth gate (middleware whitelists /api/mobile/* — handler MUST gate itself)
|
||||
const { error: authError } = await requireAuth();
|
||||
if (authError) return authError;
|
||||
|
||||
// 2. MS Graph configuration gate (D-26)
|
||||
if (!isMsgraphConfigured()) {
|
||||
return NextResponse.json(
|
||||
{ error: 'msgraph_not_configured', message: 'Microsoft Graph credentials are not configured' },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
const { userId } = await params;
|
||||
|
||||
// 3. Defensive userId shape check — prevents path traversal and malformed
|
||||
// requests from reaching MS Graph. graph_users.id is VARCHAR(255)
|
||||
// (verified in migration 041 line 4) — it typically holds Azure AD GUID
|
||||
// object IDs but the column also permits UPN-style identifiers, so this
|
||||
// check is permissive: reject anything containing '/', '?', '#', '..',
|
||||
// or whitespace, or that is empty / longer than 128 chars. Do NOT
|
||||
// tighten to a strict GUID regex — that would lock out valid UPN-form
|
||||
// rows the schema explicitly allows.
|
||||
if (!userId || userId.length > 128 || /[\s/?#]|\.\./.test(userId)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'invalid_user_id', message: 'Invalid user id' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const client = getMsgraphClient();
|
||||
const photo = await client.getUserPhotoBytes(userId);
|
||||
|
||||
if (!photo) {
|
||||
// No photo on Graph (whether the user exists or not — neutral 404)
|
||||
return NextResponse.json(
|
||||
{ error: 'no_photo', message: 'No photo available' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return new NextResponse(photo.bytes, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': photo.contentType,
|
||||
'Cache-Control': 'private, max-age=3600',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[ENGAGEMENT-USER-PHOTO] Error:', error);
|
||||
// Neutral error response — do not leak whether the user exists or whether
|
||||
// the failure was auth/network/upstream. Always 502 for "couldn't reach
|
||||
// Graph for any reason other than no-photo".
|
||||
return NextResponse.json(
|
||||
{ error: 'photo_fetch_failed', message: 'Failed to fetch photo' },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue