google + github oauth
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 15m39s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 15m39s
This commit is contained in:
@@ -41,6 +41,14 @@ FRONTEND_URL=http://localhost:3000
|
|||||||
# REQUIRED: Secret key for signing JWT tokens (generate a random 64+ character string)
|
# REQUIRED: Secret key for signing JWT tokens (generate a random 64+ character string)
|
||||||
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
|
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
|
||||||
|
|
||||||
|
# OAuth Providers (Optional - enable social login)
|
||||||
|
# Google OAuth - https://console.cloud.google.com/apis/credentials
|
||||||
|
GOOGLE_CLIENT_ID=
|
||||||
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
# GitHub OAuth - https://github.com/settings/developers
|
||||||
|
GITHUB_CLIENT_ID=
|
||||||
|
GITHUB_CLIENT_SECRET=
|
||||||
|
|
||||||
# ===================
|
# ===================
|
||||||
# AI/ML Services
|
# AI/ML Services
|
||||||
# ===================
|
# ===================
|
||||||
|
|||||||
@@ -130,6 +130,11 @@ jobs:
|
|||||||
SMTP_USER: ''
|
SMTP_USER: ''
|
||||||
SMTP_PASS: ''
|
SMTP_PASS: ''
|
||||||
SMTP_FROM_EMAIL: 'noreply@flyer-crawler.projectium.com'
|
SMTP_FROM_EMAIL: 'noreply@flyer-crawler.projectium.com'
|
||||||
|
# OAuth Providers
|
||||||
|
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||||
|
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||||
|
GITHUB_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }}
|
||||||
|
GITHUB_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }}
|
||||||
run: |
|
run: |
|
||||||
if [ -z "$DB_HOST" ] || [ -z "$DB_USER" ] || [ -z "$DB_PASSWORD" ] || [ -z "$DB_NAME" ]; then
|
if [ -z "$DB_HOST" ] || [ -z "$DB_USER" ] || [ -z "$DB_PASSWORD" ] || [ -z "$DB_NAME" ]; then
|
||||||
echo "ERROR: One or more production database secrets (DB_HOST, DB_USER, DB_PASSWORD, DB_DATABASE_PROD) are not set."
|
echo "ERROR: One or more production database secrets (DB_HOST, DB_USER, DB_PASSWORD, DB_DATABASE_PROD) are not set."
|
||||||
|
|||||||
11
CLAUDE.md
11
CLAUDE.md
@@ -1,5 +1,16 @@
|
|||||||
# Claude Code Project Instructions
|
# Claude Code Project Instructions
|
||||||
|
|
||||||
|
## Communication Style: Ask Before Assuming
|
||||||
|
|
||||||
|
**IMPORTANT**: When helping with tasks, **ask clarifying questions before making assumptions**. Do not assume:
|
||||||
|
|
||||||
|
- What steps the user has or hasn't completed
|
||||||
|
- What the user already knows or has configured
|
||||||
|
- What external services (OAuth providers, APIs, etc.) are already set up
|
||||||
|
- What secrets or credentials have already been created
|
||||||
|
|
||||||
|
Instead, ask the user to confirm the current state before providing instructions or making recommendations. This prevents wasted effort and respects the user's existing work.
|
||||||
|
|
||||||
## Platform Requirement: Linux Only
|
## Platform Requirement: Linux Only
|
||||||
|
|
||||||
**CRITICAL**: This application is designed to run **exclusively on Linux**. See [ADR-014](docs/adr/0014-containerization-and-deployment-strategy.md) for full details.
|
**CRITICAL**: This application is designed to run **exclusively on Linux**. See [ADR-014](docs/adr/0014-containerization-and-deployment-strategy.md) for full details.
|
||||||
|
|||||||
@@ -31,17 +31,17 @@ We will implement a stateless JWT-based authentication system with the following
|
|||||||
|
|
||||||
## Current Implementation Status
|
## Current Implementation Status
|
||||||
|
|
||||||
| Component | Status | Notes |
|
| Component | Status | Notes |
|
||||||
| ------------------------ | --------------- | ------------------------------------------------ |
|
| ------------------------ | ------- | ----------------------------------------------------------- |
|
||||||
| **Local Authentication** | Enabled | Email/password with bcrypt (salt rounds = 10) |
|
| **Local Authentication** | Enabled | Email/password with bcrypt (salt rounds = 10) |
|
||||||
| **JWT Access Tokens** | Enabled | 15-minute expiry, `Authorization: Bearer` header |
|
| **JWT Access Tokens** | Enabled | 15-minute expiry, `Authorization: Bearer` header |
|
||||||
| **Refresh Tokens** | Enabled | 7-day expiry, HTTP-only cookie |
|
| **Refresh Tokens** | Enabled | 7-day expiry, HTTP-only cookie |
|
||||||
| **Account Lockout** | Enabled | 5 failed attempts, 15-minute lockout |
|
| **Account Lockout** | Enabled | 5 failed attempts, 15-minute lockout |
|
||||||
| **Password Reset** | Enabled | Email-based token flow |
|
| **Password Reset** | Enabled | Email-based token flow |
|
||||||
| **Google OAuth** | Disabled | Code present, commented out |
|
| **Google OAuth** | Enabled | Requires GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env vars |
|
||||||
| **GitHub OAuth** | Disabled | Code present, commented out |
|
| **GitHub OAuth** | Enabled | Requires GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET env vars |
|
||||||
| **OAuth Routes** | Disabled | Endpoints commented out |
|
| **OAuth Routes** | Enabled | `/api/auth/google`, `/api/auth/github` + callbacks |
|
||||||
| **OAuth Frontend UI** | Not Implemented | No login buttons exist |
|
| **OAuth Frontend UI** | Enabled | Login buttons in AuthView.tsx |
|
||||||
|
|
||||||
## Implementation Details
|
## Implementation Details
|
||||||
|
|
||||||
|
|||||||
@@ -182,8 +182,8 @@ describe('createUploadMiddleware', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate a predictable filename in test environment', () => {
|
it('should generate a unique filename in test environment', () => {
|
||||||
// This test covers lines 43-46
|
// This test covers the default case in getStorageConfig
|
||||||
vi.stubEnv('NODE_ENV', 'test');
|
vi.stubEnv('NODE_ENV', 'test');
|
||||||
const mockFlyerFile = {
|
const mockFlyerFile = {
|
||||||
fieldname: 'flyerFile',
|
fieldname: 'flyerFile',
|
||||||
@@ -196,7 +196,10 @@ describe('createUploadMiddleware', () => {
|
|||||||
|
|
||||||
storageOptions.filename!(mockReq, mockFlyerFile, cb);
|
storageOptions.filename!(mockReq, mockFlyerFile, cb);
|
||||||
|
|
||||||
expect(cb).toHaveBeenCalledWith(null, 'flyerFile-test-flyer-image.jpg');
|
expect(cb).toHaveBeenCalledWith(
|
||||||
|
null,
|
||||||
|
expect.stringMatching(/^flyerFile-\d+-\d+-test-flyer\.jpg$/),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -266,4 +269,4 @@ describe('handleMulterError Middleware', () => {
|
|||||||
expect(mockNext).toHaveBeenCalledWith(err);
|
expect(mockNext).toHaveBeenCalledWith(err);
|
||||||
expect(mockResponse.status).not.toHaveBeenCalled();
|
expect(mockResponse.status).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -461,9 +461,9 @@ describe('AI Routes (/api/ai)', () => {
|
|||||||
expect(mockedDb.createFlyerAndItems).not.toHaveBeenCalled(); // Should not be called if service throws
|
expect(mockedDb.createFlyerAndItems).not.toHaveBeenCalled(); // Should not be called if service throws
|
||||||
// Assert that the file was deleted
|
// Assert that the file was deleted
|
||||||
expect(unlinkSpy).toHaveBeenCalledTimes(1);
|
expect(unlinkSpy).toHaveBeenCalledTimes(1);
|
||||||
// The filename is predictable in the test environment because of the multer config in ai.routes.ts
|
// The filename is unique in all environments to prevent race conditions
|
||||||
expect(unlinkSpy).toHaveBeenCalledWith(
|
expect(unlinkSpy).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('flyerImage-test-flyer-image.jpg'),
|
expect.stringMatching(/flyerImage-\d+-\d+-test-flyer-image\.jpg/),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -288,30 +288,65 @@ router.post('/logout', logoutLimiter, async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
// --- OAuth Routes ---
|
// --- OAuth Routes ---
|
||||||
|
|
||||||
// const handleOAuthCallback = (req: Request, res: Response) => {
|
/**
|
||||||
// const user = req.user as { user_id: string; email: string };
|
* Handles the OAuth callback after successful authentication.
|
||||||
// const payload = { user_id: user.user_id, email: user.email };
|
* Generates tokens and redirects to the frontend with the access token.
|
||||||
// const accessToken = jwt.sign(payload, JWT_SECRET, { expiresIn: '15m' });
|
* @param provider The OAuth provider name ('google' or 'github') for the query param.
|
||||||
// const refreshToken = crypto.randomBytes(64).toString('hex');
|
*/
|
||||||
|
const createOAuthCallbackHandler = (provider: 'google' | 'github') => {
|
||||||
|
return async (req: Request, res: Response) => {
|
||||||
|
const userProfile = req.user as UserProfile;
|
||||||
|
|
||||||
// db.saveRefreshToken(user.user_id, refreshToken).then(() => {
|
if (!userProfile || !userProfile.user) {
|
||||||
// res.cookie('refreshToken', refreshToken, {
|
req.log.error('OAuth callback received but no user profile found');
|
||||||
// httpOnly: true,
|
return res.redirect(`${process.env.FRONTEND_URL}/?error=auth_failed`);
|
||||||
// secure: process.env.NODE_ENV === 'production',
|
}
|
||||||
// maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
||||||
// });
|
|
||||||
// // Redirect to a frontend page that can handle the token
|
|
||||||
// res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${accessToken}`);
|
|
||||||
// }).catch(err => {
|
|
||||||
// req.log.error('Failed to save refresh token during OAuth callback:', { error: err });
|
|
||||||
// res.redirect(`${process.env.FRONTEND_URL}/login?error=auth_failed`);
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
// router.get('/google', passport.authenticate('google', { session: false }));
|
try {
|
||||||
// router.get('/google/callback', passport.authenticate('google', { session: false, failureRedirect: '/login' }), handleOAuthCallback);
|
const { accessToken, refreshToken } = await authService.handleSuccessfulLogin(
|
||||||
|
userProfile,
|
||||||
|
req.log,
|
||||||
|
);
|
||||||
|
|
||||||
// router.get('/github', passport.authenticate('github', { session: false }));
|
res.cookie('refreshToken', refreshToken, {
|
||||||
// router.get('/github/callback', passport.authenticate('github', { session: false, failureRedirect: '/login' }), handleOAuthCallback);
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redirect to frontend with the token in a provider-specific query param
|
||||||
|
// The frontend useAppInitialization hook looks for googleAuthToken or githubAuthToken
|
||||||
|
const tokenParam = provider === 'google' ? 'googleAuthToken' : 'githubAuthToken';
|
||||||
|
res.redirect(`${process.env.FRONTEND_URL}/?${tokenParam}=${accessToken}`);
|
||||||
|
} catch (err) {
|
||||||
|
req.log.error({ error: err }, `Failed to complete ${provider} OAuth login`);
|
||||||
|
res.redirect(`${process.env.FRONTEND_URL}/?error=auth_failed`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve: OAuth routes require external provider interaction, not suitable for automated testing */
|
||||||
|
// Google OAuth routes
|
||||||
|
router.get('/google', passport.authenticate('google', { session: false }));
|
||||||
|
router.get(
|
||||||
|
'/google/callback',
|
||||||
|
passport.authenticate('google', {
|
||||||
|
session: false,
|
||||||
|
failureRedirect: '/?error=google_auth_failed',
|
||||||
|
}),
|
||||||
|
createOAuthCallbackHandler('google'),
|
||||||
|
);
|
||||||
|
|
||||||
|
/* istanbul ignore next -- @preserve: OAuth routes require external provider interaction, not suitable for automated testing */
|
||||||
|
// GitHub OAuth routes
|
||||||
|
router.get('/github', passport.authenticate('github', { session: false }));
|
||||||
|
router.get(
|
||||||
|
'/github/callback',
|
||||||
|
passport.authenticate('github', {
|
||||||
|
session: false,
|
||||||
|
failureRedirect: '/?error=github_auth_failed',
|
||||||
|
}),
|
||||||
|
createOAuthCallbackHandler('github'),
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
import passport from 'passport';
|
import passport from 'passport';
|
||||||
// All route handlers now use req.log (request-scoped logger) as per ADR-004
|
// All route handlers now use req.log (request-scoped logger) as per ADR-004
|
||||||
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, Profile as GoogleProfile } from 'passport-google-oauth20';
|
||||||
//import { Strategy as GitHubStrategy } from 'passport-github2';
|
import { Strategy as GitHubStrategy, Profile as GitHubProfile } from 'passport-github2';
|
||||||
// All route handlers now use req.log (request-scoped logger) as per ADR-004
|
// All route handlers now use req.log (request-scoped logger) as per ADR-004
|
||||||
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
|
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
@@ -165,108 +165,149 @@ passport.use(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// --- Passport Google OAuth 2.0 Strategy ---
|
// --- Passport Google OAuth 2.0 Strategy ---
|
||||||
// passport.use(new GoogleStrategy({
|
// Only register the strategy if the required environment variables are set.
|
||||||
// clientID: process.env.GOOGLE_CLIENT_ID!,
|
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
||||||
// clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
passport.use(
|
||||||
// callbackURL: '/api/auth/google/callback', // Must match the one in Google Cloud Console
|
new GoogleStrategy(
|
||||||
// scope: ['profile', 'email']
|
{
|
||||||
// },
|
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||||
// async (accessToken, refreshToken, profile, done) => {
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
// try {
|
callbackURL: '/api/auth/google/callback',
|
||||||
// const email = profile.emails?.[0]?.value;
|
scope: ['profile', 'email'],
|
||||||
// if (!email) {
|
},
|
||||||
// return done(new Error("No email found in Google profile."), false);
|
async (
|
||||||
// }
|
_accessToken: string,
|
||||||
|
_refreshToken: string,
|
||||||
|
profile: GoogleProfile,
|
||||||
|
done: (error: Error | null, user?: UserProfile | false) => void,
|
||||||
|
) => {
|
||||||
|
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
|
// Check if user already exists in our database
|
||||||
// const user = await db.findUserByEmail(email); // Changed to const as 'user' is not reassigned
|
const existingUserProfile = await db.userRepo.findUserWithProfileByEmail(email, logger);
|
||||||
|
|
||||||
// if (user) {
|
if (existingUserProfile) {
|
||||||
// // User exists, proceed to log them in.
|
// User exists, proceed to log them in.
|
||||||
// req.log.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.
|
// Strip sensitive fields before returning
|
||||||
// const { password_hash, ...userWithoutHash } = user;
|
const {
|
||||||
// return done(null, userWithoutHash);
|
password_hash: _password_hash,
|
||||||
// } else {
|
failed_login_attempts: _failed_login_attempts,
|
||||||
// // User does not exist, create a new account for them.
|
last_failed_login: _last_failed_login,
|
||||||
// req.log.info(`Google OAuth: creating new user for email: ${email}`);
|
refresh_token: _refresh_token,
|
||||||
|
...cleanUserProfile
|
||||||
|
} = existingUserProfile;
|
||||||
|
return done(null, cleanUserProfile);
|
||||||
|
} 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.
|
// 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 newUserProfile = await db.userRepo.createUser(
|
||||||
// full_name: profile.displayName,
|
email,
|
||||||
// avatar_url: profile.photos?.[0]?.value
|
null, // No password for OAuth users
|
||||||
// });
|
{
|
||||||
|
full_name: profile.displayName,
|
||||||
|
avatar_url: profile.photos?.[0]?.value,
|
||||||
|
},
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
|
||||||
// // Send a welcome email to the new user
|
return done(null, newUserProfile);
|
||||||
// try {
|
}
|
||||||
// await sendWelcomeEmail(email, profile.displayName);
|
} catch (err) {
|
||||||
// } catch (emailError) {
|
logger.error({ error: err }, 'Error during Google authentication strategy');
|
||||||
// req.log.error(`Failed to send welcome email to new Google user ${email}`, { error: emailError });
|
return done(err as Error, false);
|
||||||
// // Don't block the login flow if email fails.
|
}
|
||||||
// }
|
},
|
||||||
|
),
|
||||||
// // The `createUser` function returns the user object without the password hash.
|
);
|
||||||
// return done(null, newUser);
|
logger.info('[Passport] Google OAuth strategy registered.');
|
||||||
// }
|
} else {
|
||||||
// } catch (err) {
|
logger.warn(
|
||||||
// req.log.error('Error during Google authentication strategy:', { error: err });
|
'[Passport] Google OAuth strategy NOT registered: GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET not set.',
|
||||||
// return done(err, false);
|
);
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
// ));
|
|
||||||
|
|
||||||
// --- Passport GitHub OAuth 2.0 Strategy ---
|
// --- Passport GitHub OAuth 2.0 Strategy ---
|
||||||
// passport.use(new GitHubStrategy({
|
// Only register the strategy if the required environment variables are set.
|
||||||
// clientID: process.env.GITHUB_CLIENT_ID!,
|
if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) {
|
||||||
// clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
passport.use(
|
||||||
// callbackURL: '/api/auth/github/callback', // Must match the one in GitHub OAuth App settings
|
new GitHubStrategy(
|
||||||
// scope: ['user:email'] // Request email access
|
{
|
||||||
// },
|
clientID: process.env.GITHUB_CLIENT_ID,
|
||||||
// async (accessToken, refreshToken, profile, done) => {
|
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
||||||
// try {
|
callbackURL: '/api/auth/github/callback',
|
||||||
// const email = profile.emails?.[0]?.value;
|
scope: ['user: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);
|
async (
|
||||||
// }
|
_accessToken: string,
|
||||||
|
_refreshToken: string,
|
||||||
|
profile: GitHubProfile,
|
||||||
|
done: (error: Error | null, user?: UserProfile | false) => void,
|
||||||
|
) => {
|
||||||
|
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
|
// Check if user already exists in our database
|
||||||
// const user = await db.findUserByEmail(email); // Changed to const as 'user' is not reassigned
|
const existingUserProfile = await db.userRepo.findUserWithProfileByEmail(email, logger);
|
||||||
|
|
||||||
// if (user) {
|
if (existingUserProfile) {
|
||||||
// // User exists, proceed to log them in.
|
// User exists, proceed to log them in.
|
||||||
// req.log.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.
|
// Strip sensitive fields before returning
|
||||||
// const { password_hash, ...userWithoutHash } = user;
|
const {
|
||||||
// return done(null, userWithoutHash);
|
password_hash: _password_hash,
|
||||||
// } else {
|
failed_login_attempts: _failed_login_attempts,
|
||||||
// // User does not exist, create a new account for them.
|
last_failed_login: _last_failed_login,
|
||||||
// req.log.info(`GitHub OAuth: creating new user for email: ${email}`);
|
refresh_token: _refresh_token,
|
||||||
|
...cleanUserProfile
|
||||||
|
} = existingUserProfile;
|
||||||
|
return done(null, cleanUserProfile);
|
||||||
|
} 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.
|
// 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 newUserProfile = await db.userRepo.createUser(
|
||||||
// full_name: profile.displayName || profile.username, // GitHub profile might not have displayName
|
email,
|
||||||
// avatar_url: profile.photos?.[0]?.value
|
null, // No password for OAuth users
|
||||||
// });
|
{
|
||||||
|
full_name: profile.displayName || profile.username, // GitHub profile might not have displayName
|
||||||
|
avatar_url: profile.photos?.[0]?.value,
|
||||||
|
},
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
|
||||||
// // Send a welcome email to the new user
|
return done(null, newUserProfile);
|
||||||
// try {
|
}
|
||||||
// await sendWelcomeEmail(email, profile.displayName || profile.username);
|
} catch (err) {
|
||||||
// } catch (emailError) {
|
logger.error({ error: err }, 'Error during GitHub authentication strategy');
|
||||||
// req.log.error(`Failed to send welcome email to new GitHub user ${email}`, { error: emailError });
|
return done(err as Error, false);
|
||||||
// // Don't block the login flow if email fails.
|
}
|
||||||
// }
|
},
|
||||||
|
),
|
||||||
// // The `createUser` function returns the user object without the password hash.
|
);
|
||||||
// return done(null, newUser);
|
logger.info('[Passport] GitHub OAuth strategy registered.');
|
||||||
// }
|
} else {
|
||||||
// } catch (err) {
|
logger.warn(
|
||||||
// req.log.error('Error during GitHub authentication strategy:', { error: err });
|
'[Passport] GitHub OAuth strategy NOT registered: GITHUB_CLIENT_ID or GITHUB_CLIENT_SECRET not set.',
|
||||||
// return done(err, false);
|
);
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
// ));
|
|
||||||
|
|
||||||
// --- Passport JWT Strategy (for protecting API routes) ---
|
// --- Passport JWT Strategy (for protecting API routes) ---
|
||||||
const jwtOptions = {
|
const jwtOptions = {
|
||||||
|
|||||||
Reference in New Issue
Block a user