Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions ballotiq/src/app/api/gemini/assess/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
*/

import { NextRequest, NextResponse } from 'next/server';
import { withAuth } from '@/lib/security/auth';
import { analyzeAssessment } from '@/lib/gemini/assessment';
import type { AssessmentAnswer } from '@/types';

export async function POST(req: NextRequest) {
export const POST = withAuth(async (req: NextRequest) => {
try {
const body = await req.json() as {
answers: AssessmentAnswer;
Expand Down Expand Up @@ -41,4 +42,4 @@ export async function POST(req: NextRequest) {
{ status: 500 }
);
}
}
});
5 changes: 3 additions & 2 deletions ballotiq/src/app/api/gemini/assistant/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAssistantResponse } from '@/lib/assistant/hybridAssistant';
import type { ChatMessage, ElectionStep, UserContext } from '@/types';
import { withAuth } from '@/lib/security/auth';

export async function POST(req: NextRequest) {
export const POST = withAuth(async (req: NextRequest) => {
try {
const body = await req.json() as {
question: string;
Expand Down Expand Up @@ -38,4 +39,4 @@ export async function POST(req: NextRequest) {
{ status: 500 }
);
}
}
});
5 changes: 3 additions & 2 deletions ballotiq/src/app/api/gemini/guide/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
*/

import { NextRequest, NextResponse } from 'next/server';
import { withAuth } from '@/lib/security/auth';
import { generatePersonalizedGuide, generatePersonalizedGuideStream } from '@/lib/gemini/guide';
import type { KnowledgeLevel } from '@/types';

export async function POST(req: NextRequest) {
export const POST = withAuth(async (req: NextRequest) => {
try {
const body = await req.json() as {
countryCode: string;
Expand Down Expand Up @@ -75,4 +76,4 @@ export async function POST(req: NextRequest) {
{ status: 500 }
);
}
}
});
5 changes: 3 additions & 2 deletions ballotiq/src/app/api/gemini/insight/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
*/

import { NextRequest, NextResponse } from 'next/server';
import { withAuth } from '@/lib/security/auth';
import { generatePerformanceInsight } from '@/lib/gemini/quiz';
import type { KnowledgeLevel, QuizQuestion, QuizResult } from '@/types';

export async function POST(req: NextRequest) {
export const POST = withAuth(async (req: NextRequest) => {
try {
const body = await req.json() as {
results: QuizResult[];
Expand Down Expand Up @@ -45,4 +46,4 @@ export async function POST(req: NextRequest) {
{ status: 500 }
);
}
}
});
5 changes: 3 additions & 2 deletions ballotiq/src/app/api/gemini/micro-quiz/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
*/

import { NextRequest, NextResponse } from 'next/server';
import { withAuth } from '@/lib/security/auth';
import { generateMicroQuiz } from '@/lib/gemini/quiz';
import type { ElectionStep, KnowledgeLevel } from '@/types';

export async function POST(req: NextRequest) {
export const POST = withAuth(async (req: NextRequest) => {
try {
const body = await req.json() as {
step: ElectionStep;
Expand Down Expand Up @@ -41,4 +42,4 @@ export async function POST(req: NextRequest) {
{ status: 500 }
);
}
}
});
5 changes: 3 additions & 2 deletions ballotiq/src/app/api/gemini/quiz/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
*/

import { NextRequest, NextResponse } from 'next/server';
import { withAuth } from '@/lib/security/auth';
import { generatePersonalizedQuiz } from '@/lib/gemini/quiz';
import type { ElectionStep, KnowledgeLevel } from '@/types';

export async function POST(req: NextRequest) {
export const POST = withAuth(async (req: NextRequest) => {
try {
const body = await req.json() as {
completedSteps: ElectionStep[];
Expand Down Expand Up @@ -44,4 +45,4 @@ export async function POST(req: NextRequest) {
{ status: 500 }
);
}
}
});
5 changes: 3 additions & 2 deletions ballotiq/src/app/api/gemini/re-explain/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
*/

import { NextRequest, NextResponse } from 'next/server';
import { withAuth } from '@/lib/security/auth';
import { reExplainConcept } from '@/lib/gemini/assistant';
import type { ElectionStep, KnowledgeLevel } from '@/types';

export async function POST(req: NextRequest) {
export const POST = withAuth(async (req: NextRequest) => {
try {
const body = await req.json() as {
step: ElectionStep;
Expand Down Expand Up @@ -45,4 +46,4 @@ export async function POST(req: NextRequest) {
{ status: 500 }
);
}
}
});
63 changes: 63 additions & 0 deletions ballotiq/src/lib/security/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { NextRequest, NextResponse } from 'next/server';
import * as admin from 'firebase-admin';

// Initialize Firebase Admin if not already initialized
if (!admin.apps.length) {
try {
admin.initializeApp({
credential: admin.credential.applicationDefault(),
});
} catch (error) {
console.error('Firebase admin initialization error', error);
}
}

/**
* Validates the Authorization header and returns the decoded Firebase ID token.
*
* @param req NextRequest
* @returns DecodedIdToken if valid, null if invalid or missing
*/
export async function verifyAuthToken(req: NextRequest) {
try {
const authHeader = req.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return null;
}

const token = authHeader.split('Bearer ')[1];
const decodedToken = await admin.auth().verifyIdToken(token);
return decodedToken;
} catch (error) {
console.error('Error verifying auth token', error);
return null;
}
}

/**
* Middleware wrapper for Next.js API routes to enforce authentication.
* If the request is a state-mutating method (POST, PUT, PATCH, DELETE) and lacks a valid token,
* it returns a 401 Unauthorized response.
*
* @param handler The API route handler function
* @returns A wrapped API route handler
*/
export function withAuth(handler: (req: NextRequest, ...args: any[]) => Promise<NextResponse>) {
return async (req: NextRequest, ...args: any[]) => {
// Check if the request is state-mutating
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
const decodedToken = await verifyAuthToken(req);
if (!decodedToken) {
return NextResponse.json(
{ error: 'Unauthorized: Missing or invalid authentication token.' },
{ status: 401 }
);
}

// Inject user info into headers or pass to handler if needed
// (Next.js App router doesn't allow mutating req easily, but we know it's verified)
}

return handler(req, ...args);
};
}
Loading