Initial Claude Run

This commit is contained in:
Jake Kasper
2025-08-21 07:06:36 -05:00
parent 5ead64afcd
commit 2a46f7261e
53 changed files with 7633 additions and 2 deletions

313
backend/src/routes/auth.js Normal file
View File

@@ -0,0 +1,313 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const pool = require('../config/database');
const { validateRequest } = require('../utils/validation');
const { registerSchema, loginSchema, changePasswordSchema } = require('../utils/validation');
const { AppError } = require('../middleware/errorHandler');
const router = express.Router();
// Configure Google OAuth2 strategy
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/api/auth/google/callback'
}, async (accessToken, refreshToken, profile, done) => {
try {
// Check if user already exists
const existingUser = await pool.query(
'SELECT * FROM users WHERE oauth_provider = $1 AND oauth_id = $2',
['google', profile.id]
);
if (existingUser.rows.length > 0) {
return done(null, existingUser.rows[0]);
}
// Check if user exists with same email
const emailUser = await pool.query(
'SELECT * FROM users WHERE email = $1',
[profile.emails[0].value]
);
if (emailUser.rows.length > 0) {
// Link Google account to existing user
await pool.query(
'UPDATE users SET oauth_provider = $1, oauth_id = $2 WHERE id = $3',
['google', profile.id, emailUser.rows[0].id]
);
return done(null, emailUser.rows[0]);
}
// Create new user
const newUser = await pool.query(
`INSERT INTO users (email, first_name, last_name, oauth_provider, oauth_id)
VALUES ($1, $2, $3, $4, $5) RETURNING *`,
[
profile.emails[0].value,
profile.name.givenName,
profile.name.familyName,
'google',
profile.id
]
);
return done(null, newUser.rows[0]);
} catch (error) {
return done(error, null);
}
}));
}
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
try {
const user = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
done(null, user.rows[0]);
} catch (error) {
done(error, null);
}
});
// Generate JWT token
const generateToken = (userId) => {
return jwt.sign(
{ userId },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
};
// @route POST /api/auth/register
// @desc Register a new user
// @access Public
router.post('/register', validateRequest(registerSchema), async (req, res, next) => {
try {
const { email, password, firstName, lastName } = req.body;
// Check if user already exists
const existingUser = await pool.query(
'SELECT id FROM users WHERE email = $1',
[email.toLowerCase()]
);
if (existingUser.rows.length > 0) {
throw new AppError('User with this email already exists', 409);
}
// Hash password
const saltRounds = 12;
const passwordHash = await bcrypt.hash(password, saltRounds);
// Create user
const newUser = await pool.query(
`INSERT INTO users (email, password_hash, first_name, last_name)
VALUES ($1, $2, $3, $4) RETURNING id, email, first_name, last_name, role`,
[email.toLowerCase(), passwordHash, firstName, lastName]
);
const user = newUser.rows[0];
const token = generateToken(user.id);
res.status(201).json({
success: true,
message: 'User registered successfully',
data: {
user: {
id: user.id,
email: user.email,
firstName: user.first_name,
lastName: user.last_name,
role: user.role
},
token
}
});
} catch (error) {
next(error);
}
});
// @route POST /api/auth/login
// @desc Login user
// @access Public
router.post('/login', validateRequest(loginSchema), async (req, res, next) => {
try {
const { email, password } = req.body;
// Find user
const userResult = await pool.query(
'SELECT id, email, password_hash, first_name, last_name, role FROM users WHERE email = $1',
[email.toLowerCase()]
);
if (userResult.rows.length === 0) {
throw new AppError('Invalid email or password', 401);
}
const user = userResult.rows[0];
// Check password
if (!user.password_hash) {
throw new AppError('Please use Google sign-in for this account', 401);
}
const isValidPassword = await bcrypt.compare(password, user.password_hash);
if (!isValidPassword) {
throw new AppError('Invalid email or password', 401);
}
const token = generateToken(user.id);
res.json({
success: true,
message: 'Login successful',
data: {
user: {
id: user.id,
email: user.email,
firstName: user.first_name,
lastName: user.last_name,
role: user.role
},
token
}
});
} catch (error) {
next(error);
}
});
// @route GET /api/auth/google
// @desc Start Google OAuth2 flow
// @access Public
router.get('/google', passport.authenticate('google', {
scope: ['profile', 'email']
}));
// @route GET /api/auth/google/callback
// @desc Google OAuth2 callback
// @access Public
router.get('/google/callback',
passport.authenticate('google', { session: false }),
(req, res) => {
const token = generateToken(req.user.id);
// Redirect to frontend with token
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
res.redirect(`${frontendUrl}/auth/callback?token=${token}`);
}
);
// @route POST /api/auth/change-password
// @desc Change user password
// @access Private
router.post('/change-password', validateRequest(changePasswordSchema), async (req, res, next) => {
try {
const { currentPassword, newPassword } = req.body;
const userId = req.user.id;
// Get current user
const userResult = await pool.query(
'SELECT password_hash FROM users WHERE id = $1',
[userId]
);
const user = userResult.rows[0];
if (!user.password_hash) {
throw new AppError('Cannot change password for OAuth accounts', 400);
}
// Verify current password
const isValidPassword = await bcrypt.compare(currentPassword, user.password_hash);
if (!isValidPassword) {
throw new AppError('Current password is incorrect', 401);
}
// Hash new password
const saltRounds = 12;
const newPasswordHash = await bcrypt.hash(newPassword, saltRounds);
// Update password
await pool.query(
'UPDATE users SET password_hash = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2',
[newPasswordHash, userId]
);
res.json({
success: true,
message: 'Password changed successfully'
});
} catch (error) {
next(error);
}
});
// @route POST /api/auth/forgot-password
// @desc Request password reset
// @access Public
router.post('/forgot-password', async (req, res, next) => {
try {
const { email } = req.body;
// Check if user exists
const userResult = await pool.query(
'SELECT id, first_name FROM users WHERE email = $1',
[email.toLowerCase()]
);
// Always return success for security (don't reveal if email exists)
res.json({
success: true,
message: 'If an account with that email exists, a password reset link has been sent'
});
// TODO: Implement email sending logic
// In a real application, you would:
// 1. Generate a secure reset token
// 2. Store it in database with expiration
// 3. Send email with reset link
} catch (error) {
next(error);
}
});
// @route GET /api/auth/me
// @desc Get current user info
// @access Private
router.get('/me', async (req, res, next) => {
try {
const userResult = await pool.query(
'SELECT id, email, first_name, last_name, role, created_at FROM users WHERE id = $1',
[req.user.id]
);
const user = userResult.rows[0];
res.json({
success: true,
data: {
user: {
id: user.id,
email: user.email,
firstName: user.first_name,
lastName: user.last_name,
role: user.role,
createdAt: user.created_at
}
}
});
} catch (error) {
next(error);
}
});
module.exports = router;