mirror of
https://github.com/mito-systems/ranger-app.git
synced 2026-09-08 14:14:06 +00:00
add google auth signin
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
|
||||
Generated
+119
-340
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -25,13 +25,9 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
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<NextResponse> {
|
||||
{ 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<NextResponse> {
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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 (
|
||||
<div className="min-h-screen bg-gray-900 flex flex-col justify-center items-center p-4">
|
||||
<div className="w-full max-w-md bg-gray-800 rounded-lg shadow-lg p-6">
|
||||
<h1 className="text-2xl font-bold text-red-400 mb-6 text-center">Authentication Error</h1>
|
||||
|
||||
<div className="bg-red-900/20 border border-red-700 rounded-md p-4 mb-6">
|
||||
<p className="text-red-200 text-center">{errorMessage}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center space-x-4">
|
||||
<Link
|
||||
href="/auth/signin"
|
||||
className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
|
||||
>
|
||||
Try Again
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/"
|
||||
className="bg-gray-700 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-md transition-colors"
|
||||
>
|
||||
Return Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AuthErrorPage = () => {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="min-h-screen bg-gray-900 flex flex-col justify-center items-center p-4">
|
||||
<div className="w-full max-w-md bg-gray-800 rounded-lg shadow-lg p-6">
|
||||
<h1 className="text-2xl font-bold text-red-400 mb-6 text-center">Loading...</h1>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<ErrorContent />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthErrorPage;
|
||||
@@ -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 (
|
||||
<div className="min-h-screen bg-gray-900 flex flex-col justify-center items-center p-4">
|
||||
<div className="w-full max-w-md bg-gray-800 rounded-lg shadow-lg p-6">
|
||||
<h1 className="text-2xl font-bold text-emerald-400 mb-6 text-center">Sign In to Ranger</h1>
|
||||
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={handleGoogleSignIn}
|
||||
className="w-full flex items-center justify-center space-x-2 bg-white hover:bg-gray-100 text-gray-800 font-medium py-3 px-4 rounded-md transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032s2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.503,2.988,15.139,2,12.545,2C7.021,2,2.543,6.477,2.543,12s4.478,10,10.002,10c8.396,0,10.249-7.85,9.426-11.748L12.545,10.239z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Continue with Google</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="text-emerald-300 hover:text-emerald-200 text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignInPage;
|
||||
@@ -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 (
|
||||
<div className="min-h-screen bg-gray-900 flex flex-col justify-center items-center p-4">
|
||||
<div className="w-full max-w-md bg-gray-800 rounded-lg shadow-lg p-6">
|
||||
<h1 className="text-2xl font-bold text-emerald-400 mb-6 text-center">Sign Out</h1>
|
||||
|
||||
<p className="text-gray-300 text-center mb-6">
|
||||
Are you sure you want to sign out?
|
||||
</p>
|
||||
|
||||
<div className="flex justify-center space-x-4">
|
||||
<button
|
||||
onClick={handleSignOut}
|
||||
disabled={isSigningOut}
|
||||
className={`bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-4 rounded-md transition-colors ${
|
||||
isSigningOut ? 'opacity-50 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
{isSigningOut ? 'Signing Out...' : 'Sign Out'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
disabled={isSigningOut}
|
||||
className={`bg-gray-700 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-md transition-colors ${
|
||||
isSigningOut ? 'opacity-50 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignOutPage;
|
||||
@@ -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<any>(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 (
|
||||
<div className="min-h-screen bg-gray-900 p-8">
|
||||
<div className="max-w-4xl mx-auto bg-gray-800 rounded-lg p-6 text-white">
|
||||
<h1 className="text-2xl font-bold text-emerald-400 mb-6">Auth Debug Page</h1>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
<div className="bg-gray-700 p-4 rounded-lg">
|
||||
<h2 className="text-xl font-bold text-emerald-300 mb-2">Session Status</h2>
|
||||
<p>Status: <span className="text-emerald-200">{status}</span></p>
|
||||
<pre className="bg-gray-900 p-4 rounded mt-2 overflow-auto max-h-60">
|
||||
{JSON.stringify(session, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-700 p-4 rounded-lg">
|
||||
<h2 className="text-xl font-bold text-emerald-300 mb-2">Auth API Response</h2>
|
||||
<pre className="bg-gray-900 p-4 rounded mt-2 overflow-auto max-h-60">
|
||||
{JSON.stringify(authData, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-700 p-4 rounded-lg flex flex-col space-y-4">
|
||||
<h2 className="text-xl font-bold text-emerald-300 mb-2">Auth Actions</h2>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<button
|
||||
onClick={() => signIn('google', { callbackUrl: '/' })}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded"
|
||||
>
|
||||
Sign in with Google
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => signOut({ callbackUrl: '/' })}
|
||||
className="px-4 py-2 bg-red-600 hover:bg-red-700 rounded"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
|
||||
<Link href="/" className="px-4 py-2 bg-gray-600 hover:bg-gray-500 rounded">
|
||||
Back to Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+6
-1
@@ -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 (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
<body className={inter.className}>
|
||||
<SessionProvider>
|
||||
{children}
|
||||
</SessionProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,15 @@ 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>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Single Analysis Card */}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<div className="aspect-video rounded-lg overflow-hidden mb-4 bg-emerald-900/50">
|
||||
<img
|
||||
src={record.attributes?.imageUrl}
|
||||
alt={`Wildlife sighting`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{record.attributes?.imageUrl ? (
|
||||
<>
|
||||
<div className="absolute top-0 right-0 bg-black/70 text-xs text-white p-1 z-10">
|
||||
URL: {record.attributes.imageUrl.substring(0, 30)}...
|
||||
</div>
|
||||
<img
|
||||
src={record.attributes.imageUrl}
|
||||
alt={`Wildlife sighting of ${record.attributes.mainObject}`}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
console.error(`Image load error for: ${record.attributes.imageUrl}`);
|
||||
e.currentTarget.src = 'https://placehold.co/600x400?text=Image+Error';
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full bg-emerald-900/30 text-emerald-200">
|
||||
No image available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-semibold text-emerald-300 capitalize mb-2">
|
||||
|
||||
@@ -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 (
|
||||
<button className="text-sm text-emerald-200/70 bg-transparent" disabled>
|
||||
Loading...
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (session?.user) {
|
||||
return (
|
||||
<div className="flex items-center space-x-3">
|
||||
{session.user.image && (
|
||||
<img
|
||||
src={session.user.image}
|
||||
alt={session.user.name || 'User'}
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-emerald-200">
|
||||
{session.user.name}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleSignOut}
|
||||
className="text-xs text-emerald-200/70 hover:text-emerald-200 text-left"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleSignIn}
|
||||
className="text-sm text-emerald-200/70 hover:text-emerald-200 bg-transparent"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthButton;
|
||||
@@ -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 (
|
||||
<nav className="absolute left-4 top-8">
|
||||
<ul className="flex space-x-6">
|
||||
{links.map(({ href, label }) => (
|
||||
<li key={href}>
|
||||
<Link
|
||||
href={href}
|
||||
className={`text-sm transition-colors duration-200 ${
|
||||
pathname === href
|
||||
? 'text-emerald-400'
|
||||
: 'text-emerald-200/70 hover:text-emerald-200'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="absolute w-full px-4 top-8 flex justify-between items-center bg-gray-900/50 py-2 rounded-lg">
|
||||
<nav>
|
||||
<ul className="flex space-x-6">
|
||||
{links.map(({ href, label }) => (
|
||||
<li key={href}>
|
||||
<Link
|
||||
href={href}
|
||||
className={`text-sm transition-colors duration-200 ${
|
||||
pathname === href
|
||||
? 'text-emerald-400 font-bold'
|
||||
: 'text-emerald-200/70 hover:text-emerald-200'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div className="mr-4 border border-emerald-400/30 px-3 py-1 rounded">
|
||||
<AuthButton />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <NextAuthSessionProvider>{children}</NextAuthSessionProvider>;
|
||||
};
|
||||
|
||||
export default SessionProvider;
|
||||
@@ -101,7 +101,17 @@ export async function fetchAnimalRecords(portalName: string): Promise<AnimalReco
|
||||
record.attributes.portalName === process.env.NEXT_PUBLIC_PORTAL_NAME
|
||||
);
|
||||
|
||||
console.log('Processed animal records:', records);
|
||||
// Log more detailed information about each image URL
|
||||
console.log('Processed animal records count:', records.length);
|
||||
|
||||
if (records.length > 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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+21
@@ -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;
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -21,7 +21,11 @@
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"target": "ES2017"
|
||||
"target": "ES2017",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
|
||||
Reference in New Issue
Block a user