Connect the React Native app to the admin backend so users and scans flow
into the panel and bans take effect on the device.
- Activate the bearer() plugin in lib/auth.ts so mobile clients can pass
the better-auth session token via Authorization: Bearer header
- Add requireMobileAuth() helper in lib/auth-guard.ts that resolves the
session, re-fetches the user from DB (banned flag is on User, not in
the Session payload) and returns 403 with banned/bannedReason for
banned accounts
- Extend CORS in middleware.ts to allow POST + Authorization header on
/api/mobile/* (preflight was failing before)
- New routes:
POST /api/mobile/auth/sync — passwordless mobile auth via
deterministic password derived from sha256(email + deviceId + pepper).
Tries signIn first, falls back to signUp on USER_NOT_FOUND. Returns
409 when the email exists with a different deviceId.
GET /api/mobile/auth/me — current user enriched with
banned/bannedReason/role/xp/level
POST /api/mobile/auth/sign-out — best-effort session revocation
POST /api/mobile/scans — create a scan, resolves diseaseSlug
to diseaseId, never accepts an imageUrl from the device (V1 keeps
photos local-only)
GET /api/mobile/scans — own scans, 50 most recent
Validated end-to-end via curl: signUp → me → repeat sync (idempotent) →
post scan → ban via DB → me reflects banned: true → POST scans returns
403 + banned/bannedReason.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
53 lines
1.4 KiB
TypeScript
53 lines
1.4 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
|
|
const CORS_HEADERS = {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Version",
|
|
"X-API-Version": "1.0",
|
|
};
|
|
|
|
export async function middleware(request: NextRequest) {
|
|
const { pathname } = request.nextUrl;
|
|
|
|
// ── Mobile API routes: public, CORS enabled ──
|
|
if (pathname.startsWith("/api/mobile")) {
|
|
if (request.method === "OPTIONS") {
|
|
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
}
|
|
|
|
const response = NextResponse.next();
|
|
Object.entries(CORS_HEADERS).forEach(([key, value]) => {
|
|
response.headers.set(key, value);
|
|
});
|
|
return response;
|
|
}
|
|
|
|
// ── Admin routes: require session ──
|
|
if (
|
|
pathname.startsWith("/dashboard") ||
|
|
pathname.startsWith("/diseases") ||
|
|
pathname.startsWith("/guides") ||
|
|
pathname.startsWith("/users") ||
|
|
pathname.startsWith("/alerts")
|
|
) {
|
|
const sessionCookie = request.cookies.get("better-auth.session_token");
|
|
if (!sessionCookie) {
|
|
return NextResponse.redirect(new URL("/login", request.url));
|
|
}
|
|
}
|
|
|
|
return NextResponse.next();
|
|
}
|
|
|
|
export const config = {
|
|
matcher: [
|
|
"/api/mobile/:path*",
|
|
"/dashboard/:path*",
|
|
"/diseases/:path*",
|
|
"/guides/:path*",
|
|
"/users/:path*",
|
|
"/alerts/:path*",
|
|
],
|
|
};
|