From 7741de2a1cac660199adffc193b9284f51fb2c21 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Sun, 27 Jul 2025 00:32:56 +0200 Subject: [PATCH] Add google Auth --- .gitignore | 1 + docs/google-oauth-setup.md | 89 ++++++++++++++++++++++++++++++++++++++ scripts/setup-env.js | 83 +++++++++++++++++++++++++++++++++++ src/app/dashboard/page.tsx | 2 +- src/auth.ts | 28 ++++++++++++ src/lib/prisma.ts | 2 +- 6 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 docs/google-oauth-setup.md create mode 100755 scripts/setup-env.js diff --git a/.gitignore b/.gitignore index a8d1d34e..7aeaa1b9 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ notebooks martin /src/generated/prisma +*.db diff --git a/docs/google-oauth-setup.md b/docs/google-oauth-setup.md new file mode 100644 index 00000000..2c735811 --- /dev/null +++ b/docs/google-oauth-setup.md @@ -0,0 +1,89 @@ +# Google OAuth Setup Guide + +## Prerequisites + +1. A Google Cloud Platform (GCP) account +2. A GCP project with the Google+ API enabled +3. OAuth consent screen configured + +## Step 1: Create OAuth Credentials + +1. Go to the [Google Cloud Console](https://console.cloud.google.com/) +2. Select your project or create a new one +3. Navigate to "APIs & Services" > "Credentials" +4. Click "Create Credentials" and select "OAuth client ID" + +## Step 2: Configure OAuth Consent Screen + +1. If you haven't configured the consent screen, click "Configure Consent Screen" +2. Choose "External" user type and click "Create" +3. Fill in the required app information: + - App name (e.g., "My App") + - User support email (your email) + - Developer contact information (your email) +4. Click "Save and Continue" +5. Skip the "Scopes" step (we'll use the default scopes) +6. Add test users (your email) if you're in testing mode +7. Click "Save and Continue" + +## Step 3: Create OAuth Client ID + +1. Go back to "Credentials" +2. Click "Create Credentials" > "OAuth client ID" +3. Select "Web application" as the application type +4. Enter a name (e.g., "Web Client") +5. Under "Authorized JavaScript origins", add: + - `http://localhost:3000` + - `http://localhost:3001` +6. Under "Authorized redirect URIs", add: + - `http://localhost:3000/api/auth/callback/google` + - `http://localhost:3001/api/auth/callback/google` +7. Click "Create" + +## Step 4: Set Up Environment Variables + +Create or update your `.env.local` file with the following variables: + +```env +NEXTAUTH_URL=http://localhost:3000 +NEXTAUTH_SECRET=your-secret-key-here # Generate with: openssl rand -base64 32 +GOOGLE_CLIENT_ID=your-google-client-id +GOOGLE_CLIENT_SECRET=your-google-client-secret +DATABASE_URL=file:./dev.db +``` + +## Step 5: Update Database Schema + +Run the following command to update your database schema: + +```bash +npx prisma db push +``` + +## Step 6: Restart Your Development Server + +```bash +npm run dev +``` + +## Testing Google OAuth + +1. Visit the login page at `http://localhost:3000/login` +2. Click the "Sign in with Google" button +3. You should be redirected to Google's sign-in page +4. After signing in, you'll be redirected back to your app + +## Troubleshooting + +- If you get a "redirect_uri_mismatch" error, double-check the Authorized Redirect URIs in your Google Cloud Console +- Make sure the Google+ API is enabled in your GCP project +- Check your browser's console and network tab for any errors +- Ensure your `.env.local` file has the correct values and is in the root of your project + +## Production Deployment + +When deploying to production: +1. Update the `NEXTAUTH_URL` to your production URL +2. Add your production domain to the Authorized JavaScript origins and redirect URIs in the Google Cloud Console +3. Ensure your production environment has the same environment variables set +4. Consider using a more secure database than SQLite for production diff --git a/scripts/setup-env.js b/scripts/setup-env.js new file mode 100755 index 00000000..077aff6a --- /dev/null +++ b/scripts/setup-env.js @@ -0,0 +1,83 @@ +const fs = require('fs'); +const path = require('path'); +const readline = require('readline').createInterface({ + input: process.stdin, + output: process.stdout +}); + +// Check if .env.local exists, if not create it +const envPath = path.join(__dirname, '../.env.local'); + +const questions = [ + { + name: 'NEXTAUTH_SECRET', + message: 'Enter a secure random string for NEXTAUTH_SECRET (you can generate one with `openssl rand -base64 32`): ', + validate: input => input.length >= 32 || 'Secret must be at least 32 characters long' + }, + { + name: 'NEXTAUTH_URL', + message: 'Enter your NEXTAUTH_URL (e.g., http://localhost:3000): ', + default: 'http://localhost:3000', + validate: input => input.startsWith('http') || 'Must be a valid URL starting with http:// or https://' + }, + { + name: 'GOOGLE_CLIENT_ID', + message: 'Enter your Google OAuth Client ID: ', + validate: input => !!input || 'Google Client ID is required' + }, + { + name: 'GOOGLE_CLIENT_SECRET', + message: 'Enter your Google OAuth Client Secret: ', + validate: input => !!input || 'Google Client Secret is required' + } +]; + +async function setupEnv() { + console.log('Setting up your environment variables...\n'); + + let envVars = []; + + for (const q of questions) { + const answer = await new Promise((resolve) => { + const ask = () => { + readline.question(q.message, (input) => { + const value = input.trim() || q.default || ''; + if (q.validate) { + const validation = q.validate(value); + if (validation !== true) { + console.log(validation); + return ask(); + } + } + resolve(value); + }); + }; + ask(); + }); + + envVars.push(`${q.name}=${answer}`); + } + + // Add any additional environment variables + envVars.push('DATABASE_URL=file:./dev.db'); + + // Write to .env.local + fs.writeFileSync(envPath, envVars.join('\n') + '\n'); + + console.log('\n✅ Environment variables have been saved to .env.local'); + console.log('\nNext steps:'); + console.log('1. Run `npx prisma db push` to update your database schema'); + console.log('2. Restart your development server with `npm run dev`\n'); + + readline.close(); +} + +// Create .env.local if it doesn't exist +if (!fs.existsSync(envPath)) { + setupEnv(); +} else { + console.log('.env.local already exists. Please update it with the following variables:'); + console.log(questions.map(q => `${q.name}=`).join('\n')); + console.log('\nYou can also delete .env.local and run this script again to create it interactively.'); + readline.close(); +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 37838603..f634e4c8 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -24,7 +24,7 @@ export default function DashboardPage() { } if (status === 'unauthenticated') { - router.push('/login'); + // router.push('/login'); return null; } diff --git a/src/auth.ts b/src/auth.ts index c04b7c06..658e201a 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,5 +1,7 @@ import type { NextAuthConfig } from 'next-auth'; import Credentials from 'next-auth/providers/credentials'; +import Google from 'next-auth/providers/google'; +import { PrismaAdapter } from '@auth/prisma-adapter'; import { prisma } from './lib/prisma'; import { compare } from 'bcryptjs'; @@ -37,7 +39,21 @@ export const authConfig = { return session; }, }, + adapter: PrismaAdapter(prisma), providers: [ + Google({ + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + allowDangerousEmailAccountLinking: true, + profile(profile) { + return { + id: profile.sub, + name: profile.name, + email: profile.email, + image: profile.picture, + }; + }, + }), Credentials({ name: 'Credentials', credentials: { @@ -80,4 +96,16 @@ export const authConfig = { }, secret: process.env.NEXTAUTH_SECRET, trustHost: true, + debug: process.env.NODE_ENV === 'development', + cookies: { + sessionToken: { + name: `__Secure-next-auth.session-token`, + options: { + httpOnly: true, + sameSite: 'lax', + path: '/', + secure: process.env.NODE_ENV === 'production', + }, + }, + }, } satisfies NextAuthConfig; diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 6e72cfdb..6cb2c902 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -5,7 +5,7 @@ declare global { var prisma: PrismaClient | undefined; } -const globalForPrisma = global as unknown as { prisma: PrismaClient }; +const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }; export const prisma = globalForPrisma.prisma || new PrismaClient();