Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Server Configuration
PORT=5000
NODE_ENV=development
SERVER_URL=http://localhost:5000
CLIENT_URL=http://localhost:3000

# Database
MONGODB_URI=mongodb://localhost:27017/stackit
MONGODB_URI_TEST=mongodb://localhost:27017/stackit-test

# Authentication
JWT_SECRET=your-secret-jwt-string

# Google OAuth
GOOGLE_CLIENT_ID=your-google-oauth-client-id
GOOGLE_CLIENT_SECRET=your-google-oauth-client-secret
GOOGLE_CALLBACK_URL=http://localhost:5000/api/auth/google/callback

# Twitter/X OAuth
TWITTER_CLIENT_ID=your-twitter-oauth-client-id
TWITTER_CLIENT_SECRET=your-twitter-oauth-client-secret
TWITTER_CALLBACK_URL=http://localhost:5000/api/auth/twitter/callback

# Rate Limiting
RATE_LIMIT_ENABLED=false
RATE_LIMIT_MAX=100

# Client Configuration
VITE_API_URL=/api
VITE_ENABLE_ANALYTICS=false
VITE_ENABLE_DEBUG=false
22 changes: 20 additions & 2 deletions client/src/components/AppProviders.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,26 @@ const AppProviders = ({ children }) => {
toastOptions={{
duration: 4000,
style: {
background: 'var(--toast-bg)',
color: 'var(--toast-color)',
background: '#1e293b',
color: '#f1f5f9',
borderRadius: '12px',
border: '1px solid rgba(148, 163, 184, 0.15)',
boxShadow: '0 10px 25px rgba(0, 0, 0, 0.3)',
fontSize: '14px',
fontWeight: '500',
padding: '12px 16px',
},
success: {
iconTheme: {
primary: '#22c55e',
secondary: '#f0fdf4',
},
},
error: {
iconTheme: {
primary: '#ef4444',
secondary: '#fef2f2',
},
},
}}
/>
Expand Down
22 changes: 22 additions & 0 deletions client/src/components/ProfileRedirect.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Navigate } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'

const ProfileRedirect = () => {
const { user, isAuthenticated, loading } = useAuth()

if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
</div>
)
}

if (!isAuthenticated || !user) {
return <Navigate to="/login" replace />
}

return <Navigate to={`/profile/${user.username}`} replace />
}

