testing hehehe
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 51m31s
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 51m31s
This commit is contained in:
49
README.md
49
README.md
@@ -86,3 +86,52 @@ sudo nginx -t
|
|||||||
sudo systemctl reload nginx
|
sudo systemctl reload nginx
|
||||||
|
|
||||||
actually the proper change was to do this in the /etc/nginx/sites-available/flyer-crawler.projectium.com file
|
actually the proper change was to do this in the /etc/nginx/sites-available/flyer-crawler.projectium.com file
|
||||||
|
|
||||||
|
## for OAuth
|
||||||
|
|
||||||
|
1. Get Google OAuth Credentials
|
||||||
|
This is a crucial step that you must do outside the codebase:
|
||||||
|
|
||||||
|
Go to the Google Cloud Console.
|
||||||
|
|
||||||
|
Create a new project (or select an existing one).
|
||||||
|
|
||||||
|
In the navigation menu, go to APIs & Services > Credentials.
|
||||||
|
|
||||||
|
Click Create Credentials > OAuth client ID.
|
||||||
|
|
||||||
|
Select Web application as the application type.
|
||||||
|
|
||||||
|
Under Authorized redirect URIs, click ADD URI and enter the URL where Google will redirect users back to your server. For local development, this will be: http://localhost:3001/api/auth/google/callback.
|
||||||
|
|
||||||
|
Click Create. You will be given a Client ID and a Client Secret.
|
||||||
|
|
||||||
|
Add these credentials to your .env file at the project root:
|
||||||
|
|
||||||
|
plaintext
|
||||||
|
GOOGLE_CLIENT_ID="your-client-id-from-google"
|
||||||
|
GOOGLE_CLIENT_SECRET="your-client-secret-from-google"
|
||||||
|
|
||||||
|
2. Get GitHub OAuth Credentials
|
||||||
|
You'll need to obtain a Client ID and Client Secret from GitHub:
|
||||||
|
|
||||||
|
Go to your GitHub profile settings.
|
||||||
|
|
||||||
|
Navigate to Developer settings > OAuth Apps.
|
||||||
|
|
||||||
|
Click New OAuth App.
|
||||||
|
|
||||||
|
Fill in the required fields:
|
||||||
|
|
||||||
|
Application name: A descriptive name for your app (e.g., "Flyer Crawler").
|
||||||
|
Homepage URL: The base URL of your application (e.g., http://localhost:5173 for local development).
|
||||||
|
Authorization callback URL: This is where GitHub will redirect users after they authorize your app. For local development, this will be: http://localhost:3001/api/auth/github/callback.
|
||||||
|
Click Register application.
|
||||||
|
|
||||||
|
You will be given a Client ID and a Client Secret.
|
||||||
|
|
||||||
|
Add these credentials to your .env file at the project root:
|
||||||
|
|
||||||
|
plaintext
|
||||||
|
GITHUB_CLIENT_ID="your-github-client-id"
|
||||||
|
GITHUB_CLIENT_SECRET="your-github-client-secret"
|
||||||
114
package-lock.json
generated
114
package-lock.json
generated
@@ -17,6 +17,8 @@
|
|||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"nodemailer": "^7.0.10",
|
"nodemailer": "^7.0.10",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
|
"passport-github2": "^0.1.12",
|
||||||
|
"passport-google-oauth20": "^2.0.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"pdfjs-dist": "^5.4.394",
|
"pdfjs-dist": "^5.4.394",
|
||||||
@@ -41,6 +43,8 @@
|
|||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/nodemailer": "^7.0.4",
|
"@types/nodemailer": "^7.0.4",
|
||||||
"@types/passport": "^1.0.17",
|
"@types/passport": "^1.0.17",
|
||||||
|
"@types/passport-github2": "^1.2.9",
|
||||||
|
"@types/passport-google-oauth20": "^2.0.17",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/passport-local": "^1.0.38",
|
"@types/passport-local": "^1.0.38",
|
||||||
"@types/pg": "^8.15.6",
|
"@types/pg": "^8.15.6",
|
||||||
@@ -4199,6 +4203,16 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/oauth": {
|
||||||
|
"version": "0.9.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.6.tgz",
|
||||||
|
"integrity": "sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/passport": {
|
"node_modules/@types/passport": {
|
||||||
"version": "1.0.17",
|
"version": "1.0.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
|
||||||
@@ -4209,6 +4223,30 @@
|
|||||||
"@types/express": "*"
|
"@types/express": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/passport-github2": {
|
||||||
|
"version": "1.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/passport-github2/-/passport-github2-1.2.9.tgz",
|
||||||
|
"integrity": "sha512-/nMfiPK2E6GKttwBzwj0Wjaot8eHrM57hnWxu52o6becr5/kXlH/4yE2v2rh234WGvSgEEzIII02Nc5oC5xEHA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*",
|
||||||
|
"@types/passport": "*",
|
||||||
|
"@types/passport-oauth2": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/passport-google-oauth20": {
|
||||||
|
"version": "2.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.17.tgz",
|
||||||
|
"integrity": "sha512-MHNOd2l7gOTCn3iS+wInPQMiukliAUvMpODO3VlXxOiwNEMSyzV7UNvAdqxSN872o8OXx1SqPDVT6tLW74AtqQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*",
|
||||||
|
"@types/passport": "*",
|
||||||
|
"@types/passport-oauth2": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/passport-jwt": {
|
"node_modules/@types/passport-jwt": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz",
|
||||||
@@ -4232,6 +4270,18 @@
|
|||||||
"@types/passport-strategy": "*"
|
"@types/passport-strategy": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/passport-oauth2": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-6//z+4orIOy/g3zx17HyQ71GSRK4bs7Sb+zFasRoc2xzlv7ZCJ+vkDBYFci8U6HY+or6Zy7ajf4mz4rK7nsWJQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*",
|
||||||
|
"@types/oauth": "*",
|
||||||
|
"@types/passport": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/passport-strategy": {
|
"node_modules/@types/passport-strategy": {
|
||||||
"version": "0.2.38",
|
"version": "0.2.38",
|
||||||
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz",
|
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz",
|
||||||
@@ -5457,6 +5507,15 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64url": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.25",
|
"version": "2.8.25",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz",
|
||||||
@@ -10132,6 +10191,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/oauth": {
|
||||||
|
"version": "0.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
|
||||||
|
"integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -10386,6 +10451,29 @@
|
|||||||
"url": "https://github.com/sponsors/jaredhanson"
|
"url": "https://github.com/sponsors/jaredhanson"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/passport-github2": {
|
||||||
|
"version": "0.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz",
|
||||||
|
"integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==",
|
||||||
|
"dependencies": {
|
||||||
|
"passport-oauth2": "1.x.x"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/passport-google-oauth20": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"passport-oauth2": "1.x.x"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/passport-jwt": {
|
"node_modules/passport-jwt": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
|
||||||
@@ -10407,6 +10495,26 @@
|
|||||||
"node": ">= 0.4.0"
|
"node": ">= 0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/passport-oauth2": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64url": "3.x.x",
|
||||||
|
"oauth": "0.10.x",
|
||||||
|
"passport-strategy": "1.x.x",
|
||||||
|
"uid2": "0.0.x",
|
||||||
|
"utils-merge": "1.x.x"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jaredhanson"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/passport-strategy": {
|
"node_modules/passport-strategy": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
|
||||||
@@ -12793,6 +12901,12 @@
|
|||||||
"typescript": ">=4.8.4 <6.0.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uid2": {
|
||||||
|
"version": "0.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
|
||||||
|
"integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unbox-primitive": {
|
"node_modules/unbox-primitive": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"nodemailer": "^7.0.10",
|
"nodemailer": "^7.0.10",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
|
"passport-github2": "^0.1.12",
|
||||||
|
"passport-google-oauth20": "^2.0.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"pdfjs-dist": "^5.4.394",
|
"pdfjs-dist": "^5.4.394",
|
||||||
@@ -45,6 +47,8 @@
|
|||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/nodemailer": "^7.0.4",
|
"@types/nodemailer": "^7.0.4",
|
||||||
"@types/passport": "^1.0.17",
|
"@types/passport": "^1.0.17",
|
||||||
|
"@types/passport-github2": "^1.2.9",
|
||||||
|
"@types/passport-google-oauth20": "^2.0.17",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/passport-local": "^1.0.38",
|
"@types/passport-local": "^1.0.38",
|
||||||
"@types/pg": "^8.15.6",
|
"@types/pg": "^8.15.6",
|
||||||
|
|||||||
110
server.ts
110
server.ts
@@ -1,6 +1,8 @@
|
|||||||
import express, { Request, Response, NextFunction } from 'express';
|
import express, { Request, Response, NextFunction } from 'express';
|
||||||
import passport from 'passport';
|
import passport from 'passport';
|
||||||
import { Strategy as LocalStrategy } from 'passport-local';
|
import { Strategy as LocalStrategy } from 'passport-local';
|
||||||
|
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
|
||||||
|
import { Strategy as GitHubStrategy } from 'passport-github2';
|
||||||
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
|
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import zxcvbn from 'zxcvbn';
|
import zxcvbn from 'zxcvbn';
|
||||||
@@ -14,7 +16,7 @@ import multer from 'multer';
|
|||||||
import * as db from './src/services/db';
|
import * as db from './src/services/db';
|
||||||
import { logger } from './src/services/logger'; // This import is correct
|
import { logger } from './src/services/logger'; // This import is correct
|
||||||
import * as aiService from './src/services/aiService.server'; // Import the new server-side AI service
|
import * as aiService from './src/services/aiService.server'; // Import the new server-side AI service
|
||||||
import { sendPasswordResetEmail } from './src/services/emailService';
|
import { sendPasswordResetEmail, sendWelcomeEmail } from './src/services/emailService';
|
||||||
import { UserProfile, ShoppingListItem } from './src/types';
|
import { UserProfile, ShoppingListItem } from './src/types';
|
||||||
|
|
||||||
// Load environment variables from a .env file at the root of your project
|
// Load environment variables from a .env file at the root of your project
|
||||||
@@ -132,6 +134,112 @@ passport.use(new LocalStrategy(
|
|||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// --- Passport Google OAuth 2.0 Strategy ---
|
||||||
|
passport.use(new GoogleStrategy({
|
||||||
|
clientID: process.env.GOOGLE_CLIENT_ID!,
|
||||||
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||||
|
callbackURL: '/api/auth/google/callback', // Must match the one in Google Cloud Console
|
||||||
|
scope: ['profile', 'email']
|
||||||
|
},
|
||||||
|
async (accessToken, refreshToken, profile, done) => {
|
||||||
|
try {
|
||||||
|
const email = profile.emails?.[0]?.value;
|
||||||
|
if (!email) {
|
||||||
|
return done(new Error("No email found in Google profile."), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user already exists in our database
|
||||||
|
let user = await db.findUserByEmail(email);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
// User exists, proceed to log them in.
|
||||||
|
logger.info(`Google OAuth successful for existing user: ${email}`);
|
||||||
|
const { password_hash, ...userWithoutHash } = user;
|
||||||
|
return done(null, userWithoutHash);
|
||||||
|
} else {
|
||||||
|
// User does not exist, create a new account for them.
|
||||||
|
logger.info(`Google OAuth: creating new user for email: ${email}`);
|
||||||
|
|
||||||
|
// Since this is an OAuth user, they don't have a password.
|
||||||
|
// We pass `null` for the password hash.
|
||||||
|
const newUser = await db.createUser(email, null, {
|
||||||
|
full_name: profile.displayName,
|
||||||
|
avatar_url: profile.photos?.[0]?.value
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send a welcome email to the new user
|
||||||
|
try {
|
||||||
|
await sendWelcomeEmail(email, profile.displayName);
|
||||||
|
} catch (emailError) {
|
||||||
|
logger.error(`Failed to send welcome email to new Google user ${email}`, { error: emailError });
|
||||||
|
// Don't block the login flow if email fails.
|
||||||
|
}
|
||||||
|
|
||||||
|
// The `createUser` function returns the user object without the password hash.
|
||||||
|
return done(null, newUser);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Error during Google authentication strategy:', { error: err });
|
||||||
|
return done(err, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
// --- Passport GitHub OAuth 2.0 Strategy ---
|
||||||
|
passport.use(new GitHubStrategy({
|
||||||
|
clientID: process.env.GITHUB_CLIENT_ID!,
|
||||||
|
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
||||||
|
callbackURL: '/api/auth/github/callback', // Must match the one in GitHub OAuth App settings
|
||||||
|
scope: ['user:email'] // Request email access
|
||||||
|
},
|
||||||
|
async (accessToken, refreshToken, profile, done) => {
|
||||||
|
try {
|
||||||
|
const email = profile.emails?.[0]?.value;
|
||||||
|
if (!email) {
|
||||||
|
return done(new Error("No public email found in GitHub profile. Please ensure your primary email is public or add one."), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user already exists in our database
|
||||||
|
let user = await db.findUserByEmail(email);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
// User exists, proceed to log them in.
|
||||||
|
logger.info(`GitHub OAuth successful for existing user: ${email}`);
|
||||||
|
const { password_hash, ...userWithoutHash } = user;
|
||||||
|
return done(null, userWithoutHash);
|
||||||
|
} else {
|
||||||
|
// User does not exist, create a new account for them.
|
||||||
|
logger.info(`GitHub OAuth: creating new user for email: ${email}`);
|
||||||
|
|
||||||
|
// Since this is an OAuth user, they don't have a password.
|
||||||
|
// We pass `null` for the password hash.
|
||||||
|
const newUser = await db.createUser(email, null, {
|
||||||
|
full_name: profile.displayName || profile.username, // GitHub profile might not have displayName
|
||||||
|
avatar_url: profile.photos?.[0]?.value
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send a welcome email to the new user
|
||||||
|
try {
|
||||||
|
await sendWelcomeEmail(email, profile.displayName || profile.username);
|
||||||
|
} catch (emailError) {
|
||||||
|
logger.error(`Failed to send welcome email to new GitHub user ${email}`, { error: emailError });
|
||||||
|
// Don't block the login flow if email fails.
|
||||||
|
}
|
||||||
|
|
||||||
|
// The `createUser` function returns the user object without the password hash.
|
||||||
|
return done(null, newUser);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Error during GitHub authentication strategy:', { error: err });
|
||||||
|
return done(err, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
// Serialize and deserialize user are not needed for JWT-based sessions
|
||||||
|
// passport.serializeUser((user, done) => done(null, user));
|
||||||
|
// passport.deserializeUser((user, done) => done(null, user as any));
|
||||||
|
|
||||||
// --- Passport JWT Strategy (for protecting API routes) ---
|
// --- Passport JWT Strategy (for protecting API routes) ---
|
||||||
const jwtOptions = {
|
const jwtOptions = {
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // Expect JWT in 'Authorization: Bearer <token>' header
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // Expect JWT in 'Authorization: Bearer <token>' header
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- For generating UUIDs
|
|||||||
CREATE TABLE IF NOT EXISTS public.users (
|
CREATE TABLE IF NOT EXISTS public.users (
|
||||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
email TEXT NOT NULL UNIQUE,
|
email TEXT NOT NULL UNIQUE,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT,
|
||||||
refresh_token TEXT,
|
refresh_token TEXT,
|
||||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL
|
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL
|
||||||
|
|||||||
40
src/App.tsx
40
src/App.tsx
@@ -248,6 +248,46 @@ function App() {
|
|||||||
checkAuthToken();
|
checkAuthToken();
|
||||||
}, [fetchWatchedItems, fetchShoppingLists]); // Add callbacks to dependency array.
|
}, [fetchWatchedItems, fetchShoppingLists]); // Add callbacks to dependency array.
|
||||||
|
|
||||||
|
// Effect to handle the token from Google OAuth redirect
|
||||||
|
useEffect(() => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const googleToken = urlParams.get('googleAuthToken');
|
||||||
|
|
||||||
|
if (googleToken) {
|
||||||
|
logger.info('Received Google Auth token from URL. Authenticating...');
|
||||||
|
// The token is already a valid access token from our server.
|
||||||
|
// We can use it to fetch the user profile and complete the login flow.
|
||||||
|
localStorage.setItem('authToken', googleToken);
|
||||||
|
getAuthenticatedUserProfile()
|
||||||
|
.then(userProfile => {
|
||||||
|
handleLoginSuccess(userProfile.user, googleToken);
|
||||||
|
})
|
||||||
|
.catch(err => logger.error('Failed to log in with Google token', { error: err }));
|
||||||
|
|
||||||
|
// Clean the token from the URL
|
||||||
|
window.history.replaceState({}, document.title, "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
const githubToken = urlParams.get('githubAuthToken');
|
||||||
|
if (githubToken) {
|
||||||
|
logger.info('Received GitHub Auth token from URL. Authenticating...');
|
||||||
|
// The token is already a valid access token from our server.
|
||||||
|
// We can use it to fetch the user profile and complete the login flow.
|
||||||
|
localStorage.setItem('authToken', githubToken);
|
||||||
|
getAuthenticatedUserProfile()
|
||||||
|
.then(userProfile => {
|
||||||
|
handleLoginSuccess(userProfile.user, githubToken);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
logger.error('Failed to log in with GitHub token', { error: err });
|
||||||
|
// Optionally, redirect to a page with an error message
|
||||||
|
// navigate('/login?error=github_auth_failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean the token from the URL
|
||||||
|
window.history.replaceState({}, document.title, "/");
|
||||||
|
}
|
||||||
|
}, [handleLoginSuccess]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isReady) {
|
if (isReady) {
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import { notifySuccess, notifyError } from '../services/notificationService';
|
|||||||
import { logger } from '../services/logger';
|
import { logger } from '../services/logger';
|
||||||
import { LoadingSpinner } from './LoadingSpinner';
|
import { LoadingSpinner } from './LoadingSpinner';
|
||||||
import { XMarkIcon } from './icons/XMarkIcon';
|
import { XMarkIcon } from './icons/XMarkIcon';
|
||||||
|
import { GoogleIcon } from './icons/GoogleIcon';
|
||||||
|
import { GithubIcon } from './icons/GithubIcon';
|
||||||
import { ConfirmationModal } from './ConfirmationModal';
|
import { ConfirmationModal } from './ConfirmationModal';
|
||||||
import { User } from '../types'; // Import User type for props
|
import { User } from '../types';
|
||||||
import { PasswordInput } from './PasswordInput';
|
import { PasswordInput } from './PasswordInput';
|
||||||
|
|
||||||
type AuthStatus = 'SIGNED_OUT' | 'ANONYMOUS' | 'AUTHENTICATED';
|
type AuthStatus = 'SIGNED_OUT' | 'ANONYMOUS' | 'AUTHENTICATED';
|
||||||
@@ -265,19 +267,16 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOAuthSignIn = async (provider: 'google' | 'github') => {
|
const handleOAuthSignIn = (provider: 'google' | 'github') => {
|
||||||
|
// Redirect to the backend OAuth initiation route
|
||||||
|
// The backend will then handle the redirect to the OAuth provider
|
||||||
|
// and eventually redirect back to the frontend with a token.
|
||||||
|
window.location.href = `/api/auth/${provider}`;
|
||||||
|
// Set authLoading to true to show a spinner while redirecting
|
||||||
setAuthLoading(true);
|
setAuthLoading(true);
|
||||||
const message = `Sign in with ${provider} is not yet implemented.`;
|
|
||||||
notifyError(message);
|
|
||||||
logger.warn(`OAuth sign-in attempt with ${provider} failed: not implemented.`);
|
|
||||||
|
|
||||||
// We set a timeout to turn off the loading indicator and clear the error
|
|
||||||
// so the user can try another method.
|
|
||||||
setTimeout(() => {
|
|
||||||
setAuthLoading(false);
|
|
||||||
}, 3000); // This was a closing parenthesis for the setTimeout
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -383,6 +382,24 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
|||||||
{isRegistering ? 'Already have an account? Sign In' : "Don't have an account? Register"}
|
{isRegistering ? 'Already have an account? Sign In' : "Don't have an account? Register"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="relative my-6">
|
||||||
|
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||||
|
<div className="w-full border-t border-gray-300 dark:border-gray-600" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center">
|
||||||
|
<span className="bg-white dark:bg-gray-800 px-2 text-sm text-gray-500 dark:text-gray-400">Or continue with</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<button type="button" onClick={() => handleOAuthSignIn('google')} disabled={authLoading} className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
{authLoading ? <div className="w-5 h-5 mr-3"><LoadingSpinner /></div> : <GoogleIcon className="w-5 h-5 mr-3" />}
|
||||||
|
Sign In with Google
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => handleOAuthSignIn('github')} disabled={authLoading} className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
{authLoading ? <div className="w-5 h-5 mr-3"><LoadingSpinner /></div> : <GithubIcon className="w-5 h-5 mr-3" />}
|
||||||
|
Sign In with GitHub
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
@@ -458,6 +475,20 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||||
|
<h3 className="text-md font-semibold text-gray-800 dark:text-white">Link Social Accounts</h3>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1 mb-3">Connect other accounts for easier sign-in.</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<button type="button" onClick={() => handleOAuthLink('google')} className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
|
<GoogleIcon className="w-5 h-5 mr-3" />
|
||||||
|
Link Google Account
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => handleOAuthLink('github')} className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
|
<GithubIcon className="w-5 h-5 mr-3" />
|
||||||
|
Link GitHub Account
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { generateSpeechFromText, startVoiceSession } from '../services/aiApiClie
|
|||||||
import { logger } from '../services/logger';
|
import { logger } from '../services/logger';
|
||||||
import { notifyError } from '../services/notificationService';
|
import { notifyError } from '../services/notificationService';
|
||||||
import { LoadingSpinner } from '../components/LoadingSpinner';
|
import { LoadingSpinner } from '../components/LoadingSpinner';
|
||||||
import { SpeakerWaveIcon, MicrophoneIcon } from '../components/icons/HeroIcons';
|
import { SpeakerWaveIcon } from '../components/icons/SpeakerWaveIcon';
|
||||||
|
import { MicrophoneIcon } from '../components/icons/MicrophoneIcon';
|
||||||
|
|
||||||
export const VoiceLabPage: React.FC = () => {
|
export const VoiceLabPage: React.FC = () => {
|
||||||
const [textToSpeak, setTextToSpeak] = useState('Hello! This is a test of the text-to-speech generation.');
|
const [textToSpeak, setTextToSpeak] = useState('Hello! This is a test of the text-to-speech generation.');
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export async function findUserByEmail(email: string): Promise<DbUser | undefined
|
|||||||
*/
|
*/
|
||||||
export async function createUser(
|
export async function createUser(
|
||||||
email: string,
|
email: string,
|
||||||
passwordHash: string,
|
passwordHash: string | null,
|
||||||
profileData: { full_name?: string; avatar_url?: string }
|
profileData: { full_name?: string; avatar_url?: string }
|
||||||
): Promise<{ id: string; email: string }> {
|
): Promise<{ id: string; email: string }> {
|
||||||
// Use a client from the pool to run multiple queries in a transaction
|
// Use a client from the pool to run multiple queries in a transaction
|
||||||
|
|||||||
Reference in New Issue
Block a user