prod broken
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 1m40s

This commit is contained in:
2025-11-22 19:21:49 -08:00
parent 1d00ab2e5d
commit 6d815eca7d
3 changed files with 119 additions and 117 deletions

View File

@@ -236,30 +236,30 @@ router.post('/refresh-token', async (req: Request, res: Response) => {
// --- OAuth Routes --- // --- OAuth Routes ---
const handleOAuthCallback = (req: Request, res: Response) => { // const handleOAuthCallback = (req: Request, res: Response) => {
const user = req.user as { id: string; email: string }; // const user = req.user as { id: string; email: string };
const payload = { id: user.id, email: user.email }; // const payload = { id: user.id, email: user.email };
const accessToken = jwt.sign(payload, JWT_SECRET, { expiresIn: '15m' }); // const accessToken = jwt.sign(payload, JWT_SECRET, { expiresIn: '15m' });
const refreshToken = crypto.randomBytes(64).toString('hex'); // const refreshToken = crypto.randomBytes(64).toString('hex');
db.saveRefreshToken(user.id, refreshToken).then(() => { // db.saveRefreshToken(user.id, refreshToken).then(() => {
res.cookie('refreshToken', refreshToken, { // res.cookie('refreshToken', refreshToken, {
httpOnly: true, // httpOnly: true,
secure: process.env.NODE_ENV === 'production', // secure: process.env.NODE_ENV === 'production',
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days // maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
}); // });
// Redirect to a frontend page that can handle the token // // Redirect to a frontend page that can handle the token
res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${accessToken}`); // res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${accessToken}`);
}).catch(err => { // }).catch(err => {
logger.error('Failed to save refresh token during OAuth callback:', { error: err }); // logger.error('Failed to save refresh token during OAuth callback:', { error: err });
res.redirect(`${process.env.FRONTEND_URL}/login?error=auth_failed`); // res.redirect(`${process.env.FRONTEND_URL}/login?error=auth_failed`);
}); // });
}; // };
router.get('/google', passport.authenticate('google', { session: false })); // router.get('/google', passport.authenticate('google', { session: false }));
router.get('/google/callback', passport.authenticate('google', { session: false, failureRedirect: '/login' }), handleOAuthCallback); // router.get('/google/callback', passport.authenticate('google', { session: false, failureRedirect: '/login' }), handleOAuthCallback);
router.get('/github', passport.authenticate('github', { session: false })); // router.get('/github', passport.authenticate('github', { session: false }));
router.get('/github/callback', passport.authenticate('github', { session: false, failureRedirect: '/login' }), handleOAuthCallback); // router.get('/github/callback', passport.authenticate('github', { session: false, failureRedirect: '/login' }), handleOAuthCallback);
export default router; export default router;

View File

@@ -1,7 +1,7 @@
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 GoogleStrategy } from 'passport-google-oauth20';
import { Strategy as GitHubStrategy } from 'passport-github2'; //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 { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
@@ -89,110 +89,110 @@ passport.use(new LocalStrategy(
)); ));
// --- Passport Google OAuth 2.0 Strategy --- // --- Passport Google OAuth 2.0 Strategy ---
passport.use(new GoogleStrategy({ // passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID!, // clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!, // clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
callbackURL: '/api/auth/google/callback', // Must match the one in Google Cloud Console // callbackURL: '/api/auth/google/callback', // Must match the one in Google Cloud Console
scope: ['profile', 'email'] // scope: ['profile', 'email']
}, // },
async (accessToken, refreshToken, profile, done) => { // async (accessToken, refreshToken, profile, done) => {
try { // try {
const email = profile.emails?.[0]?.value; // const email = profile.emails?.[0]?.value;
if (!email) { // if (!email) {
return done(new Error("No email found in Google profile."), false); // return done(new Error("No email found in Google profile."), false);
} // }
// Check if user already exists in our database // // Check if user already exists in our database
const user = await db.findUserByEmail(email); // Changed to const as 'user' is not reassigned // const user = await db.findUserByEmail(email); // Changed to const as 'user' is not reassigned
if (user) { // if (user) {
// User exists, proceed to log them in. // // User exists, proceed to log them in.
logger.info(`Google OAuth successful for existing user: ${email}`); // logger.info(`Google OAuth successful for existing user: ${email}`);
// The password_hash is intentionally destructured and discarded for security. // // The password_hash is intentionally destructured and discarded for security.
// eslint-disable-next-line @typescript-eslint/no-unused-vars // // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password_hash, ...userWithoutHash } = user; // const { password_hash, ...userWithoutHash } = user;
return done(null, userWithoutHash); // return done(null, userWithoutHash);
} else { // } else {
// User does not exist, create a new account for them. // // User does not exist, create a new account for them.
logger.info(`Google OAuth: creating new user for email: ${email}`); // logger.info(`Google OAuth: creating new user for email: ${email}`);
// Since this is an OAuth user, they don't have a password. // // Since this is an OAuth user, they don't have a password.
// We pass `null` for the password hash. // // We pass `null` for the password hash.
const newUser = await db.createUser(email, null, { // const newUser = await db.createUser(email, null, {
full_name: profile.displayName, // full_name: profile.displayName,
avatar_url: profile.photos?.[0]?.value // avatar_url: profile.photos?.[0]?.value
}); // });
// Send a welcome email to the new user // // Send a welcome email to the new user
try { // try {
await sendWelcomeEmail(email, profile.displayName); // await sendWelcomeEmail(email, profile.displayName);
} catch (emailError) { // } catch (emailError) {
logger.error(`Failed to send welcome email to new Google user ${email}`, { error: emailError }); // logger.error(`Failed to send welcome email to new Google user ${email}`, { error: emailError });
// Don't block the login flow if email fails. // // Don't block the login flow if email fails.
} // }
// The `createUser` function returns the user object without the password hash. // // The `createUser` function returns the user object without the password hash.
return done(null, newUser); // return done(null, newUser);
} // }
} catch (err) { // } catch (err) {
logger.error('Error during Google authentication strategy:', { error: err }); // logger.error('Error during Google authentication strategy:', { error: err });
return done(err, false); // return done(err, false);
} // }
} // }
)); // ));
// --- Passport GitHub OAuth 2.0 Strategy --- // --- Passport GitHub OAuth 2.0 Strategy ---
passport.use(new GitHubStrategy({ // passport.use(new GitHubStrategy({
clientID: process.env.GITHUB_CLIENT_ID!, // clientID: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!, // clientSecret: process.env.GITHUB_CLIENT_SECRET!,
callbackURL: '/api/auth/github/callback', // Must match the one in GitHub OAuth App settings // callbackURL: '/api/auth/github/callback', // Must match the one in GitHub OAuth App settings
scope: ['user:email'] // Request email access // scope: ['user:email'] // Request email access
}, // },
async (accessToken, refreshToken, profile, done) => { // async (accessToken, refreshToken, profile, done) => {
try { // try {
const email = profile.emails?.[0]?.value; // const email = profile.emails?.[0]?.value;
if (!email) { // if (!email) {
return done(new Error("No public email found in GitHub profile. Please ensure your primary email is public or add one."), false); // 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 // // Check if user already exists in our database
const user = await db.findUserByEmail(email); // Changed to const as 'user' is not reassigned // const user = await db.findUserByEmail(email); // Changed to const as 'user' is not reassigned
if (user) { // if (user) {
// User exists, proceed to log them in. // // User exists, proceed to log them in.
logger.info(`GitHub OAuth successful for existing user: ${email}`); // logger.info(`GitHub OAuth successful for existing user: ${email}`);
// The password_hash is intentionally destructured and discarded for security. // // The password_hash is intentionally destructured and discarded for security.
// eslint-disable-next-line @typescript-eslint/no-unused-vars // // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password_hash, ...userWithoutHash } = user; // const { password_hash, ...userWithoutHash } = user;
return done(null, userWithoutHash); // return done(null, userWithoutHash);
} else { // } else {
// User does not exist, create a new account for them. // // User does not exist, create a new account for them.
logger.info(`GitHub OAuth: creating new user for email: ${email}`); // logger.info(`GitHub OAuth: creating new user for email: ${email}`);
// Since this is an OAuth user, they don't have a password. // // Since this is an OAuth user, they don't have a password.
// We pass `null` for the password hash. // // We pass `null` for the password hash.
const newUser = await db.createUser(email, null, { // const newUser = await db.createUser(email, null, {
full_name: profile.displayName || profile.username, // GitHub profile might not have displayName // full_name: profile.displayName || profile.username, // GitHub profile might not have displayName
avatar_url: profile.photos?.[0]?.value // avatar_url: profile.photos?.[0]?.value
}); // });
// Send a welcome email to the new user // // Send a welcome email to the new user
try { // try {
await sendWelcomeEmail(email, profile.displayName || profile.username); // await sendWelcomeEmail(email, profile.displayName || profile.username);
} catch (emailError) { // } catch (emailError) {
logger.error(`Failed to send welcome email to new GitHub user ${email}`, { error: emailError }); // logger.error(`Failed to send welcome email to new GitHub user ${email}`, { error: emailError });
// Don't block the login flow if email fails. // // Don't block the login flow if email fails.
} // }
// The `createUser` function returns the user object without the password hash. // // The `createUser` function returns the user object without the password hash.
return done(null, newUser); // return done(null, newUser);
} // }
} catch (err) { // } catch (err) {
logger.error('Error during GitHub authentication strategy:', { error: err }); // logger.error('Error during GitHub authentication strategy:', { error: err });
return done(err, false); // return done(err, false);
} // }
} // }
)); // ));
// --- Passport JWT Strategy (for protecting API routes) --- // --- Passport JWT Strategy (for protecting API routes) ---
const jwtOptions = { const jwtOptions = {

View File

@@ -12,6 +12,8 @@ interface DbUser {
email: string; email: string;
password_hash: string; password_hash: string;
refresh_token?: string | null; refresh_token?: string | null;
failed_login_attempts: number;
last_failed_login: string | null; // This will be a date string from the DB
} }
/** /**
@@ -22,7 +24,7 @@ interface DbUser {
export async function findUserByEmail(email: string): Promise<DbUser | undefined> { export async function findUserByEmail(email: string): Promise<DbUser | undefined> {
try { try {
const res = await getPool().query<DbUser>( const res = await getPool().query<DbUser>(
'SELECT id, email, password_hash, refresh_token FROM public.users WHERE email = $1', 'SELECT id, email, password_hash, refresh_token, failed_login_attempts, last_failed_login FROM public.users WHERE email = $1',
[email] [email]
); );
return res.rows[0]; return res.rows[0];