export default ProfileRedirect
222 changes: 137 additions & 85 deletions client/src/contexts/AuthContext.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
import { createContext, useContext, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-hot-toast';
import api from '../utils/api';
import { createContext, useContext, useState, useEffect, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import { toast } from 'react-hot-toast'
import api, { API_BASE_URL } from '../utils/api'

const AuthContext = createContext();

// Session duration: 7 days in milliseconds
const SESSION_DURATION_MS = 7 * 24 * 60 * 60 * 1000

const saveSession = (token, userData) => {
localStorage.setItem('token', token)
localStorage.setItem('tokenExpiry', String(Date.now() + SESSION_DURATION_MS))
localStorage.setItem('cachedUser', JSON.stringify(userData))
}

const clearSession = () => {
localStorage.removeItem('token')
localStorage.removeItem('tokenExpiry')
localStorage.removeItem('cachedUser')
delete api.defaults.headers.common['Authorization']
}

const isSessionValid = () => {
const token = localStorage.getItem('token')
const expiry = localStorage.getItem('tokenExpiry')
if (!token || !expiry) return false
return Date.now() < Number(expiry)
}

export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
Expand All @@ -14,53 +37,50 @@ export const useAuth = () => {
};

export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
const [user, setUser] = useState(() => {
// Restore user from cache immediately to prevent flash of logged-out state
if (isSessionValid()) {
try {
const cached = localStorage.getItem('cachedUser')
return cached ? JSON.parse(cached) : null
} catch {
return null
}
}
return null
})
const [loading, setLoading] = useState(true)
const navigate = useNavigate()

useEffect(() => {
checkAuth();
}, []);

const checkAuth = async () => {
try {
const token = localStorage.getItem('token');
console.log(
'🔍 AuthContext Debug - Checking auth, token exists:',
!!token
);

if (token) {
api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
const response = await api.get('api/auth/me');
console.log(
'🔍 AuthContext Debug - Auth check successful:',
response.data.user.username
);
setUser(response.data.user);
} else {
console.log(
'🔍 AuthContext Debug - No token found, user not authenticated'
);
if (!isSessionValid()) {
clearSession()
setUser(null)
setLoading(false)
return
}

const token = localStorage.getItem('token')
api.defaults.headers.common['Authorization'] = `Bearer ${token}`

// Validate token with server in the background
const response = await api.get('/auth/me')
const freshUser = response.data.user
setUser(freshUser)
// Update the cached user with fresh data
localStorage.setItem('cachedUser', JSON.stringify(freshUser))
} catch (error) {
console.log(
'🔍 AuthContext Debug - Auth check failed:',
error.response?.data?.message
);
// Only clear token if it's a token-related error
// Only clear session if it's a token-related error
if (error.response?.status === 401) {
const errorMessage = error.response?.data?.message || '';
if (
errorMessage.includes('token') ||
errorMessage.includes('expired') ||
errorMessage.includes('Invalid')
) {
console.log(
'🔍 AuthContext Debug - Token error, clearing localStorage'
);
localStorage.removeItem('token');
delete api.defaults.headers.common['Authorization'];
const errorMessage = error.response?.data?.message || ''
if (errorMessage.includes('token') || errorMessage.includes('expired') || errorMessage.includes('Invalid')) {
clearSession()
setUser(null)
}
}
} finally {
Expand All @@ -70,66 +90,92 @@ export const AuthProvider = ({ children }) => {

const login = async (email, password) => {
try {
const response = await api.post('api/auth/login', { email, password });
const { token, user } = response.data;

console.log(
'🔍 AuthContext Debug - Login response token:',
token ? token.substring(0, 20) + '...' : 'No token'
);
console.log('🔍 AuthContext Debug - Login response user:', user.username);
const response = await api.post('/auth/login', { email, password })
const { token, user } = response.data

localStorage.setItem('token', token);
console.log('🔍 AuthContext Debug - Token saved to localStorage');
saveSession(token, user)
api.defaults.headers.common['Authorization'] = `Bearer ${token}`
setUser(user)

api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
setUser(user);

toast.success('Welcome back!');
navigate('/questions');
return { success: true };
toast.success('Welcome back!')
navigate('/questions')
return { success: true }
} catch (error) {
console.log('❌ AuthContext Debug - Login error:', error.response?.data);
const message = error.response?.data?.message || 'Login failed';
toast.error(message);
return { success: false, error: message };
let message = error.response?.data?.message || 'Login failed'
if (message === 'Validation failed' && error.response?.data?.errors?.length > 0) {
message = error.response.data.errors[0].msg
}
toast.error(message)
return { success: false, error: message }
}
};

const signup = async userData => {
try {
const response = await api.post('api/auth/signup', userData);
const { token, user } = response.data;
const response = await api.post('/auth/signup', userData)
const { token, user } = response.data

localStorage.setItem('token', token);
api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
setUser(user);
saveSession(token, user)
api.defaults.headers.common['Authorization'] = `Bearer ${token}`
setUser(user)

toast.success('Account created successfully!');
navigate('/questions');
return { success: true };
toast.success('Account created successfully!')
navigate('/questions')
return { success: true }
} catch (error) {
const message = error.response?.data?.message || 'Signup failed';
toast.error(message);
return { success: false, error: message };
let message = error.response?.data?.message || 'Signup failed'
if (message === 'Validation failed' && error.response?.data?.errors?.length > 0) {
message = error.response.data.errors[0].msg
}
toast.error(message)
return { success: false, error: message }
}
};

const startOAuthLogin = useCallback((provider) => {
const apiBaseUrl = API_BASE_URL.replace(/\/$/, '')
window.location.assign(`${apiBaseUrl}/auth/${provider}`)
}, [])

const completeOAuthLogin = useCallback(async (token, provider) => {
try {
api.defaults.headers.common['Authorization'] = `Bearer ${token}`

const response = await api.get('/auth/me')
const userData = response.data.user
saveSession(token, userData)
setUser(userData)

const providerName = provider === 'twitter' ? 'Twitter' : 'Google'
toast.success(`Signed in with ${providerName}`)
navigate('/questions')
return { success: true }
} catch (error) {
clearSession()
setUser(null)

const message = error.response?.data?.message || 'OAuth login failed'
toast.error(message)
navigate('/login')
return { success: false, error: message }
}
}, [navigate])

const logout = () => {
console.log('🔍 AuthContext Debug - Logging out user');
localStorage.removeItem('token');
delete api.defaults.headers.common['Authorization'];
setUser(null);
toast.success('Logged out successfully');
navigate('/');
};
clearSession()
setUser(null)
toast.success('Logged out successfully')
navigate('/')
}

const updateProfile = async userData => {
try {
const response = await api.put('api/auth/profile', userData);
setUser(response.data.user);
toast.success('Profile updated successfully');
return { success: true };
const response = await api.put('/auth/profile', userData)
const updatedUser = response.data.user
setUser(updatedUser)
localStorage.setItem('cachedUser', JSON.stringify(updatedUser))
toast.success('Profile updated successfully')
return { success: true }
} catch (error) {
const message = error.response?.data?.message || 'Update failed';
toast.error(message);
Expand All @@ -142,11 +188,17 @@ export const AuthProvider = ({ children }) => {
loading,
login,
signup,
startOAuthLogin,
completeOAuthLogin,
logout,
updateProfile,
isAuthenticated: !!user,
isAdmin: user?.role === 'admin',
};

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
Loading
Loading