From e0e3f79f3882634f184cc1fffbffa0de61e1ef44 Mon Sep 17 00:00:00 2001 From: zramsay Date: Tue, 11 Mar 2025 12:01:21 -0400 Subject: [PATCH] add google auth signin --- CLAUDE.md | 27 ++ README.md | 55 +++ package-lock.json | 459 ++++++------------------ package.json | 1 + src/app/api/analyze/route.ts | 27 +- src/app/api/auth/[...nextauth]/route.ts | 48 +++ src/app/auth/error/page.tsx | 67 ++++ src/app/auth/signin/page.tsx | 52 +++ src/app/auth/signout/page.tsx | 64 ++++ src/app/debug/page.tsx | 72 ++++ src/app/layout.tsx | 7 +- src/app/page.tsx | 9 + src/app/sightings/page.tsx | 25 +- src/components/AuthButton.tsx | 62 ++++ src/components/Navigation.tsx | 47 ++- src/components/SessionProvider.tsx | 14 + src/services/laconicQueryService.ts | 12 +- src/services/pinataService.ts | 92 +++-- src/types/auth.d.ts | 21 ++ tsconfig.json | 6 +- 20 files changed, 752 insertions(+), 415 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/app/api/auth/[...nextauth]/route.ts create mode 100644 src/app/auth/error/page.tsx create mode 100644 src/app/auth/signin/page.tsx create mode 100644 src/app/auth/signout/page.tsx create mode 100644 src/app/debug/page.tsx create mode 100644 src/components/AuthButton.tsx create mode 100644 src/components/SessionProvider.tsx create mode 100644 src/types/auth.d.ts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..98c9e7f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,27 @@ +# Wildlife Project Development Guidelines + +## Build/Lint/Test Commands +- `npm run dev` - Start development server +- `npm run build` - Build for production +- `npm run start` - Start production server +- `npm run lint` - Run ESLint + +## Code Style Guidelines +- **Imports**: Group imports by type (React, components, services, config) +- **Types**: Use TypeScript interfaces for data structures, React.FC for components +- **React Components**: Use functional components with explicit return types +- **Error Handling**: Use try/catch with detailed error logging in services +- **Naming Conventions**: + - PascalCase for components (ImageAnalysisCard.tsx) + - camelCase for functions, variables, and file names (googleVisionService.ts) + - Use descriptive names for functions and variables +- **Services**: Keep service files focused on a single responsibility +- **State Management**: Use React hooks for local state +- **Styling**: Use Tailwind CSS with theme configurations from appConfig + +## Project Structure +- `/src/app` - Next.js pages and API routes +- `/src/components` - Reusable UI components +- `/src/services` - Business logic and API services +- `/src/config` - Application configuration +- `/src/types` - TypeScript type definitions \ No newline at end of file diff --git a/README.md b/README.md index a240fc2..75bf626 100644 --- a/README.md +++ b/README.md @@ -32,3 +32,58 @@ Receive links to custom portals and are invited to add data by uploading photogr ### Data consumers Query a data set from any custom portal + +## Social Login Setup + +This application supports social login with Google and GitHub. To set up social login, follow these steps: + +1. **Set up environment variables**: + Update the following variables in your `.env.local` file: + ``` + # Authentication + NEXTAUTH_URL=http://localhost:3000 + NEXTAUTH_SECRET=your-nextauth-secret-key-at-least-32-chars + + # OAuth Providers + GOOGLE_CLIENT_ID=your-google-client-id + GOOGLE_CLIENT_SECRET=your-google-client-secret + + GITHUB_CLIENT_ID=your-github-client-id + GITHUB_CLIENT_SECRET=your-github-client-secret + ``` + +2. **Create OAuth applications**: + + **For Google**: + - Go to [Google Cloud Console](https://console.cloud.google.com/) + - Create a new project or select an existing one + - Navigate to "APIs & Services" > "Credentials" + - Click "Create Credentials" > "OAuth client ID" + - Select "Web application" as the application type + - Add your app's URL to the authorized JavaScript origins (e.g., `http://localhost:3000`) + - Add your app's callback URL to the authorized redirect URIs (e.g., `http://localhost:3000/api/auth/callback/google`) + - Copy the Client ID and Client Secret to your `.env.local` file + + **For GitHub**: + - Go to [GitHub Developer Settings](https://github.com/settings/developers) + - Click on "New OAuth App" + - Fill in the application details + - Set the Authorization callback URL to your app's callback URL (e.g., `http://localhost:3000/api/auth/callback/github`) + - Copy the Client ID and Client Secret to your `.env.local` file + +3. **Generate a secure NEXTAUTH_SECRET**: + Run the following command to generate a secure secret: + ```bash + openssl rand -base64 32 + ``` + Copy the output to your `.env.local` file as the NEXTAUTH_SECRET value. + +4. **Install required packages**: + ```bash + npm install next-auth@4.22.1 + ``` + +5. **Restart your application**: + ```bash + npm run dev + ``` diff --git a/package-lock.json b/package-lock.json index 6a182f9..6089ba2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "lucide-react": "^0.469.0", "multer": "^1.4.5-lts.1", "next": "^15.1.4", + "next-auth": "^5.0.0-beta.25", "next-connect": "^1.0.0", "openai": "^4.77.0", "react": "^18", @@ -55,6 +56,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@auth/core": { + "version": "0.37.2", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.2.tgz", + "integrity": "sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "@types/cookie": "0.6.0", + "cookie": "0.7.1", + "jose": "^5.9.3", + "oauth4webapi": "^3.0.0", + "preact": "10.11.3", + "preact-render-to-string": "5.2.3" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/@babel/runtime": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", @@ -66,16 +97,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", - "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -311,28 +332,6 @@ "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, "node_modules/@img/sharp-libvips-darwin-arm64": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", @@ -349,307 +348,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.2.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -798,7 +496,6 @@ "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -814,7 +511,6 @@ "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -830,7 +526,6 @@ "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -846,7 +541,6 @@ "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -862,7 +556,6 @@ "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -878,7 +571,6 @@ "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -894,7 +586,6 @@ "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -983,6 +674,14 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1320,6 +1019,11 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, "node_modules/@types/exif-js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@types/exif-js/-/exif-js-2.3.1.tgz", @@ -2539,6 +2243,14 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -4961,6 +4673,14 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5409,6 +5129,32 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.25", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.25.tgz", + "integrity": "sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog==", + "dependencies": { + "@auth/core": "0.37.2" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0-0", + "nodemailer": "^6.6.5", + "react": "^18.2.0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next-connect": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-connect/-/next-connect-1.0.0.tgz", @@ -5494,6 +5240,14 @@ "node": ">=0.10.0" } }, + "node_modules/oauth4webapi": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.3.1.tgz", + "integrity": "sha512-ZwX7UqYrP3Lr+Glhca3a1/nF2jqf7VVyJfhGuW5JtrfDUxt0u+IoBPzFjZ2dd7PJGkdM6CFPVVYzuDYKHv101A==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5991,6 +5745,26 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/preact": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", + "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", + "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -6000,6 +5774,11 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/package.json b/package.json index 1fe5fb2..5fda396 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "lucide-react": "^0.469.0", "multer": "^1.4.5-lts.1", "next": "^15.1.4", + "next-auth": "^5.0.0-beta.25", "next-connect": "^1.0.0", "openai": "^4.77.0", "react": "^18", diff --git a/src/app/api/analyze/route.ts b/src/app/api/analyze/route.ts index d14c2ae..17dca63 100644 --- a/src/app/api/analyze/route.ts +++ b/src/app/api/analyze/route.ts @@ -25,13 +25,9 @@ export async function POST(req: NextRequest): Promise { const formData = await req.formData() const imageFile = formData.get('image') - // More explicit type checking and conversion - const file = imageFile instanceof File - ? imageFile - : (Array.isArray(imageFile) ? imageFile[0] : null) - - // Validate image file - if (!file) { + // 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)) { console.error('Invalid file upload:', { imageFile: imageFile ? 'Exists' : 'Not found', type: imageFile ? typeof imageFile : 'undefined' @@ -42,16 +38,16 @@ export async function POST(req: NextRequest): Promise { { status: 400 } ) } - - // Log file details + + // Log file details (some properties may be different from browser File objects) console.log('Uploaded file details:', { - name: file.name, - type: file.type, - size: file.size + name: 'name' in imageFile ? imageFile.name : 'unknown', + type: 'type' in imageFile ? imageFile.type : 'unknown', + size: 'size' in imageFile ? imageFile.size : 'unknown' }) - // Convert image to buffer - const arrayBuffer = await file.arrayBuffer() + // Convert image to buffer using arrayBuffer method + const arrayBuffer = await imageFile.arrayBuffer() const buffer = Buffer.from(arrayBuffer) // Log buffer details @@ -91,11 +87,12 @@ export async function POST(req: NextRequest): Promise { // Background processing for animal images if (visionResult.isAnimal) { + const fileName = 'name' in imageFile ? imageFile.name : 'unknown-image.jpg' processAnimalImage( buffer, visionResult.description, visionResult.rawResponse, - file.name + fileName ).catch(console.error) } diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..5b7eb47 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,48 @@ +import NextAuth from "next-auth"; +import GoogleProvider from "next-auth/providers/google"; + +export const GET = 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.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; \ No newline at end of file diff --git a/src/app/auth/error/page.tsx b/src/app/auth/error/page.tsx new file mode 100644 index 0000000..b57001a --- /dev/null +++ b/src/app/auth/error/page.tsx @@ -0,0 +1,67 @@ +'use client'; + +import Link from 'next/link'; +import { useSearchParams } from 'next/navigation'; +import { Suspense } from 'react'; + +const ErrorContent = () => { + const searchParams = useSearchParams(); + const error = searchParams.get('error'); + + let errorMessage = 'An unknown error occurred during authentication.'; + + if (error === 'OAuthSignin') errorMessage = 'Error during OAuth sign-in process.'; + if (error === 'OAuthCallback') errorMessage = 'Error during OAuth callback.'; + if (error === 'OAuthCreateAccount') errorMessage = 'Error creating OAuth user.'; + if (error === 'EmailCreateAccount') errorMessage = 'Error creating email user.'; + if (error === 'Callback') errorMessage = 'Error during callback handling.'; + if (error === 'OAuthAccountNotLinked') errorMessage = 'Email already used with a different provider.'; + if (error === 'EmailSignin') errorMessage = 'Error sending email sign-in link.'; + if (error === 'CredentialsSignin') errorMessage = 'Sign in failed. Check your credentials.'; + if (error === 'SessionRequired') errorMessage = 'You must be signed in to access this page.'; + if (error === 'Default') errorMessage = 'An unknown error occurred.'; + + return ( +
+
+

