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
+
+
+
+
+
+ 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"
>
-

+ {record.attributes?.imageUrl ? (
+ <>
+
+ URL: {record.attributes.imageUrl.substring(0, 30)}...
+
+

{
+ 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}
+
+
+
+
+ );
+ }
+
+ 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",