auth the whole app

This commit is contained in:
zramsay 2025-03-11 12:12:48 -04:00
parent e0e3f79f38
commit bd44611448
6 changed files with 133 additions and 59 deletions

View File

@ -1,5 +1,6 @@
// src/app/api/analyze/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getSessionFromCookie } from '../../../lib/auth'
import { analyzeImageWithVision } from '../../../services/googleVisionCore'
import { processAnimalImage } from '../../../services/animalProcessingService'
@ -14,11 +15,23 @@ export const config = {
export async function POST(req: NextRequest): Promise<NextResponse> {
try {
// Check authentication
const session = await getSessionFromCookie(req)
if (!session) {
console.log('Unauthorized access attempt to analyze API')
return NextResponse.json(
{ error: 'Unauthorized. Please sign in to use this feature.' },
{ status: 401 }
)
}
// Log incoming request details
console.log('Incoming request:', {
method: req.method,
contentType: req.headers.get('content-type'),
contentLength: req.headers.get('content-length')
contentLength: req.headers.get('content-length'),
authenticated: true
})
// Parse form data
@ -27,7 +40,7 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
// For server-side, check if we have a valid Blob-like object
// In Node.js environment, Formidable gives us a Blob-like interface
if (!imageFile || !(typeof imageFile === 'object' && 'arrayBuffer' in imageFile)) {
if (!imageFile || !(typeof imageFile === 'object' && 'arrayBuffer' in (imageFile as object))) {
console.error('Invalid file upload:', {
imageFile: imageFile ? 'Exists' : 'Not found',
type: imageFile ? typeof imageFile : 'undefined'
@ -41,13 +54,13 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
// Log file details (some properties may be different from browser File objects)
console.log('Uploaded file details:', {
name: 'name' in imageFile ? imageFile.name : 'unknown',
type: 'type' in imageFile ? imageFile.type : 'unknown',
size: 'size' in imageFile ? imageFile.size : 'unknown'
name: 'name' in (imageFile as any) ? (imageFile as any).name : 'unknown',
type: 'type' in (imageFile as any) ? (imageFile as any).type : 'unknown',
size: 'size' in (imageFile as any) ? (imageFile as any).size : 'unknown'
})
// Convert image to buffer using arrayBuffer method
const arrayBuffer = await imageFile.arrayBuffer()
const arrayBuffer = await (imageFile as any).arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
// Log buffer details
@ -87,7 +100,7 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
// Background processing for animal images
if (visionResult.isAnimal) {
const fileName = 'name' in imageFile ? imageFile.name : 'unknown-image.jpg'
const fileName = 'name' in (imageFile as any) ? (imageFile as any).name : 'unknown-image.jpg'
processAnimalImage(
buffer,
visionResult.description,

View File

@ -1,7 +1,8 @@
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
export const GET = NextAuth({
// Define auth options
const authOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || '',
@ -22,27 +23,11 @@ export const GET = NextAuth({
return session;
},
},
}).handlers.GET;
};
export const POST = NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || '',
clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
}),
],
pages: {
signIn: '/auth/signin',
signOut: '/auth/signout',
error: '/auth/error',
},
callbacks: {
async session({ session, token }) {
// Add user info to the session
if (session.user && token.sub) {
session.user.id = token.sub;
}
return session;
},
},
}).handlers.POST;
// Create a handler with the auth options
const handler = NextAuth(authOptions);
// Export the handler functions
export const GET = handler.handlers.GET;
export const POST = handler.handlers.POST;

View File

@ -1,18 +1,22 @@
// src/app/page.tsx
'use client'
import React, { useState } from 'react'
import React from 'react'
import { useSession } from 'next-auth/react'
import ImageAnalysisCard from '../components/ImageAnalysisCard'
import Navigation from '../components/Navigation'
import { analyzeImage, VisionAnalysisResult, VISION_CONFIG } from '../services/googleVisionService'
import { APP_CONFIG, getThemeColors } from '../config/appConfig'
import { normalizeImageUpload } from '../services/imageService'
import { normalizeImageUpload } from '../services/imageService'
const Page: React.FC = (): React.ReactElement => {
// Get auth session
const { data: session, status } = useSession()
const isAuthenticated = status === 'authenticated' && !!session
const theme = getThemeColors(APP_CONFIG.theme)
const handleImageAnalysis = () => {
const handleImageAnalysis = () => {
return async (imageFile: File): Promise<VisionAnalysisResult> => {
try {
// Normalize the image first
@ -32,6 +36,41 @@ const Page: React.FC = (): React.ReactElement => {
}
}
// Auth-locked content
const renderAuthContent = () => {
if (isAuthenticated) {
return (
<div className="max-w-2xl mx-auto relative">
{/* Decorative elements */}
<div className="absolute -top-4 -left-4 w-8 h-8 bg-emerald-500/10 rounded-full blur-lg" />
<div className="absolute -bottom-4 -right-4 w-8 h-8 bg-teal-500/10 rounded-full blur-lg" />
<ImageAnalysisCard
title={VISION_CONFIG.name}
description={VISION_CONFIG.description}
onAnalyze={handleImageAnalysis()}
/>
</div>
)
}
// For non-authenticated users, show sign-in prompt
return (
<div className="max-w-2xl mx-auto bg-emerald-900/20 backdrop-blur-lg rounded-2xl shadow-xl border border-emerald-800/50 p-8 text-center">
<h2 className="text-2xl font-bold text-emerald-300 mb-4">Sign in to Document Wildlife</h2>
<p className="text-emerald-200 mb-6">
You need to sign in with your account to access wildlife documentation features.
</p>
<a
href="/auth/signin"
className="inline-block text-white bg-emerald-600 hover:bg-emerald-700 font-medium rounded-lg px-6 py-3 text-center transition-colors"
>
Sign in with Google
</a>
</div>
)
}
return (
<div className={`min-h-screen w-full flex flex-col items-center bg-gradient-to-b ${theme.gradient}`}>
<div className="container max-w-7xl mx-auto px-4 py-8">
@ -45,31 +84,22 @@ const Page: React.FC = (): React.ReactElement => {
Document sightings of {APP_CONFIG.description} in {APP_CONFIG.location}
</p>
<div className="mb-8 p-4 bg-emerald-900/30 rounded-lg inline-block">
<a
href="/auth/signin"
className="text-white bg-emerald-600 hover:bg-emerald-700 font-medium rounded-lg text-sm px-5 py-2.5 text-center"
>
Sign in to get started
</a>
</div>
{!isAuthenticated && status !== 'loading' && (
<div className="mb-8 p-4 bg-emerald-900/30 rounded-lg inline-block">
<a
href="/auth/signin"
className="text-white bg-emerald-600 hover:bg-emerald-700 font-medium rounded-lg text-sm px-5 py-2.5 text-center"
>
Sign in to get started
</a>
</div>
)}
</div>
{/* Single Analysis Card */}
<div className="max-w-2xl mx-auto relative">
{/* Decorative elements */}
<div className="absolute -top-4 -left-4 w-8 h-8 bg-emerald-500/10 rounded-full blur-lg" />
<div className="absolute -bottom-4 -right-4 w-8 h-8 bg-teal-500/10 rounded-full blur-lg" />
<ImageAnalysisCard
title={VISION_CONFIG.name}
description={VISION_CONFIG.description}
onAnalyze={handleImageAnalysis()} // ?
/>
</div>
{/* Render content based on authentication status */}
{renderAuthContent()}
{/* Info Section (not needed right now) */}
{/* Info Section */}
<div className="mt-12 text-center text-emerald-300/60">
<p className="text-sm">
Built by <a href="https://mito.systems">Mito Systems</a> - Powered by <a href="https://laconic.com">Laconic</a>

View File

@ -4,6 +4,7 @@
import { useState, useEffect } from 'react'
import { MapPin } from 'lucide-react'
import { useSession } from 'next-auth/react'
import { fetchAnimalRecords } from '../../services/laconicQueryService'
import Navigation from '../../components/Navigation'
import { AnimalRecord } from '../../types/records'
@ -15,13 +16,22 @@ const hiddenIndices = (process.env.NEXT_PUBLIC_HIDDEN_INDICES?.split(',') || [])
.filter(num => !isNaN(num))
export default function AnimalsPage() {
const { data: session, status } = useSession()
const isAuthenticated = status === 'authenticated' && !!session
const [records, setRecords] = useState<AnimalRecord[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
loadRecords()
}, [])
// Only load records if user is authenticated
if (isAuthenticated) {
loadRecords()
} else if (status !== 'loading') {
// If user is not authenticated and we've finished loading auth status
setLoading(false)
}
}, [isAuthenticated, status])
const loadRecords = async () => {
try {
@ -49,6 +59,34 @@ export default function AnimalsPage() {
}
const renderContent = () => {
// Show loading indicator while authenticating
if (status === 'loading') {
return (
<div className="text-emerald-200 text-center p-8">
Checking authentication...
</div>
)
}
// Show sign-in prompt if not authenticated
if (!isAuthenticated) {
return (
<div className="max-w-2xl mx-auto bg-emerald-900/20 backdrop-blur-lg rounded-2xl shadow-xl border border-emerald-800/50 p-8 text-center">
<h2 className="text-2xl font-bold text-emerald-300 mb-4">Authentication Required</h2>
<p className="text-emerald-200 mb-6">
You need to sign in to view wildlife sightings from our community.
</p>
<a
href="/auth/signin"
className="inline-block text-white bg-emerald-600 hover:bg-emerald-700 font-medium rounded-lg px-6 py-3 text-center transition-colors"
>
Sign in with Google
</a>
</div>
)
}
// Show loading state for records
if (loading) {
return (
<div className="text-emerald-200 text-center">

7
src/lib/auth.ts Normal file
View File

@ -0,0 +1,7 @@
import { NextRequest } from "next/server";
// Simple helper to check for the auth cookie
export async function getSessionFromCookie(req: NextRequest) {
const sessionCookie = req.cookies.get('next-auth.session-token');
return sessionCookie ? { isAuthenticated: true } : null;
}

View File

@ -25,7 +25,8 @@
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"strictNullChecks": false
},
"include": [
"next-env.d.ts",