Authentication Error

+ +
+

{errorMessage}

+
+ +
+ + Try Again + + + + Return Home + +
+
+
+ ); +}; + +const AuthErrorPage = () => { + return ( + +
+

Loading...

+
+ + }> + +
+ ); +}; + +export default AuthErrorPage; \ No newline at end of file diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx new file mode 100644 index 0000000..3f24b56 --- /dev/null +++ b/src/app/auth/signin/page.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { signIn } from 'next-auth/react'; +import { useCallback, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +const SignInPage = () => { + const router = useRouter(); + + const handleGoogleSignIn = useCallback(() => { + console.log('Signing in with Google...'); + signIn('google', { callbackUrl: '/' }); + }, []); + + const handleCancel = useCallback(() => { + router.push('/'); + }, [router]); + + return ( +
+
+

Sign In to Ranger

+ +
+ +
+ +
+ +
+
+
+ ); +}; + +export default SignInPage; \ No newline at end of file diff --git a/src/app/auth/signout/page.tsx b/src/app/auth/signout/page.tsx new file mode 100644 index 0000000..e8a0066 --- /dev/null +++ b/src/app/auth/signout/page.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { signOut } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import { useCallback, useEffect, useState } from 'react'; + +const SignOutPage = () => { + const router = useRouter(); + const [isSigningOut, setIsSigningOut] = useState(false); + + const handleSignOut = useCallback(() => { + setIsSigningOut(true); + signOut({ callbackUrl: '/' }); + }, []); + + const handleCancel = useCallback(() => { + router.back(); + }, [router]); + + // Auto redirect after 5 seconds + useEffect(() => { + const timer = setTimeout(() => { + router.push('/'); + }, 5000); + + return () => clearTimeout(timer); + }, [router]); + + return ( +
+
+

Sign Out

+ +

+ Are you sure you want to sign out? +

+ +
+ + + +
+
+
+ ); +}; + +export default SignOutPage; \ No newline at end of file diff --git a/src/app/debug/page.tsx b/src/app/debug/page.tsx new file mode 100644 index 0000000..8652e2e --- /dev/null +++ b/src/app/debug/page.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useSession, signIn, signOut } from 'next-auth/react'; +import { useEffect, useState } from 'react'; +import Link from 'next/link'; + +export default function DebugPage() { + const { data: session, status } = useSession(); + const [authData, setAuthData] = useState(null); + + useEffect(() => { + const checkAuth = async () => { + try { + const res = await fetch('/api/auth/session'); + const data = await res.json(); + setAuthData(data); + } catch (error) { + console.error('Error fetching auth session', error); + } + }; + + checkAuth(); + }, []); + + return ( +
+
+

Auth Debug Page

+ +
+
+

Session Status

+

Status: {status}

+
+              {JSON.stringify(session, null, 2)}
+            
+
+ +
+

Auth API Response

+
+              {JSON.stringify(authData, null, 2)}
+            
+
+ +
+

Auth Actions

+
+ + + + + + Back to Home + +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1d8ac5a..9fb1bd4 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next' import { Inter } from 'next/font/google' import './globals.css' +import SessionProvider from '../components/SessionProvider' const inter = Inter({ subsets: ['latin'] }) @@ -16,7 +17,11 @@ export default function RootLayout({ }) { return ( - {children} + + + {children} + + ) } diff --git a/src/app/page.tsx b/src/app/page.tsx index 056e374..14d48b9 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -45,6 +45,15 @@ const Page: React.FC = (): React.ReactElement => { Document sightings of {APP_CONFIG.description} in {APP_CONFIG.location}

+ + {/* Single Analysis Card */} diff --git a/src/app/sightings/page.tsx b/src/app/sightings/page.tsx index 7d92bd8..52d8d47 100644 --- a/src/app/sightings/page.tsx +++ b/src/app/sightings/page.tsx @@ -81,11 +81,26 @@ export default function AnimalsPage() { className="bg-emerald-900/20 rounded-xl p-6 border border-emerald-800/50 hover:border-emerald-700/50 transition-colors" >
- {`Wildlife + {record.attributes?.imageUrl ? ( + <> +
+ URL: {record.attributes.imageUrl.substring(0, 30)}... +
+ {`Wildlife { + console.error(`Image load error for: ${record.attributes.imageUrl}`); + e.currentTarget.src = 'https://placehold.co/600x400?text=Image+Error'; + }} + /> + + ) : ( +
+ No image available +
+ )}

diff --git a/src/components/AuthButton.tsx b/src/components/AuthButton.tsx new file mode 100644 index 0000000..0616079 --- /dev/null +++ b/src/components/AuthButton.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { signIn, signOut, useSession } from 'next-auth/react'; +import Link from 'next/link'; +import { useCallback } from 'react'; + +const AuthButton = () => { + const { data: session, status } = useSession(); + const isLoading = status === 'loading'; + + const handleSignIn = useCallback(() => { + signIn('google', { callbackUrl: '/' }); + }, []); + + const handleSignOut = useCallback(() => { + signOut({ callbackUrl: '/' }); + }, []); + + if (isLoading) { + return ( + + ); + } + + if (session?.user) { + return ( +
+ {session.user.image && ( + {session.user.name + )} +
+ + {session.user.name} + + +
+
+ ); + } + + return ( + + ); +}; + +export default AuthButton; \ No newline at end of file diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index 71a43e6..2dc81e7 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -3,6 +3,7 @@ import Link from 'next/link' import { usePathname } from 'next/navigation' +import AuthButton from './AuthButton' const Navigation = () => { const pathname = usePathname() @@ -10,28 +11,36 @@ const Navigation = () => { const links = [ { href: '/', label: 'Home' }, { href: '/sightings', label: 'Sightings' }, - { href: '/about', label: 'About' } + { href: '/about', label: 'About' }, + { href: '/auth/signin', label: 'Sign In' }, + { href: '/debug', label: 'Debug' }, ] return ( - +
+ + +
+ +
+
) } diff --git a/src/components/SessionProvider.tsx b/src/components/SessionProvider.tsx new file mode 100644 index 0000000..5fe9e88 --- /dev/null +++ b/src/components/SessionProvider.tsx @@ -0,0 +1,14 @@ +'use client'; + +import { SessionProvider as NextAuthSessionProvider } from 'next-auth/react'; +import { ReactNode } from 'react'; + +interface SessionProviderProps { + children: ReactNode; +} + +const SessionProvider = ({ children }: SessionProviderProps) => { + return {children}; +}; + +export default SessionProvider; \ No newline at end of file diff --git a/src/services/laconicQueryService.ts b/src/services/laconicQueryService.ts index 0428c73..9a2ef86 100644 --- a/src/services/laconicQueryService.ts +++ b/src/services/laconicQueryService.ts @@ -101,7 +101,17 @@ export async function fetchAnimalRecords(portalName: string): Promise 0) { + console.log('Image URLs from records:'); + records.forEach((record, index) => { + console.log(`Record ${index + 1} image URL: ${record.attributes.imageUrl}`); + }); + } else { + console.log('No records found with valid image URLs'); + } return records; } catch (error) { diff --git a/src/services/pinataService.ts b/src/services/pinataService.ts index 8f2c7e1..957cddc 100644 --- a/src/services/pinataService.ts +++ b/src/services/pinataService.ts @@ -1,4 +1,6 @@ // src/services/pinataService.ts +import FormData from 'form-data'; +import axios from 'axios'; interface PinataResponse { IpfsHash: string @@ -15,53 +17,77 @@ export async function uploadToIpfs(imageBuffer: Buffer, filename: string): Promi throw new Error('Pinata configuration missing') } - // Use native FormData creation - const formData = new FormData() + // Use Node.js form-data library for server-side + const formData = new FormData(); - // Create file from buffer - // Use File instead of Blob for better compatibility - const file = new File([imageBuffer], filename, { type: 'image/jpeg' }) - formData.append('file', file, filename) + // In Node.js, we can append buffer directly with filename + formData.append('file', imageBuffer, { + filename, + contentType: 'image/jpeg' + }); // Add metadata const pinataMetadata = JSON.stringify({ name: filename, - }) - formData.append('pinataMetadata', pinataMetadata) + }); + formData.append('pinataMetadata', pinataMetadata); // Add options const pinataOptions = JSON.stringify({ cidVersion: 1, - }) - formData.append('pinataOptions', pinataOptions) + }); + formData.append('pinataOptions', pinataOptions); console.log('Uploading to Pinata:', { filename, size: imageBuffer.length - }) + }); + + // Add more detailed logging + console.log('About to send request to Pinata with headers:', { + 'Authorization': 'Bearer [REDACTED]', + ...formData.getHeaders(), + }); + + try { + // Use axios instead of fetch as it handles Node.js FormData better + const response = await axios.post( + 'https://api.pinata.cloud/pinning/pinFileToIPFS', + formData, + { + headers: { + 'Authorization': `Bearer ${jwt}`, + ...formData.getHeaders(), + }, + maxBodyLength: Infinity, // This is needed to prevent axios from limiting the size + } + ); - const response = await fetch('https://api.pinata.cloud/pinning/pinFileToIPFS', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${jwt}` - }, - body: formData, - }) - - console.log('Pinata response status:', response.status) - const data = await response.json() - console.log('Pinata response:', data) - - if (!response.ok) { - throw new Error(`Failed to upload to IPFS: ${response.status} - ${JSON.stringify(data)}`) + console.log('Pinata response status:', response.status); + const data = response.data; + console.log('Pinata response:', data); + + if (response.status !== 200) { + throw new Error(`Failed to upload to IPFS: ${response.status} - ${JSON.stringify(data)}`); + } + + // Log the full URL that will be used + const ipfsUrl = `${baseUrl}/ipfs/${data.IpfsHash}`; + console.log('Complete IPFS URL for image:', ipfsUrl); + + return ipfsUrl; + + } catch (error) { + console.error('Axios request to Pinata failed:', { + message: error.message, + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data + }); + throw error; } - - const ipfsUrl = `${baseUrl}/ipfs/${data.IpfsHash}` - console.log('Uploaded to IPFS:', ipfsUrl) - return ipfsUrl - } catch (error) { - console.error('IPFS upload failed:', error) - throw error + console.error('IPFS upload failed:', error); + throw error; } -} +} \ No newline at end of file diff --git a/src/types/auth.d.ts b/src/types/auth.d.ts new file mode 100644 index 0000000..525de87 --- /dev/null +++ b/src/types/auth.d.ts @@ -0,0 +1,21 @@ +import "next-auth"; + +declare module "next-auth" { + interface Session { + user: { + id: string; + name?: string | null; + email?: string | null; + image?: string | null; + }; + } +} + +declare module "next-auth/jwt" { + interface JWT { + sub: string; + name?: string | null; + email?: string | null; + picture?: string | null; + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index c175f95..8b06edf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,11 @@ "name": "next" } ], - "target": "ES2017" + "target": "ES2017", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } }, "include": [ "next-env.d.ts",