nn
Browse files- backend/api/accounts.py +3 -47
- backend/services/auth_service.py +60 -14
- frontend/src/components/LinkedInAccount/LinkedInCallbackHandler.jsx +2 -2
- frontend/src/pages/Accounts.jsx +20 -15
- frontend/src/pages/Home.jsx +0 -47
- frontend/src/pages/Login.jsx +0 -37
- frontend/src/pages/Posts.jsx +9 -8
- frontend/src/pages/Register.jsx +23 -40
- frontend/src/services/accountService.js +0 -21
- frontend/src/services/authService.js +0 -8
- frontend/src/services/linkedinAuthService.js +3 -52
- frontend/src/store/reducers/authSlice.js +57 -4
- frontend/src/store/reducers/linkedinAccountsSlice.js +1 -24
backend/api/accounts.py
CHANGED
|
@@ -24,12 +24,8 @@ def get_accounts():
|
|
| 24 |
try:
|
| 25 |
user_id = get_jwt_identity()
|
| 26 |
|
| 27 |
-
# DEBUG: Log account retrieval request
|
| 28 |
-
current_app.logger.info(f"π± [Accounts] Retrieving accounts for user: {user_id}")
|
| 29 |
-
|
| 30 |
# Check if Supabase client is initialized
|
| 31 |
if not hasattr(current_app, 'supabase') or current_app.supabase is None:
|
| 32 |
-
current_app.logger.error(f"π± [Accounts] Supabase client not initialized for user: {user_id}")
|
| 33 |
# Add CORS headers to error response
|
| 34 |
response_data = jsonify({
|
| 35 |
'success': False,
|
|
@@ -39,9 +35,6 @@ def get_accounts():
|
|
| 39 |
response_data.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 40 |
return response_data, 500
|
| 41 |
|
| 42 |
-
# DEBUG: Log database query
|
| 43 |
-
current_app.logger.info(f"π± [Accounts] Querying database for user: {user_id}")
|
| 44 |
-
|
| 45 |
# Fetch accounts from Supabase
|
| 46 |
response = (
|
| 47 |
current_app.supabase
|
|
@@ -51,13 +44,7 @@ def get_accounts():
|
|
| 51 |
.execute()
|
| 52 |
)
|
| 53 |
|
| 54 |
-
# DEBUG: Log database response
|
| 55 |
-
current_app.logger.info(f"π± [Accounts] Database response: {response}")
|
| 56 |
-
current_app.logger.info(f"π± [Accounts] Response data: {response.data}")
|
| 57 |
-
current_app.logger.info(f"π± [Accounts] Response error: {getattr(response, 'error', None)}")
|
| 58 |
-
|
| 59 |
accounts = response.data if response.data else []
|
| 60 |
-
current_app.logger.info(f"π± [Accounts] Found {len(accounts)} accounts for user: {user_id}")
|
| 61 |
|
| 62 |
# Add CORS headers explicitly
|
| 63 |
response_data = jsonify({
|
|
@@ -100,13 +87,8 @@ def add_account():
|
|
| 100 |
user_id = get_jwt_identity()
|
| 101 |
data = request.get_json()
|
| 102 |
|
| 103 |
-
# DEBUG: Log account creation request
|
| 104 |
-
current_app.logger.info(f"β [Accounts] Add account request for user: {user_id}")
|
| 105 |
-
current_app.logger.info(f"β [Accounts] Request data: {data}")
|
| 106 |
-
|
| 107 |
# Validate required fields
|
| 108 |
if not data or not all(k in data for k in ('account_name', 'social_network')):
|
| 109 |
-
current_app.logger.error(f"β [Accounts] Missing required fields. Data: {data}")
|
| 110 |
return jsonify({
|
| 111 |
'success': False,
|
| 112 |
'message': 'Account name and social network are required'
|
|
@@ -115,28 +97,17 @@ def add_account():
|
|
| 115 |
account_name = data['account_name']
|
| 116 |
social_network = data['social_network']
|
| 117 |
|
| 118 |
-
# DEBUG: Log validated parameters
|
| 119 |
-
current_app.logger.info(f"β [Accounts] Validated - Account: {account_name}, Network: {social_network}")
|
| 120 |
-
|
| 121 |
# For LinkedIn, initiate OAuth flow
|
| 122 |
if social_network.lower() == 'linkedin':
|
| 123 |
-
current_app.logger.info(f"β [Accounts] Initiating LinkedIn OAuth for user: {user_id}")
|
| 124 |
-
|
| 125 |
linkedin_service = LinkedInService()
|
| 126 |
# Generate a random state for security
|
| 127 |
state = secrets.token_urlsafe(32)
|
| 128 |
|
| 129 |
-
# DEBUG: Log OAuth state
|
| 130 |
-
current_app.logger.info(f"β [Accounts] Generated OAuth state: {state}")
|
| 131 |
-
|
| 132 |
# Store state in session or database for verification later
|
| 133 |
# For now, we'll return it to the frontend
|
| 134 |
try:
|
| 135 |
authorization_url = linkedin_service.get_authorization_url(state)
|
| 136 |
-
current_app.logger.info(f"β [Accounts] OAuth authorization URL generated successfully")
|
| 137 |
-
current_app.logger.info(f"β [Accounts] Authorization URL length: {len(authorization_url)}")
|
| 138 |
except Exception as oauth_error:
|
| 139 |
-
current_app.logger.error(f"β [Accounts] OAuth URL generation failed: {str(oauth_error)}")
|
| 140 |
return jsonify({
|
| 141 |
'success': False,
|
| 142 |
'message': f'Failed to generate authorization URL: {str(oauth_error)}'
|
|
@@ -183,14 +154,8 @@ def handle_oauth_callback():
|
|
| 183 |
user_id = get_jwt_identity()
|
| 184 |
data = request.get_json()
|
| 185 |
|
| 186 |
-
# DEBUG: Log the start of OAuth callback
|
| 187 |
-
current_app.logger.info(f"π [OAuth] Starting callback for user: {user_id}")
|
| 188 |
-
current_app.logger.info(f"π [OAuth] Received data: {data}")
|
| 189 |
-
current_app.logger.info(f"π [OAuth] Request headers: {dict(request.headers)}")
|
| 190 |
-
|
| 191 |
# Validate required fields
|
| 192 |
if not data or not all(k in data for k in ('code', 'state', 'social_network')):
|
| 193 |
-
current_app.logger.error(f"π [OAuth] Missing required fields. Data: {data}")
|
| 194 |
return jsonify({
|
| 195 |
'success': False,
|
| 196 |
'message': 'Code, state, and social network are required'
|
|
@@ -200,21 +165,12 @@ def handle_oauth_callback():
|
|
| 200 |
state = data['state']
|
| 201 |
social_network = data['social_network']
|
| 202 |
|
| 203 |
-
# DEBUG: Log validated parameters
|
| 204 |
-
current_app.logger.info(f"π [OAuth] Validated parameters - Code: {code[:20]}..., State: {state}, Network: {social_network}")
|
| 205 |
-
|
| 206 |
# Verify state (in a real implementation, you would check against stored state)
|
| 207 |
# For now, we'll skip this verification
|
| 208 |
|
| 209 |
if social_network.lower() == 'linkedin':
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
# DEBUG: Check Supabase client availability
|
| 213 |
-
supabase_available = hasattr(current_app, 'supabase') and current_app.supabase is not None
|
| 214 |
-
current_app.logger.info(f"π [OAuth] Supabase client available: {supabase_available}")
|
| 215 |
-
|
| 216 |
-
if not supabase_available:
|
| 217 |
-
current_app.logger.error(f"π [OAuth] Supabase client not available for user: {user_id}")
|
| 218 |
return jsonify({
|
| 219 |
'success': False,
|
| 220 |
'message': 'Database connection not available'
|
|
@@ -441,7 +397,7 @@ def get_session_data():
|
|
| 441 |
return jsonify({
|
| 442 |
'success': True,
|
| 443 |
'oauth_data': oauth_data
|
| 444 |
-
}),
|
| 445 |
else:
|
| 446 |
current_app.logger.warning(f"π [Session] No OAuth data found in session for user: {user_id}")
|
| 447 |
current_app.logger.info(f"π [Session] Current session keys: {list(session.keys())}")
|
|
|
|
| 24 |
try:
|
| 25 |
user_id = get_jwt_identity()
|
| 26 |
|
|
|
|
|
|
|
|
|
|
| 27 |
# Check if Supabase client is initialized
|
| 28 |
if not hasattr(current_app, 'supabase') or current_app.supabase is None:
|
|
|
|
| 29 |
# Add CORS headers to error response
|
| 30 |
response_data = jsonify({
|
| 31 |
'success': False,
|
|
|
|
| 35 |
response_data.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 36 |
return response_data, 500
|
| 37 |
|
|
|
|
|
|
|
|
|
|
| 38 |
# Fetch accounts from Supabase
|
| 39 |
response = (
|
| 40 |
current_app.supabase
|
|
|
|
| 44 |
.execute()
|
| 45 |
)
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
accounts = response.data if response.data else []
|
|
|
|
| 48 |
|
| 49 |
# Add CORS headers explicitly
|
| 50 |
response_data = jsonify({
|
|
|
|
| 87 |
user_id = get_jwt_identity()
|
| 88 |
data = request.get_json()
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
# Validate required fields
|
| 91 |
if not data or not all(k in data for k in ('account_name', 'social_network')):
|
|
|
|
| 92 |
return jsonify({
|
| 93 |
'success': False,
|
| 94 |
'message': 'Account name and social network are required'
|
|
|
|
| 97 |
account_name = data['account_name']
|
| 98 |
social_network = data['social_network']
|
| 99 |
|
|
|
|
|
|
|
|
|
|
| 100 |
# For LinkedIn, initiate OAuth flow
|
| 101 |
if social_network.lower() == 'linkedin':
|
|
|
|
|
|
|
| 102 |
linkedin_service = LinkedInService()
|
| 103 |
# Generate a random state for security
|
| 104 |
state = secrets.token_urlsafe(32)
|
| 105 |
|
|
|
|
|
|
|
|
|
|
| 106 |
# Store state in session or database for verification later
|
| 107 |
# For now, we'll return it to the frontend
|
| 108 |
try:
|
| 109 |
authorization_url = linkedin_service.get_authorization_url(state)
|
|
|
|
|
|
|
| 110 |
except Exception as oauth_error:
|
|
|
|
| 111 |
return jsonify({
|
| 112 |
'success': False,
|
| 113 |
'message': f'Failed to generate authorization URL: {str(oauth_error)}'
|
|
|
|
| 154 |
user_id = get_jwt_identity()
|
| 155 |
data = request.get_json()
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
# Validate required fields
|
| 158 |
if not data or not all(k in data for k in ('code', 'state', 'social_network')):
|
|
|
|
| 159 |
return jsonify({
|
| 160 |
'success': False,
|
| 161 |
'message': 'Code, state, and social network are required'
|
|
|
|
| 165 |
state = data['state']
|
| 166 |
social_network = data['social_network']
|
| 167 |
|
|
|
|
|
|
|
|
|
|
| 168 |
# Verify state (in a real implementation, you would check against stored state)
|
| 169 |
# For now, we'll skip this verification
|
| 170 |
|
| 171 |
if social_network.lower() == 'linkedin':
|
| 172 |
+
# Check Supabase client availability
|
| 173 |
+
if not hasattr(current_app, 'supabase') or current_app.supabase is None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
return jsonify({
|
| 175 |
'success': False,
|
| 176 |
'message': 'Database connection not available'
|
|
|
|
| 397 |
return jsonify({
|
| 398 |
'success': True,
|
| 399 |
'oauth_data': oauth_data
|
| 400 |
+
}),
|
| 401 |
else:
|
| 402 |
current_app.logger.warning(f"π [Session] No OAuth data found in session for user: {user_id}")
|
| 403 |
current_app.logger.info(f"π [Session] Current session keys: {list(session.keys())}")
|
backend/services/auth_service.py
CHANGED
|
@@ -25,14 +25,28 @@ def register_user(email: str, password: str) -> dict:
|
|
| 25 |
user = User.from_dict({
|
| 26 |
'id': response.user.id,
|
| 27 |
'email': response.user.email,
|
| 28 |
-
'created_at': response.user.created_at
|
|
|
|
| 29 |
})
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
else:
|
| 37 |
return {
|
| 38 |
'success': False,
|
|
@@ -40,10 +54,21 @@ def register_user(email: str, password: str) -> dict:
|
|
| 40 |
}
|
| 41 |
except Exception as e:
|
| 42 |
# Check if it's a duplicate user error
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
return {
|
| 45 |
'success': False,
|
| 46 |
-
'message': '
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
else:
|
| 49 |
return {
|
|
@@ -72,7 +97,8 @@ def login_user(email: str, password: str, remember_me: bool = False) -> dict:
|
|
| 72 |
if not response.user.email_confirmed_at:
|
| 73 |
return {
|
| 74 |
'success': False,
|
| 75 |
-
'message': 'Please confirm your email before logging in'
|
|
|
|
| 76 |
}
|
| 77 |
|
| 78 |
# Set token expiration based on remember me flag
|
|
@@ -115,14 +141,34 @@ def login_user(email: str, password: str, remember_me: bool = False) -> dict:
|
|
| 115 |
else:
|
| 116 |
return {
|
| 117 |
'success': False,
|
| 118 |
-
'message': 'Invalid email or password'
|
| 119 |
}
|
| 120 |
except Exception as e:
|
| 121 |
current_app.logger.error(f"Login error: {str(e)}")
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
def get_user_by_id(user_id: str) -> dict:
|
| 128 |
"""
|
|
|
|
| 25 |
user = User.from_dict({
|
| 26 |
'id': response.user.id,
|
| 27 |
'email': response.user.email,
|
| 28 |
+
'created_at': response.user.created_at,
|
| 29 |
+
'email_confirmed_at': response.user.email_confirmed_at
|
| 30 |
})
|
| 31 |
|
| 32 |
+
# Check if email is confirmed
|
| 33 |
+
if response.user.email_confirmed_at:
|
| 34 |
+
# Email is confirmed, user can login immediately
|
| 35 |
+
return {
|
| 36 |
+
'success': True,
|
| 37 |
+
'message': 'Account created successfully! You can now log in with your email and password.',
|
| 38 |
+
'user': user.to_dict(),
|
| 39 |
+
'email_confirmed': True
|
| 40 |
+
}
|
| 41 |
+
else:
|
| 42 |
+
# Email confirmation is required
|
| 43 |
+
return {
|
| 44 |
+
'success': True,
|
| 45 |
+
'message': 'Account created successfully! Please check your email for a confirmation link to activate your account.',
|
| 46 |
+
'user': user.to_dict(),
|
| 47 |
+
'email_confirmed': False,
|
| 48 |
+
'requires_confirmation': True
|
| 49 |
+
}
|
| 50 |
else:
|
| 51 |
return {
|
| 52 |
'success': False,
|
|
|
|
| 54 |
}
|
| 55 |
except Exception as e:
|
| 56 |
# Check if it's a duplicate user error
|
| 57 |
+
error_str = str(e).lower()
|
| 58 |
+
if 'already registered' in error_str or 'already exists' in error_str:
|
| 59 |
+
return {
|
| 60 |
+
'success': False,
|
| 61 |
+
'message': 'An account with this email already exists. Please login instead or use a different email.'
|
| 62 |
+
}
|
| 63 |
+
elif 'invalid email' in error_str:
|
| 64 |
return {
|
| 65 |
'success': False,
|
| 66 |
+
'message': 'Please enter a valid email address.'
|
| 67 |
+
}
|
| 68 |
+
elif 'password' in error_str:
|
| 69 |
+
return {
|
| 70 |
+
'success': False,
|
| 71 |
+
'message': 'Password does not meet requirements. Please use at least 8 characters.'
|
| 72 |
}
|
| 73 |
else:
|
| 74 |
return {
|
|
|
|
| 97 |
if not response.user.email_confirmed_at:
|
| 98 |
return {
|
| 99 |
'success': False,
|
| 100 |
+
'message': 'Please confirm your email before logging in. Check your inbox for the confirmation email.',
|
| 101 |
+
'requires_confirmation': True
|
| 102 |
}
|
| 103 |
|
| 104 |
# Set token expiration based on remember me flag
|
|
|
|
| 141 |
else:
|
| 142 |
return {
|
| 143 |
'success': False,
|
| 144 |
+
'message': 'Invalid email or password. Please check your credentials and try again.'
|
| 145 |
}
|
| 146 |
except Exception as e:
|
| 147 |
current_app.logger.error(f"Login error: {str(e)}")
|
| 148 |
+
|
| 149 |
+
# Provide more specific error messages
|
| 150 |
+
error_str = str(e).lower()
|
| 151 |
+
if 'invalid credentials' in error_str or 'unauthorized' in error_str:
|
| 152 |
+
return {
|
| 153 |
+
'success': False,
|
| 154 |
+
'message': 'Invalid email or password. Please check your credentials and try again.'
|
| 155 |
+
}
|
| 156 |
+
elif 'email not confirmed' in error_str or 'email not verified' in error_str:
|
| 157 |
+
return {
|
| 158 |
+
'success': False,
|
| 159 |
+
'message': 'Please confirm your email before logging in. Check your inbox for the confirmation email.',
|
| 160 |
+
'requires_confirmation': True
|
| 161 |
+
}
|
| 162 |
+
elif 'user not found' in error_str:
|
| 163 |
+
return {
|
| 164 |
+
'success': False,
|
| 165 |
+
'message': 'No account found with this email. Please check your email or register for a new account.'
|
| 166 |
+
}
|
| 167 |
+
else:
|
| 168 |
+
return {
|
| 169 |
+
'success': False,
|
| 170 |
+
'message': f'Login failed: {str(e)}'
|
| 171 |
+
}
|
| 172 |
|
| 173 |
def get_user_by_id(user_id: str) -> dict:
|
| 174 |
"""
|
frontend/src/components/LinkedInAccount/LinkedInCallbackHandler.jsx
CHANGED
|
@@ -53,7 +53,7 @@ const LinkedInCallbackHandler = () => {
|
|
| 53 |
// Redirect to accounts page after a short delay
|
| 54 |
setTimeout(() => {
|
| 55 |
console.log('π [Frontend] Redirecting to accounts page...');
|
| 56 |
-
|
| 57 |
}, 2000);
|
| 58 |
} else {
|
| 59 |
console.log('π [Frontend] OAuth successful but account not linked');
|
|
@@ -137,7 +137,7 @@ const LinkedInCallbackHandler = () => {
|
|
| 137 |
<button onClick={handleRetry} className="btn btn-primary">
|
| 138 |
Try Again
|
| 139 |
</button>
|
| 140 |
-
<button onClick={() =>
|
| 141 |
Go to Accounts
|
| 142 |
</button>
|
| 143 |
</div>
|
|
|
|
| 53 |
// Redirect to accounts page after a short delay
|
| 54 |
setTimeout(() => {
|
| 55 |
console.log('π [Frontend] Redirecting to accounts page...');
|
| 56 |
+
window.location.href = '/accounts'; // Use direct navigation instead of React Router
|
| 57 |
}, 2000);
|
| 58 |
} else {
|
| 59 |
console.log('π [Frontend] OAuth successful but account not linked');
|
|
|
|
| 137 |
<button onClick={handleRetry} className="btn btn-primary">
|
| 138 |
Try Again
|
| 139 |
</button>
|
| 140 |
+
<button onClick={() => window.location.href = '/accounts'} className="btn btn-secondary">
|
| 141 |
Go to Accounts
|
| 142 |
</button>
|
| 143 |
</div>
|
frontend/src/pages/Accounts.jsx
CHANGED
|
@@ -1,26 +1,24 @@
|
|
| 1 |
import React, { useState, useEffect } from 'react';
|
| 2 |
import { useDispatch, useSelector } from 'react-redux';
|
|
|
|
| 3 |
import { fetchLinkedInAccounts, deleteLinkedInAccount, setPrimaryLinkedInAccount, clearLinkedInError } from '../store/reducers/linkedinAccountsSlice';
|
| 4 |
import LinkedInAccountsManager from '../components/LinkedInAccount/LinkedInAccountsManager';
|
| 5 |
import { testApiStructure, testServiceBehavior } from '../debug/testApi';
|
| 6 |
|
| 7 |
const Accounts = () => {
|
| 8 |
const dispatch = useDispatch();
|
| 9 |
-
const {
|
| 10 |
-
|
| 11 |
-
console.log('π [DEBUG] Accounts component - linkedinAccounts state:', state.linkedinAccounts);
|
| 12 |
-
return state.linkedinAccounts;
|
| 13 |
-
});
|
| 14 |
|
| 15 |
useEffect(() => {
|
| 16 |
-
|
| 17 |
-
|
| 18 |
|
|
|
|
| 19 |
dispatch(fetchLinkedInAccounts());
|
| 20 |
dispatch(clearLinkedInError());
|
| 21 |
|
| 22 |
// Run API tests
|
| 23 |
-
console.log('π§ͺ [TEST] Running API tests...');
|
| 24 |
testApiStructure();
|
| 25 |
testServiceBehavior();
|
| 26 |
}, [dispatch]);
|
|
@@ -54,7 +52,7 @@ const Accounts = () => {
|
|
| 54 |
<div className="flex items-center justify-between">
|
| 55 |
<div>
|
| 56 |
<p className="text-xs sm:text-sm font-medium text-gray-600">Total Accounts</p>
|
| 57 |
-
<p className="text-lg sm:text-2xl font-bold text-gray-900">
|
| 58 |
</div>
|
| 59 |
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
| 60 |
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -68,7 +66,7 @@ const Accounts = () => {
|
|
| 68 |
<div className="flex items-center justify-between">
|
| 69 |
<div>
|
| 70 |
<p className="text-xs sm:text-sm font-medium text-gray-600">Active</p>
|
| 71 |
-
<p className="text-lg sm:text-2xl font-bold text-gray-900">
|
| 72 |
</div>
|
| 73 |
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-green-100 rounded-lg flex items-center justify-center">
|
| 74 |
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -82,7 +80,7 @@ const Accounts = () => {
|
|
| 82 |
<div className="flex items-center justify-between">
|
| 83 |
<div>
|
| 84 |
<p className="text-xs sm:text-sm font-medium text-gray-600">Primary</p>
|
| 85 |
-
<p className="text-lg sm:text-2xl font-bold text-gray-900">
|
| 86 |
</div>
|
| 87 |
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-purple-100 rounded-lg flex items-center justify-center">
|
| 88 |
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -96,7 +94,9 @@ const Accounts = () => {
|
|
| 96 |
<div className="flex items-center justify-between">
|
| 97 |
<div>
|
| 98 |
<p className="text-xs sm:text-sm font-medium text-gray-600">Connected</p>
|
| 99 |
-
<p className="text-lg sm:text-2xl font-bold text-gray-900">
|
|
|
|
|
|
|
| 100 |
</div>
|
| 101 |
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-orange-100 rounded-lg flex items-center justify-center">
|
| 102 |
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -109,7 +109,7 @@ const Accounts = () => {
|
|
| 109 |
</div>
|
| 110 |
|
| 111 |
{/* Error Display */}
|
| 112 |
-
{error && (
|
| 113 |
<div className="mb-6 sm:mb-8 animate-fade-in">
|
| 114 |
<div className="bg-red-50 border border-red-200 rounded-xl p-3 sm:p-4 flex items-start space-x-3">
|
| 115 |
<div className="flex-shrink-0">
|
|
@@ -118,14 +118,19 @@ const Accounts = () => {
|
|
| 118 |
</svg>
|
| 119 |
</div>
|
| 120 |
<div className="flex-1">
|
| 121 |
-
<p className="text-xs sm:text-sm font-medium text-red-800">
|
|
|
|
|
|
|
| 122 |
</div>
|
| 123 |
</div>
|
| 124 |
</div>
|
| 125 |
)}
|
| 126 |
|
| 127 |
<div className="accounts-content">
|
| 128 |
-
<LinkedInAccountsManager
|
|
|
|
|
|
|
|
|
|
| 129 |
</div>
|
| 130 |
</div>
|
| 131 |
</div>
|
|
|
|
| 1 |
import React, { useState, useEffect } from 'react';
|
| 2 |
import { useDispatch, useSelector } from 'react-redux';
|
| 3 |
+
import { fetchAccounts } from '../store/reducers/accountsSlice';
|
| 4 |
import { fetchLinkedInAccounts, deleteLinkedInAccount, setPrimaryLinkedInAccount, clearLinkedInError } from '../store/reducers/linkedinAccountsSlice';
|
| 5 |
import LinkedInAccountsManager from '../components/LinkedInAccount/LinkedInAccountsManager';
|
| 6 |
import { testApiStructure, testServiceBehavior } from '../debug/testApi';
|
| 7 |
|
| 8 |
const Accounts = () => {
|
| 9 |
const dispatch = useDispatch();
|
| 10 |
+
const { items: accounts, loading, error } = useSelector(state => state.accounts);
|
| 11 |
+
const { linkedinAccounts, loading: linkedinLoading, error: linkedinError } = useSelector(state => state.linkedinAccounts);
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
useEffect(() => {
|
| 14 |
+
// Fetch accounts from the main accounts slice
|
| 15 |
+
dispatch(fetchAccounts());
|
| 16 |
|
| 17 |
+
// Also fetch LinkedIn accounts for compatibility
|
| 18 |
dispatch(fetchLinkedInAccounts());
|
| 19 |
dispatch(clearLinkedInError());
|
| 20 |
|
| 21 |
// Run API tests
|
|
|
|
| 22 |
testApiStructure();
|
| 23 |
testServiceBehavior();
|
| 24 |
}, [dispatch]);
|
|
|
|
| 52 |
<div className="flex items-center justify-between">
|
| 53 |
<div>
|
| 54 |
<p className="text-xs sm:text-sm font-medium text-gray-600">Total Accounts</p>
|
| 55 |
+
<p className="text-lg sm:text-2xl font-bold text-gray-900">{accounts.length}</p>
|
| 56 |
</div>
|
| 57 |
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
| 58 |
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
| 66 |
<div className="flex items-center justify-between">
|
| 67 |
<div>
|
| 68 |
<p className="text-xs sm:text-sm font-medium text-gray-600">Active</p>
|
| 69 |
+
<p className="text-lg sm:text-2xl font-bold text-gray-900">{accounts.filter(account => account.token).length}</p>
|
| 70 |
</div>
|
| 71 |
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-green-100 rounded-lg flex items-center justify-center">
|
| 72 |
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
| 80 |
<div className="flex items-center justify-between">
|
| 81 |
<div>
|
| 82 |
<p className="text-xs sm:text-sm font-medium text-gray-600">Primary</p>
|
| 83 |
+
<p className="text-lg sm:text-2xl font-bold text-gray-900">{accounts.filter(account => account.is_primary).length}</p>
|
| 84 |
</div>
|
| 85 |
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-purple-100 rounded-lg flex items-center justify-center">
|
| 86 |
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
| 94 |
<div className="flex items-center justify-between">
|
| 95 |
<div>
|
| 96 |
<p className="text-xs sm:text-sm font-medium text-gray-600">Connected</p>
|
| 97 |
+
<p className="text-lg sm:text-2xl font-bold text-gray-900">
|
| 98 |
+
{accounts.length > 0 ? Math.round((accounts.filter(account => account.token).length / accounts.length) * 100) : 0}%
|
| 99 |
+
</p>
|
| 100 |
</div>
|
| 101 |
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-orange-100 rounded-lg flex items-center justify-center">
|
| 102 |
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
| 109 |
</div>
|
| 110 |
|
| 111 |
{/* Error Display */}
|
| 112 |
+
{(error || linkedinError) && (
|
| 113 |
<div className="mb-6 sm:mb-8 animate-fade-in">
|
| 114 |
<div className="bg-red-50 border border-red-200 rounded-xl p-3 sm:p-4 flex items-start space-x-3">
|
| 115 |
<div className="flex-shrink-0">
|
|
|
|
| 118 |
</svg>
|
| 119 |
</div>
|
| 120 |
<div className="flex-1">
|
| 121 |
+
<p className="text-xs sm:text-sm font-medium text-red-800">
|
| 122 |
+
{error || linkedinError}
|
| 123 |
+
</p>
|
| 124 |
</div>
|
| 125 |
</div>
|
| 126 |
</div>
|
| 127 |
)}
|
| 128 |
|
| 129 |
<div className="accounts-content">
|
| 130 |
+
<LinkedInAccountsManager
|
| 131 |
+
accounts={accounts.length > 0 ? accounts : linkedinAccounts}
|
| 132 |
+
loading={loading || linkedinLoading}
|
| 133 |
+
/>
|
| 134 |
</div>
|
| 135 |
</div>
|
| 136 |
</div>
|
frontend/src/pages/Home.jsx
CHANGED
|
@@ -236,53 +236,6 @@ const Home = () => {
|
|
| 236 |
</div>
|
| 237 |
</section>
|
| 238 |
|
| 239 |
-
{/* Testimonials Section */}
|
| 240 |
-
<section className="relative py-12 sm:py-16 lg:py-24 bg-gradient-to-br from-secondary-50 via-accent-50 to-primary-50">
|
| 241 |
-
{/* Background pattern */}
|
| 242 |
-
<div className="absolute inset-0 bg-gradient-to-r from-primary-50/10 via-accent-50/10 to-secondary-50/10"></div>
|
| 243 |
-
<div className="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxnIGZpbGw9IiM5MTAwMjkiIGZpbGwtb3BhY2l0eT0iMC4wMiI+PGNpcmNsZSBjeD0iMzAiIGN5PSIzMAIgcj0iMyIvPjwvZz48L2c+PC9zdmc+')] opacity-30"></div>
|
| 244 |
-
|
| 245 |
-
<div className="relative container mx-auto px-4 sm:px-6 lg:px-8">
|
| 246 |
-
{/* Section header */}
|
| 247 |
-
<div className="text-center mb-12 sm:mb-16 lg:mb-20">
|
| 248 |
-
<div className={`inline-flex items-center px-3 sm:px-4 py-2 bg-primary-100 text-primary-800 rounded-full text-xs sm:text-sm font-medium mb-4 sm:mb-6 transition-all duration-700 ${
|
| 249 |
-
isVisible ? 'animate-slide-up opacity-100' : 'opacity-0 translate-y-4'
|
| 250 |
-
}`} style={{ animationDelay: '0.1s' }}>
|
| 251 |
-
Trusted by Professionals
|
| 252 |
-
</div>
|
| 253 |
-
<h2 className={`text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 mb-4 sm:mb-6 transition-all duration-1000 ${
|
| 254 |
-
isVisible ? 'animate-slide-up opacity-100' : 'opacity-0 translate-y-8'
|
| 255 |
-
}`} style={{ animationDelay: '0.2s' }}>
|
| 256 |
-
Real Results from <span className="text-primary-600">Real Users</span>
|
| 257 |
-
</h2>
|
| 258 |
-
<p className={`text-lg sm:text-xl text-secondary-600 max-w-2xl sm:max-w-3xl mx-auto transition-all duration-1000 ${
|
| 259 |
-
isVisible ? 'animate-slide-up opacity-100' : 'opacity-0 translate-y-8'
|
| 260 |
-
}`} style={{ animationDelay: '0.3s' }}>
|
| 261 |
-
Join thousands of marketers, creators, and businesses who have transformed their LinkedIn presence with Lin.
|
| 262 |
-
</p>
|
| 263 |
-
</div>
|
| 264 |
-
|
| 265 |
-
{/* Testimonials grid */}
|
| 266 |
-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 lg:gap-8">
|
| 267 |
-
<Suspense fallback={<div className="col-span-full flex items-center justify-center py-8">Loading...</div>}>
|
| 268 |
-
{testimonials.map((testimonial, index) => (
|
| 269 |
-
<TestimonialCard
|
| 270 |
-
key={index}
|
| 271 |
-
avatar={testimonial.avatar}
|
| 272 |
-
name={testimonial.name}
|
| 273 |
-
role={testimonial.role}
|
| 274 |
-
company={testimonial.company}
|
| 275 |
-
content={testimonial.content}
|
| 276 |
-
rating={testimonial.rating}
|
| 277 |
-
delay={`${index * 150}ms`}
|
| 278 |
-
isVisible={isVisible}
|
| 279 |
-
/>
|
| 280 |
-
))}
|
| 281 |
-
</Suspense>
|
| 282 |
-
</div>
|
| 283 |
-
</div>
|
| 284 |
-
</section>
|
| 285 |
-
|
| 286 |
{/* Enhanced CTA Section */}
|
| 287 |
<section className="relative py-12 sm:py-16 lg:py-24 bg-gradient-to-r from-primary-600 via-primary-700 to-primary-800 overflow-hidden">
|
| 288 |
{/* Background effects */}
|
|
|
|
| 236 |
</div>
|
| 237 |
</section>
|
| 238 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
{/* Enhanced CTA Section */}
|
| 240 |
<section className="relative py-12 sm:py-16 lg:py-24 bg-gradient-to-r from-primary-600 via-primary-700 to-primary-800 overflow-hidden">
|
| 241 |
{/* Background effects */}
|
frontend/src/pages/Login.jsx
CHANGED
|
@@ -279,43 +279,6 @@ const Login = () => {
|
|
| 279 |
</button>
|
| 280 |
</form>
|
| 281 |
|
| 282 |
-
{/* Divider */}
|
| 283 |
-
<div className="relative my-4 sm:my-6">
|
| 284 |
-
<div className="absolute inset-0 flex items-center">
|
| 285 |
-
<div className="w-full border-t border-gray-300"></div>
|
| 286 |
-
</div>
|
| 287 |
-
<div className="relative flex justify-center text-xs sm:text-sm">
|
| 288 |
-
<span className="px-2 bg-white text-gray-500">Or continue with</span>
|
| 289 |
-
</div>
|
| 290 |
-
</div>
|
| 291 |
-
|
| 292 |
-
{/* Social Login Buttons */}
|
| 293 |
-
<div className="grid grid-cols-2 gap-2 sm:gap-3">
|
| 294 |
-
<button
|
| 295 |
-
type="button"
|
| 296 |
-
className="w-full flex items-center justify-center px-3 sm:px-4 py-2.5 sm:py-2 border border-gray-300 rounded-xl bg-white text-xs sm:text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 touch-manipulation active:scale-95"
|
| 297 |
-
aria-label="Sign in with Google"
|
| 298 |
-
>
|
| 299 |
-
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="currentColor" viewBox="0 0 24 24">
|
| 300 |
-
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
| 301 |
-
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
| 302 |
-
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
| 303 |
-
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
| 304 |
-
</svg>
|
| 305 |
-
<span className="ml-2 text-xs sm:text-sm">Google</span>
|
| 306 |
-
</button>
|
| 307 |
-
|
| 308 |
-
<button
|
| 309 |
-
type="button"
|
| 310 |
-
className="w-full flex items-center justify-center px-3 sm:px-4 py-2.5 sm:py-2 border border-gray-300 rounded-xl bg-white text-xs sm:text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 touch-manipulation active:scale-95"
|
| 311 |
-
aria-label="Sign in with LinkedIn"
|
| 312 |
-
>
|
| 313 |
-
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="currentColor" viewBox="0 0 24 24">
|
| 314 |
-
<path d="M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.024-.105-.949-.199-2.403.041-3.439.219-.937 1.219-5.175 1.219-5.175s-.311-.623-.311-1.543c0-1.446.839-2.526 1.885-2.526.888 0 1.318.666 1.318 1.466 0 .893-.568 2.229-.861 3.467-.245 1.04.52 1.888 1.546 1.888 1.856 0 3.283-1.958 3.283-4.789 0-2.503-1.799-4.253-4.37-4.253-2.977 0-4.727 2.234-4.727 4.546 0 .9.347 1.863.781 2.387.085.104.098.195.072.301-.079.329-.254 1.037-.289 1.183-.047.196-.153.238-.353.144-1.314-.612-2.137-2.536-2.137-4.078 0-3.298 2.394-6.325 6.901-6.325 3.628 0 6.44 2.586 6.44 6.043 0 3.607-2.274 6.505-5.431 6.505-1.06 0-2.057-.552-2.396-1.209 0 0-.523 1.992-.65 2.479-.235.9-.871 2.028-1.297 2.717.976.301 2.018.461 3.096.461 6.624 0 11.99-5.367 11.99-11.987C24.007 5.367 18.641.001 12.017.001z"/>
|
| 315 |
-
</svg>
|
| 316 |
-
<span className="ml-2 text-xs sm:text-sm">LinkedIn</span>
|
| 317 |
-
</button>
|
| 318 |
-
</div>
|
| 319 |
|
| 320 |
{/* Register Link */}
|
| 321 |
<div className="text-center">
|
|
|
|
| 279 |
</button>
|
| 280 |
</form>
|
| 281 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
|
| 283 |
{/* Register Link */}
|
| 284 |
<div className="text-center">
|
frontend/src/pages/Posts.jsx
CHANGED
|
@@ -61,21 +61,21 @@ const Posts = () => {
|
|
| 61 |
return;
|
| 62 |
}
|
| 63 |
|
| 64 |
-
console.log('π [Posts] Publishing post directly to LinkedIn
|
| 65 |
|
| 66 |
setIsCreating(true);
|
| 67 |
|
| 68 |
try {
|
| 69 |
-
//
|
| 70 |
console.log('π [Posts] Publishing to LinkedIn');
|
| 71 |
-
await dispatch(publishPostDirect({
|
| 72 |
social_account_id: selectedAccount,
|
| 73 |
text_content: postContent
|
| 74 |
})).unwrap();
|
| 75 |
|
| 76 |
-
console.log('π [Posts] Published to LinkedIn successfully');
|
| 77 |
|
| 78 |
-
//
|
| 79 |
console.log('π [Posts] Saving post to database as published');
|
| 80 |
await dispatch(createPost({
|
| 81 |
social_account_id: selectedAccount,
|
|
@@ -89,7 +89,8 @@ const Posts = () => {
|
|
| 89 |
setSelectedAccount('');
|
| 90 |
setPostContent('');
|
| 91 |
} catch (err) {
|
| 92 |
-
console.error('π [Posts] Failed to publish
|
|
|
|
| 93 |
} finally {
|
| 94 |
setIsCreating(false);
|
| 95 |
}
|
|
@@ -302,14 +303,14 @@ const Posts = () => {
|
|
| 302 |
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
| 303 |
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
| 304 |
</svg>
|
| 305 |
-
<span className="text-xs sm:text-sm">
|
| 306 |
</>
|
| 307 |
) : (
|
| 308 |
<>
|
| 309 |
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 310 |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
| 311 |
</svg>
|
| 312 |
-
<span className="text-xs sm:text-sm">
|
| 313 |
</>
|
| 314 |
)}
|
| 315 |
</button>
|
|
|
|
| 61 |
return;
|
| 62 |
}
|
| 63 |
|
| 64 |
+
console.log('π [Posts] Publishing post directly to LinkedIn:', { selectedAccount, postContentLength: postContent.length });
|
| 65 |
|
| 66 |
setIsCreating(true);
|
| 67 |
|
| 68 |
try {
|
| 69 |
+
// Publish directly to LinkedIn
|
| 70 |
console.log('π [Posts] Publishing to LinkedIn');
|
| 71 |
+
const publishResult = await dispatch(publishPostDirect({
|
| 72 |
social_account_id: selectedAccount,
|
| 73 |
text_content: postContent
|
| 74 |
})).unwrap();
|
| 75 |
|
| 76 |
+
console.log('π [Posts] Published to LinkedIn successfully:', publishResult);
|
| 77 |
|
| 78 |
+
// Only save to database if LinkedIn publish was successful
|
| 79 |
console.log('π [Posts] Saving post to database as published');
|
| 80 |
await dispatch(createPost({
|
| 81 |
social_account_id: selectedAccount,
|
|
|
|
| 89 |
setSelectedAccount('');
|
| 90 |
setPostContent('');
|
| 91 |
} catch (err) {
|
| 92 |
+
console.error('π [Posts] Failed to publish post:', err);
|
| 93 |
+
// Don't save to database if LinkedIn publish failed
|
| 94 |
} finally {
|
| 95 |
setIsCreating(false);
|
| 96 |
}
|
|
|
|
| 303 |
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
| 304 |
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
| 305 |
</svg>
|
| 306 |
+
<span className="text-xs sm:text-sm">Publishing...</span>
|
| 307 |
</>
|
| 308 |
) : (
|
| 309 |
<>
|
| 310 |
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 311 |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
| 312 |
</svg>
|
| 313 |
+
<span className="text-xs sm:text-sm">Publish</span>
|
| 314 |
</>
|
| 315 |
)}
|
| 316 |
</button>
|
frontend/src/pages/Register.jsx
CHANGED
|
@@ -75,6 +75,8 @@ const Register = () => {
|
|
| 75 |
});
|
| 76 |
};
|
| 77 |
|
|
|
|
|
|
|
| 78 |
const handleSubmit = async (e) => {
|
| 79 |
e.preventDefault();
|
| 80 |
|
|
@@ -91,8 +93,12 @@ const Register = () => {
|
|
| 91 |
|
| 92 |
try {
|
| 93 |
await dispatch(registerUser(formData)).unwrap();
|
| 94 |
-
//
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
} catch (err) {
|
| 97 |
// Error is handled by the Redux slice
|
| 98 |
console.error('Registration failed:', err);
|
|
@@ -130,8 +136,22 @@ const Register = () => {
|
|
| 130 |
|
| 131 |
{/* Auth Card */}
|
| 132 |
<div className="bg-white rounded-2xl shadow-xl p-4 sm:p-8 space-y-4 sm:space-y-6 animate-slide-up animate-delay-100">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
{/* Error Message */}
|
| 134 |
-
{error && (
|
| 135 |
<div className="bg-red-50 border border-red-200 rounded-lg p-3 sm:p-4 animate-slide-up animate-delay-200">
|
| 136 |
<div className="flex items-start space-x-2">
|
| 137 |
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-red-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
|
@@ -344,43 +364,6 @@ const Register = () => {
|
|
| 344 |
</button>
|
| 345 |
</form>
|
| 346 |
|
| 347 |
-
{/* Divider */}
|
| 348 |
-
<div className="relative my-4 sm:my-6">
|
| 349 |
-
<div className="absolute inset-0 flex items-center">
|
| 350 |
-
<div className="w-full border-t border-gray-300"></div>
|
| 351 |
-
</div>
|
| 352 |
-
<div className="relative flex justify-center text-xs sm:text-sm">
|
| 353 |
-
<span className="px-2 bg-white text-gray-500">Or continue with</span>
|
| 354 |
-
</div>
|
| 355 |
-
</div>
|
| 356 |
-
|
| 357 |
-
{/* Social Login Buttons */}
|
| 358 |
-
<div className="grid grid-cols-2 gap-2 sm:gap-3">
|
| 359 |
-
<button
|
| 360 |
-
type="button"
|
| 361 |
-
className="w-full flex items-center justify-center px-3 sm:px-4 py-2.5 sm:py-2 border border-gray-300 rounded-xl bg-white text-xs sm:text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 touch-manipulation active:scale-95"
|
| 362 |
-
aria-label="Sign up with Google"
|
| 363 |
-
>
|
| 364 |
-
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="currentColor" viewBox="0 0 24 24">
|
| 365 |
-
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
| 366 |
-
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
| 367 |
-
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
| 368 |
-
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
| 369 |
-
</svg>
|
| 370 |
-
<span className="ml-2 text-xs sm:text-sm">Google</span>
|
| 371 |
-
</button>
|
| 372 |
-
|
| 373 |
-
<button
|
| 374 |
-
type="button"
|
| 375 |
-
className="w-full flex items-center justify-center px-3 sm:px-4 py-2.5 sm:py-2 border border-gray-300 rounded-xl bg-white text-xs sm:text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 touch-manipulation active:scale-95"
|
| 376 |
-
aria-label="Sign up with LinkedIn"
|
| 377 |
-
>
|
| 378 |
-
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="currentColor" viewBox="0 0 24 24">
|
| 379 |
-
<path d="M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.024-.105-.949-.199-2.403.041-3.439.219-.937 1.219-5.175 1.219-5.175s-.311-.623-.311-1.543c0-1.446.839-2.526 1.885-2.526.888 0 1.318.666 1.318 1.466 0 .893-.568 2.229-.861 3.467-.245 1.04.52 1.888 1.546 1.888 1.856 0 3.283-1.958 3.283-4.789 0-2.503-1.799-4.253-4.37-4.253-2.977 0-4.727 2.234-4.727 4.546 0 .9.347 1.863.781 2.387.085.104.098.195.072.301-.079.329-.254 1.037-.289 1.183-.047.196-.153.238-.353.144-1.314-.612-2.137-2.536-2.137-4.078 0-3.298 2.394-6.325 6.901-6.325 3.628 0 6.44 2.586 6.44 6.043 0 3.607-2.274 6.505-5.431 6.505-1.06 0-2.057-.552-2.396-1.209 0 0-.523 1.992-.65 2.479-.235.9-.871 2.028-1.297 2.717.976.301 2.018.461 3.096.461 6.624 0 11.99-5.367 11.99-11.987C24.007 5.367 18.641.001 12.017.001z"/>
|
| 380 |
-
</svg>
|
| 381 |
-
<span className="ml-2 text-xs sm:text-sm">LinkedIn</span>
|
| 382 |
-
</button>
|
| 383 |
-
</div>
|
| 384 |
|
| 385 |
{/* Login Link */}
|
| 386 |
<div className="text-center">
|
|
|
|
| 75 |
});
|
| 76 |
};
|
| 77 |
|
| 78 |
+
const [showSuccess, setShowSuccess] = useState(false);
|
| 79 |
+
|
| 80 |
const handleSubmit = async (e) => {
|
| 81 |
e.preventDefault();
|
| 82 |
|
|
|
|
| 93 |
|
| 94 |
try {
|
| 95 |
await dispatch(registerUser(formData)).unwrap();
|
| 96 |
+
// Show success message
|
| 97 |
+
setShowSuccess(true);
|
| 98 |
+
// After successful registration, redirect to login after 3 seconds
|
| 99 |
+
setTimeout(() => {
|
| 100 |
+
navigate('/login');
|
| 101 |
+
}, 3000);
|
| 102 |
} catch (err) {
|
| 103 |
// Error is handled by the Redux slice
|
| 104 |
console.error('Registration failed:', err);
|
|
|
|
| 136 |
|
| 137 |
{/* Auth Card */}
|
| 138 |
<div className="bg-white rounded-2xl shadow-xl p-4 sm:p-8 space-y-4 sm:space-y-6 animate-slide-up animate-delay-100">
|
| 139 |
+
{/* Success Message */}
|
| 140 |
+
{showSuccess && (
|
| 141 |
+
<div className="bg-green-50 border border-green-200 rounded-lg p-3 sm:p-4 animate-slide-up animate-delay-200">
|
| 142 |
+
<div className="flex items-start space-x-2">
|
| 143 |
+
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-green-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
| 144 |
+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
| 145 |
+
</svg>
|
| 146 |
+
<span className="text-green-700 text-xs sm:text-sm font-medium">
|
| 147 |
+
Account created successfully! Please check your email for a confirmation link to activate your account.
|
| 148 |
+
</span>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
)}
|
| 152 |
+
|
| 153 |
{/* Error Message */}
|
| 154 |
+
{error && !showSuccess && (
|
| 155 |
<div className="bg-red-50 border border-red-200 rounded-lg p-3 sm:p-4 animate-slide-up animate-delay-200">
|
| 156 |
<div className="flex items-start space-x-2">
|
| 157 |
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-red-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
|
|
|
| 364 |
</button>
|
| 365 |
</form>
|
| 366 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
|
| 368 |
{/* Login Link */}
|
| 369 |
<div className="text-center">
|
frontend/src/services/accountService.js
CHANGED
|
@@ -9,15 +9,9 @@ class AccountService {
|
|
| 9 |
try {
|
| 10 |
const response = await apiClient.get('/accounts');
|
| 11 |
|
| 12 |
-
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 13 |
-
console.log('π± [Account] Retrieved accounts:', response.data);
|
| 14 |
-
}
|
| 15 |
|
| 16 |
return response;
|
| 17 |
} catch (error) {
|
| 18 |
-
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 19 |
-
console.error('π± [Account] Get accounts error:', error.response?.data || error.message);
|
| 20 |
-
}
|
| 21 |
throw error;
|
| 22 |
}
|
| 23 |
}
|
|
@@ -42,9 +36,6 @@ class AccountService {
|
|
| 42 |
|
| 43 |
return response;
|
| 44 |
} catch (error) {
|
| 45 |
-
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 46 |
-
console.error('π± [Account] Create account error:', error.response?.data || error.message);
|
| 47 |
-
}
|
| 48 |
throw error;
|
| 49 |
}
|
| 50 |
}
|
|
@@ -58,15 +49,9 @@ class AccountService {
|
|
| 58 |
try {
|
| 59 |
const response = await apiClient.delete(`/accounts/${accountId}`);
|
| 60 |
|
| 61 |
-
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 62 |
-
console.log('π± [Account] Account deleted:', response.data);
|
| 63 |
-
}
|
| 64 |
|
| 65 |
return response;
|
| 66 |
} catch (error) {
|
| 67 |
-
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 68 |
-
console.error('π± [Account] Delete account error:', error.response?.data || error.message);
|
| 69 |
-
}
|
| 70 |
throw error;
|
| 71 |
}
|
| 72 |
}
|
|
@@ -87,15 +72,9 @@ class AccountService {
|
|
| 87 |
social_network: callbackData.social_network
|
| 88 |
});
|
| 89 |
|
| 90 |
-
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 91 |
-
console.log('π± [Account] OAuth callback handled:', response.data);
|
| 92 |
-
}
|
| 93 |
|
| 94 |
return response;
|
| 95 |
} catch (error) {
|
| 96 |
-
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 97 |
-
console.error('π± [Account] OAuth callback error:', error.response?.data || error.message);
|
| 98 |
-
}
|
| 99 |
throw error;
|
| 100 |
}
|
| 101 |
}
|
|
|
|
| 9 |
try {
|
| 10 |
const response = await apiClient.get('/accounts');
|
| 11 |
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
return response;
|
| 14 |
} catch (error) {
|
|
|
|
|
|
|
|
|
|
| 15 |
throw error;
|
| 16 |
}
|
| 17 |
}
|
|
|
|
| 36 |
|
| 37 |
return response;
|
| 38 |
} catch (error) {
|
|
|
|
|
|
|
|
|
|
| 39 |
throw error;
|
| 40 |
}
|
| 41 |
}
|
|
|
|
| 49 |
try {
|
| 50 |
const response = await apiClient.delete(`/accounts/${accountId}`);
|
| 51 |
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
return response;
|
| 54 |
} catch (error) {
|
|
|
|
|
|
|
|
|
|
| 55 |
throw error;
|
| 56 |
}
|
| 57 |
}
|
|
|
|
| 72 |
social_network: callbackData.social_network
|
| 73 |
});
|
| 74 |
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
return response;
|
| 77 |
} catch (error) {
|
|
|
|
|
|
|
|
|
|
| 78 |
throw error;
|
| 79 |
}
|
| 80 |
}
|
frontend/src/services/authService.js
CHANGED
|
@@ -15,9 +15,7 @@ class AuthService {
|
|
| 15 |
*/
|
| 16 |
async register(userData) {
|
| 17 |
try {
|
| 18 |
-
console.log('AuthService: Registering user', userData);
|
| 19 |
const response = await apiClient.post('/auth/register', userData);
|
| 20 |
-
console.log('AuthService: Registration response', response.data);
|
| 21 |
return response;
|
| 22 |
} catch (error) {
|
| 23 |
console.error('AuthService: Registration error', error);
|
|
@@ -39,9 +37,7 @@ class AuthService {
|
|
| 39 |
*/
|
| 40 |
async login(credentials) {
|
| 41 |
try {
|
| 42 |
-
console.log('AuthService: Logging in user', credentials);
|
| 43 |
const response = await apiClient.post('/auth/login', credentials);
|
| 44 |
-
console.log('AuthService: Login response', response.data);
|
| 45 |
return response;
|
| 46 |
} catch (error) {
|
| 47 |
console.error('AuthService: Login error', error);
|
|
@@ -59,12 +55,10 @@ class AuthService {
|
|
| 59 |
*/
|
| 60 |
async logout() {
|
| 61 |
try {
|
| 62 |
-
console.log('AuthService: Logging out user');
|
| 63 |
// For logout, we don't need to send any data
|
| 64 |
// We just need to clear the token on the client side
|
| 65 |
// The server will handle token invalidation if needed
|
| 66 |
const response = await apiClient.post('/auth/logout');
|
| 67 |
-
console.log('AuthService: Logout response', response.data);
|
| 68 |
return response;
|
| 69 |
} catch (error) {
|
| 70 |
console.warn('AuthService: Logout error (continuing anyway):', error.message);
|
|
@@ -80,9 +74,7 @@ class AuthService {
|
|
| 80 |
*/
|
| 81 |
async getCurrentUser() {
|
| 82 |
try {
|
| 83 |
-
console.log('AuthService: Getting current user');
|
| 84 |
const response = await apiClient.get('/auth/user');
|
| 85 |
-
console.log('AuthService: Get user response', response.data);
|
| 86 |
return response;
|
| 87 |
} catch (error) {
|
| 88 |
console.error('AuthService: Get user error', error);
|
|
|
|
| 15 |
*/
|
| 16 |
async register(userData) {
|
| 17 |
try {
|
|
|
|
| 18 |
const response = await apiClient.post('/auth/register', userData);
|
|
|
|
| 19 |
return response;
|
| 20 |
} catch (error) {
|
| 21 |
console.error('AuthService: Registration error', error);
|
|
|
|
| 37 |
*/
|
| 38 |
async login(credentials) {
|
| 39 |
try {
|
|
|
|
| 40 |
const response = await apiClient.post('/auth/login', credentials);
|
|
|
|
| 41 |
return response;
|
| 42 |
} catch (error) {
|
| 43 |
console.error('AuthService: Login error', error);
|
|
|
|
| 55 |
*/
|
| 56 |
async logout() {
|
| 57 |
try {
|
|
|
|
| 58 |
// For logout, we don't need to send any data
|
| 59 |
// We just need to clear the token on the client side
|
| 60 |
// The server will handle token invalidation if needed
|
| 61 |
const response = await apiClient.post('/auth/logout');
|
|
|
|
| 62 |
return response;
|
| 63 |
} catch (error) {
|
| 64 |
console.warn('AuthService: Logout error (continuing anyway):', error.message);
|
|
|
|
| 74 |
*/
|
| 75 |
async getCurrentUser() {
|
| 76 |
try {
|
|
|
|
| 77 |
const response = await apiClient.get('/auth/user');
|
|
|
|
| 78 |
return response;
|
| 79 |
} catch (error) {
|
| 80 |
console.error('AuthService: Get user error', error);
|
frontend/src/services/linkedinAuthService.js
CHANGED
|
@@ -13,9 +13,6 @@ class LinkedInAuthService {
|
|
| 13 |
});
|
| 14 |
|
| 15 |
if (response.data.success && response.data.authorization_url) {
|
| 16 |
-
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 17 |
-
console.log('π [LinkedIn] Auth initiated successfully');
|
| 18 |
-
}
|
| 19 |
return {
|
| 20 |
success: true,
|
| 21 |
authorization_url: response.data.authorization_url,
|
|
@@ -25,9 +22,6 @@ class LinkedInAuthService {
|
|
| 25 |
throw new Error(response.data.message || 'Failed to initiate LinkedIn authentication');
|
| 26 |
}
|
| 27 |
} catch (error) {
|
| 28 |
-
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 29 |
-
console.error('π [LinkedIn] Auth initiation error:', error.response?.data || error.message);
|
| 30 |
-
}
|
| 31 |
return {
|
| 32 |
success: false,
|
| 33 |
message: error.response?.data?.message || 'Failed to start LinkedIn authentication'
|
|
@@ -43,32 +37,14 @@ class LinkedInAuthService {
|
|
| 43 |
*/
|
| 44 |
async handleCallback(code, state) {
|
| 45 |
try {
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
console.log('π [LinkedIn] Callback payload:', {
|
| 49 |
-
code: code.substring(0, 20) + '...',
|
| 50 |
state: state,
|
| 51 |
social_network: 'LinkedIn'
|
| 52 |
});
|
| 53 |
|
| 54 |
-
|
| 55 |
-
// We just need to return success to indicate the callback was processed
|
| 56 |
-
console.log('π [LinkedIn] Callback processing initiated by frontend handler');
|
| 57 |
-
|
| 58 |
-
return {
|
| 59 |
-
success: true,
|
| 60 |
-
message: 'OAuth callback processing initiated'
|
| 61 |
-
};
|
| 62 |
} catch (error) {
|
| 63 |
-
// DEBUG: Log callback error
|
| 64 |
-
console.error('π [LinkedIn] OAuth callback error:', error);
|
| 65 |
-
console.error('π [LinkedIn] Error details:', {
|
| 66 |
-
message: error.message,
|
| 67 |
-
response: error.response?.data,
|
| 68 |
-
status: error.response?.status,
|
| 69 |
-
headers: error.response?.headers
|
| 70 |
-
});
|
| 71 |
-
|
| 72 |
return {
|
| 73 |
success: false,
|
| 74 |
message: error.response?.data?.message || 'Failed to complete LinkedIn authentication'
|
|
@@ -82,10 +58,6 @@ class LinkedInAuthService {
|
|
| 82 |
*/
|
| 83 |
async getLinkedInAccounts() {
|
| 84 |
try {
|
| 85 |
-
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 86 |
-
console.log('π [LinkedIn] Fetching LinkedIn accounts...');
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
const response = await apiClient.get('/accounts');
|
| 90 |
|
| 91 |
// Handle different response formats
|
|
@@ -106,15 +78,8 @@ class LinkedInAuthService {
|
|
| 106 |
account => account && account.social_network && account.social_network.toLowerCase() === 'linkedin'
|
| 107 |
);
|
| 108 |
|
| 109 |
-
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 110 |
-
console.log('π [LinkedIn] Retrieved LinkedIn accounts:', linkedinAccounts);
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
return { success: true, accounts: linkedinAccounts };
|
| 114 |
} catch (error) {
|
| 115 |
-
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 116 |
-
console.error('π [LinkedIn] Get LinkedIn accounts error:', error.response?.data || error.message);
|
| 117 |
-
}
|
| 118 |
return { success: false, accounts: [], message: error.response?.data?.message || 'Failed to fetch LinkedIn accounts' };
|
| 119 |
}
|
| 120 |
}
|
|
@@ -128,15 +93,8 @@ class LinkedInAuthService {
|
|
| 128 |
try {
|
| 129 |
const response = await apiClient.delete(`/accounts/${accountId}`);
|
| 130 |
|
| 131 |
-
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 132 |
-
console.log('π [LinkedIn] LinkedIn account deleted successfully');
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
return response.data;
|
| 136 |
} catch (error) {
|
| 137 |
-
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 138 |
-
console.error('π [LinkedIn] Delete LinkedIn account error:', error.response?.data || error.message);
|
| 139 |
-
}
|
| 140 |
return {
|
| 141 |
success: false,
|
| 142 |
message: error.response?.data?.message || 'Failed to delete LinkedIn account'
|
|
@@ -153,15 +111,8 @@ class LinkedInAuthService {
|
|
| 153 |
try {
|
| 154 |
const response = await apiClient.put(`/accounts/${accountId}/primary`);
|
| 155 |
|
| 156 |
-
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 157 |
-
console.log('π [LinkedIn] Primary account set successfully');
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
return response.data;
|
| 161 |
} catch (error) {
|
| 162 |
-
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 163 |
-
console.error('π [LinkedIn] Set primary account error:', error.response?.data || error.message);
|
| 164 |
-
}
|
| 165 |
return {
|
| 166 |
success: false,
|
| 167 |
message: error.response?.data?.message || 'Failed to set primary account'
|
|
|
|
| 13 |
});
|
| 14 |
|
| 15 |
if (response.data.success && response.data.authorization_url) {
|
|
|
|
|
|
|
|
|
|
| 16 |
return {
|
| 17 |
success: true,
|
| 18 |
authorization_url: response.data.authorization_url,
|
|
|
|
| 22 |
throw new Error(response.data.message || 'Failed to initiate LinkedIn authentication');
|
| 23 |
}
|
| 24 |
} catch (error) {
|
|
|
|
|
|
|
|
|
|
| 25 |
return {
|
| 26 |
success: false,
|
| 27 |
message: error.response?.data?.message || 'Failed to start LinkedIn authentication'
|
|
|
|
| 37 |
*/
|
| 38 |
async handleCallback(code, state) {
|
| 39 |
try {
|
| 40 |
+
const response = await apiClient.post('/accounts/callback', {
|
| 41 |
+
code: code,
|
|
|
|
|
|
|
| 42 |
state: state,
|
| 43 |
social_network: 'LinkedIn'
|
| 44 |
});
|
| 45 |
|
| 46 |
+
return response.data;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
} catch (error) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
return {
|
| 49 |
success: false,
|
| 50 |
message: error.response?.data?.message || 'Failed to complete LinkedIn authentication'
|
|
|
|
| 58 |
*/
|
| 59 |
async getLinkedInAccounts() {
|
| 60 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
const response = await apiClient.get('/accounts');
|
| 62 |
|
| 63 |
// Handle different response formats
|
|
|
|
| 78 |
account => account && account.social_network && account.social_network.toLowerCase() === 'linkedin'
|
| 79 |
);
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
return { success: true, accounts: linkedinAccounts };
|
| 82 |
} catch (error) {
|
|
|
|
|
|
|
|
|
|
| 83 |
return { success: false, accounts: [], message: error.response?.data?.message || 'Failed to fetch LinkedIn accounts' };
|
| 84 |
}
|
| 85 |
}
|
|
|
|
| 93 |
try {
|
| 94 |
const response = await apiClient.delete(`/accounts/${accountId}`);
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
return response.data;
|
| 97 |
} catch (error) {
|
|
|
|
|
|
|
|
|
|
| 98 |
return {
|
| 99 |
success: false,
|
| 100 |
message: error.response?.data?.message || 'Failed to delete LinkedIn account'
|
|
|
|
| 111 |
try {
|
| 112 |
const response = await apiClient.put(`/accounts/${accountId}/primary`);
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
return response.data;
|
| 115 |
} catch (error) {
|
|
|
|
|
|
|
|
|
|
| 116 |
return {
|
| 117 |
success: false,
|
| 118 |
message: error.response?.data?.message || 'Failed to set primary account'
|
frontend/src/store/reducers/authSlice.js
CHANGED
|
@@ -392,7 +392,7 @@ const authSlice = createSlice({
|
|
| 392 |
state.isAuthenticated = false;
|
| 393 |
})
|
| 394 |
|
| 395 |
-
// Register user (
|
| 396 |
.addCase(registerUser.pending, (state) => {
|
| 397 |
state.loading = 'pending';
|
| 398 |
state.error = null;
|
|
@@ -400,11 +400,41 @@ const authSlice = createSlice({
|
|
| 400 |
.addCase(registerUser.fulfilled, (state, action) => {
|
| 401 |
state.loading = 'succeeded';
|
| 402 |
state.user = action.payload.user;
|
| 403 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
})
|
| 405 |
.addCase(registerUser.rejected, (state, action) => {
|
| 406 |
state.loading = 'failed';
|
| 407 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
})
|
| 409 |
|
| 410 |
// Login user (enhanced)
|
|
@@ -424,8 +454,31 @@ const authSlice = createSlice({
|
|
| 424 |
})
|
| 425 |
.addCase(loginUser.rejected, (state, action) => {
|
| 426 |
state.loading = 'failed';
|
| 427 |
-
state.error = action.payload?.message || 'Login failed';
|
| 428 |
state.security.failedAttempts += 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
})
|
| 430 |
|
| 431 |
// Logout user (enhanced)
|
|
|
|
| 392 |
state.isAuthenticated = false;
|
| 393 |
})
|
| 394 |
|
| 395 |
+
// Register user (enhanced)
|
| 396 |
.addCase(registerUser.pending, (state) => {
|
| 397 |
state.loading = 'pending';
|
| 398 |
state.error = null;
|
|
|
|
| 400 |
.addCase(registerUser.fulfilled, (state, action) => {
|
| 401 |
state.loading = 'succeeded';
|
| 402 |
state.user = action.payload.user;
|
| 403 |
+
|
| 404 |
+
// Check if email confirmation is required
|
| 405 |
+
if (action.payload.requires_confirmation) {
|
| 406 |
+
state.isAuthenticated = false;
|
| 407 |
+
state.error = null; // Don't show error, show success message instead
|
| 408 |
+
} else {
|
| 409 |
+
state.isAuthenticated = true;
|
| 410 |
+
}
|
| 411 |
})
|
| 412 |
.addCase(registerUser.rejected, (state, action) => {
|
| 413 |
state.loading = 'failed';
|
| 414 |
+
|
| 415 |
+
// Handle different error types with specific messages
|
| 416 |
+
const errorPayload = action.payload;
|
| 417 |
+
let errorMessage = 'Registration failed';
|
| 418 |
+
|
| 419 |
+
if (errorPayload) {
|
| 420 |
+
if (errorPayload.message) {
|
| 421 |
+
// Check for specific error types
|
| 422 |
+
const errorMsg = errorPayload.message.toLowerCase();
|
| 423 |
+
if (errorMsg.includes('already registered') || errorMsg.includes('already exists')) {
|
| 424 |
+
errorMessage = 'An account with this email already exists. Please login instead or use a different email.';
|
| 425 |
+
} else if (errorMsg.includes('invalid email')) {
|
| 426 |
+
errorMessage = 'Please enter a valid email address.';
|
| 427 |
+
} else if (errorMsg.includes('password')) {
|
| 428 |
+
errorMessage = 'Password does not meet requirements. Please use at least 8 characters.';
|
| 429 |
+
} else {
|
| 430 |
+
errorMessage = errorPayload.message;
|
| 431 |
+
}
|
| 432 |
+
} else if (typeof errorPayload === 'string') {
|
| 433 |
+
errorMessage = errorPayload;
|
| 434 |
+
}
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
state.error = errorMessage;
|
| 438 |
})
|
| 439 |
|
| 440 |
// Login user (enhanced)
|
|
|
|
| 454 |
})
|
| 455 |
.addCase(loginUser.rejected, (state, action) => {
|
| 456 |
state.loading = 'failed';
|
|
|
|
| 457 |
state.security.failedAttempts += 1;
|
| 458 |
+
|
| 459 |
+
// Handle different error types with specific messages
|
| 460 |
+
const errorPayload = action.payload;
|
| 461 |
+
let errorMessage = 'Login failed';
|
| 462 |
+
|
| 463 |
+
if (errorPayload) {
|
| 464 |
+
if (errorPayload.message) {
|
| 465 |
+
// Check for specific error types
|
| 466 |
+
const errorMsg = errorPayload.message.toLowerCase();
|
| 467 |
+
if (errorMsg.includes('email not confirmed') || errorMsg.includes('email not verified') || errorPayload.requires_confirmation) {
|
| 468 |
+
errorMessage = 'Please verify your email before logging in. Check your inbox for the confirmation email.';
|
| 469 |
+
} else if (errorMsg.includes('invalid credentials') || errorMsg.includes('invalid email') || errorMsg.includes('invalid password')) {
|
| 470 |
+
errorMessage = 'Email or password is incorrect. Please try again.';
|
| 471 |
+
} else if (errorMsg.includes('user not found')) {
|
| 472 |
+
errorMessage = 'No account found with this email. Please check your email or register for a new account.';
|
| 473 |
+
} else {
|
| 474 |
+
errorMessage = errorPayload.message;
|
| 475 |
+
}
|
| 476 |
+
} else if (typeof errorPayload === 'string') {
|
| 477 |
+
errorMessage = errorPayload;
|
| 478 |
+
}
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
state.error = errorMessage;
|
| 482 |
})
|
| 483 |
|
| 484 |
// Logout user (enhanced)
|
frontend/src/store/reducers/linkedinAccountsSlice.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
| 2 |
import linkedinAuthService from '../../services/linkedinAuthService';
|
| 3 |
|
|
@@ -20,9 +21,6 @@ export const fetchLinkedInAccounts = createAsyncThunk(
|
|
| 20 |
async (_, { rejectWithValue }) => {
|
| 21 |
try {
|
| 22 |
const response = await linkedinAuthService.getLinkedInAccounts();
|
| 23 |
-
console.log('π [DEBUG] Async thunk - Service response:', response);
|
| 24 |
-
console.log('π [DEBUG] Async thunk - Service response type:', typeof response);
|
| 25 |
-
console.log('π [DEBUG] Async thunk - Service response is array:', Array.isArray(response));
|
| 26 |
|
| 27 |
// Return the response directly, not response.data
|
| 28 |
return response;
|
|
@@ -38,7 +36,6 @@ export const initiateLinkedInAuth = createAsyncThunk(
|
|
| 38 |
async (_, { rejectWithValue }) => {
|
| 39 |
try {
|
| 40 |
const response = await linkedinAuthService.initiateAuth();
|
| 41 |
-
console.log('π [DEBUG] Initiate auth - Service response:', response);
|
| 42 |
return response;
|
| 43 |
} catch (error) {
|
| 44 |
console.error('π [DEBUG] Initiate auth - Error:', error);
|
|
@@ -52,7 +49,6 @@ export const handleLinkedInCallback = createAsyncThunk(
|
|
| 52 |
async ({ code, state }, { rejectWithValue }) => {
|
| 53 |
try {
|
| 54 |
const response = await linkedinAuthService.handleCallback(code, state);
|
| 55 |
-
console.log('π [DEBUG] Handle callback - Service response:', response);
|
| 56 |
return response;
|
| 57 |
} catch (error) {
|
| 58 |
console.error('π [DEBUG] Handle callback - Error:', error);
|
|
@@ -66,7 +62,6 @@ export const deleteLinkedInAccount = createAsyncThunk(
|
|
| 66 |
async (accountId, { rejectWithValue }) => {
|
| 67 |
try {
|
| 68 |
const response = await linkedinAuthService.deleteLinkedInAccount(accountId);
|
| 69 |
-
console.log('π [DEBUG] Delete account - Service response:', response);
|
| 70 |
return { accountId, ...response };
|
| 71 |
} catch (error) {
|
| 72 |
console.error('π [DEBUG] Delete account - Error:', error);
|
|
@@ -80,10 +75,8 @@ export const setPrimaryLinkedInAccount = createAsyncThunk(
|
|
| 80 |
async (accountId, { rejectWithValue }) => {
|
| 81 |
try {
|
| 82 |
const response = await linkedinAuthService.setPrimaryAccount(accountId);
|
| 83 |
-
console.log('π [DEBUG] Set primary - Service response:', response);
|
| 84 |
return { accountId, ...response };
|
| 85 |
} catch (error) {
|
| 86 |
-
console.error('π [DEBUG] Set primary - Error:', error);
|
| 87 |
return rejectWithValue(error.message);
|
| 88 |
}
|
| 89 |
}
|
|
@@ -117,29 +110,20 @@ const linkedinAccountsSlice = createSlice({
|
|
| 117 |
// Fetch LinkedIn accounts
|
| 118 |
builder
|
| 119 |
.addCase(fetchLinkedInAccounts.pending, (state) => {
|
| 120 |
-
console.log('π [DEBUG] fetchLinkedInAccounts.pending - State:', state);
|
| 121 |
state.loading = true;
|
| 122 |
state.error = null;
|
| 123 |
})
|
| 124 |
.addCase(fetchLinkedInAccounts.fulfilled, (state, action) => {
|
| 125 |
-
console.log('π [DEBUG] fetchLinkedInAccounts.fulfilled - Action:', action);
|
| 126 |
-
console.log('π [DEBUG] fetchLinkedInAccounts.fulfilled - Action payload:', action.payload);
|
| 127 |
-
console.log('π [DEBUG] fetchLinkedInAccounts.fulfilled - Action payload type:', typeof action.payload);
|
| 128 |
-
console.log('π [DEBUG] fetchLinkedInAccounts.fulfilled - Action payload is array:', Array.isArray(action.payload));
|
| 129 |
-
console.log('π [DEBUG] fetchLinkedInAccounts.fulfilled - State before update:', state);
|
| 130 |
-
|
| 131 |
state.loading = false;
|
| 132 |
|
| 133 |
// Handle different response formats robustly
|
| 134 |
if (!action.payload) {
|
| 135 |
-
console.warn('π [DEBUG] fetchLinkedInAccounts.fulfilled - Empty payload');
|
| 136 |
state.linkedinAccounts = [];
|
| 137 |
return;
|
| 138 |
}
|
| 139 |
|
| 140 |
// If service returns error object, handle it
|
| 141 |
if (action.payload.success === false) {
|
| 142 |
-
console.error('π [DEBUG] fetchLinkedInAccounts.fulfilled - Service error:', action.payload.message);
|
| 143 |
state.error = action.payload.message;
|
| 144 |
state.linkedinAccounts = [];
|
| 145 |
return;
|
|
@@ -147,27 +131,20 @@ const linkedinAccountsSlice = createSlice({
|
|
| 147 |
|
| 148 |
// If service returns object with accounts property (expected format)
|
| 149 |
if (action.payload.accounts && Array.isArray(action.payload.accounts)) {
|
| 150 |
-
console.log('π [DEBUG] fetchLinkedInAccounts.fulfilled - Using accounts property format');
|
| 151 |
state.linkedinAccounts = action.payload.accounts;
|
| 152 |
return;
|
| 153 |
}
|
| 154 |
|
| 155 |
// If service returns array directly (fallback)
|
| 156 |
if (Array.isArray(action.payload)) {
|
| 157 |
-
console.log('π [DEBUG] fetchLinkedInAccounts.fulfilled - Using direct array format');
|
| 158 |
state.linkedinAccounts = action.payload;
|
| 159 |
return;
|
| 160 |
}
|
| 161 |
|
| 162 |
// Fallback to empty array
|
| 163 |
-
console.warn('π [DEBUG] fetchLinkedInAccounts.fulfilled - Unexpected payload format, using empty array');
|
| 164 |
state.linkedinAccounts = [];
|
| 165 |
-
|
| 166 |
-
console.log('π [DEBUG] fetchLinkedInAccounts.fulfilled - State after update:', state);
|
| 167 |
})
|
| 168 |
.addCase(fetchLinkedInAccounts.rejected, (state, action) => {
|
| 169 |
-
console.log('π [DEBUG] fetchLinkedInAccounts.rejected - Action:', action);
|
| 170 |
-
console.log('π [DEBUG] fetchLinkedInAccounts.rejected - Error:', action.payload);
|
| 171 |
state.loading = false;
|
| 172 |
state.error = action.payload;
|
| 173 |
})
|
|
|
|
| 1 |
+
|
| 2 |
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
| 3 |
import linkedinAuthService from '../../services/linkedinAuthService';
|
| 4 |
|
|
|
|
| 21 |
async (_, { rejectWithValue }) => {
|
| 22 |
try {
|
| 23 |
const response = await linkedinAuthService.getLinkedInAccounts();
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
// Return the response directly, not response.data
|
| 26 |
return response;
|
|
|
|
| 36 |
async (_, { rejectWithValue }) => {
|
| 37 |
try {
|
| 38 |
const response = await linkedinAuthService.initiateAuth();
|
|
|
|
| 39 |
return response;
|
| 40 |
} catch (error) {
|
| 41 |
console.error('π [DEBUG] Initiate auth - Error:', error);
|
|
|
|
| 49 |
async ({ code, state }, { rejectWithValue }) => {
|
| 50 |
try {
|
| 51 |
const response = await linkedinAuthService.handleCallback(code, state);
|
|
|
|
| 52 |
return response;
|
| 53 |
} catch (error) {
|
| 54 |
console.error('π [DEBUG] Handle callback - Error:', error);
|
|
|
|
| 62 |
async (accountId, { rejectWithValue }) => {
|
| 63 |
try {
|
| 64 |
const response = await linkedinAuthService.deleteLinkedInAccount(accountId);
|
|
|
|
| 65 |
return { accountId, ...response };
|
| 66 |
} catch (error) {
|
| 67 |
console.error('π [DEBUG] Delete account - Error:', error);
|
|
|
|
| 75 |
async (accountId, { rejectWithValue }) => {
|
| 76 |
try {
|
| 77 |
const response = await linkedinAuthService.setPrimaryAccount(accountId);
|
|
|
|
| 78 |
return { accountId, ...response };
|
| 79 |
} catch (error) {
|
|
|
|
| 80 |
return rejectWithValue(error.message);
|
| 81 |
}
|
| 82 |
}
|
|
|
|
| 110 |
// Fetch LinkedIn accounts
|
| 111 |
builder
|
| 112 |
.addCase(fetchLinkedInAccounts.pending, (state) => {
|
|
|
|
| 113 |
state.loading = true;
|
| 114 |
state.error = null;
|
| 115 |
})
|
| 116 |
.addCase(fetchLinkedInAccounts.fulfilled, (state, action) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
state.loading = false;
|
| 118 |
|
| 119 |
// Handle different response formats robustly
|
| 120 |
if (!action.payload) {
|
|
|
|
| 121 |
state.linkedinAccounts = [];
|
| 122 |
return;
|
| 123 |
}
|
| 124 |
|
| 125 |
// If service returns error object, handle it
|
| 126 |
if (action.payload.success === false) {
|
|
|
|
| 127 |
state.error = action.payload.message;
|
| 128 |
state.linkedinAccounts = [];
|
| 129 |
return;
|
|
|
|
| 131 |
|
| 132 |
// If service returns object with accounts property (expected format)
|
| 133 |
if (action.payload.accounts && Array.isArray(action.payload.accounts)) {
|
|
|
|
| 134 |
state.linkedinAccounts = action.payload.accounts;
|
| 135 |
return;
|
| 136 |
}
|
| 137 |
|
| 138 |
// If service returns array directly (fallback)
|
| 139 |
if (Array.isArray(action.payload)) {
|
|
|
|
| 140 |
state.linkedinAccounts = action.payload;
|
| 141 |
return;
|
| 142 |
}
|
| 143 |
|
| 144 |
// Fallback to empty array
|
|
|
|
| 145 |
state.linkedinAccounts = [];
|
|
|
|
|
|
|
| 146 |
})
|
| 147 |
.addCase(fetchLinkedInAccounts.rejected, (state, action) => {
|
|
|
|
|
|
|
| 148 |
state.loading = false;
|
| 149 |
state.error = action.payload;
|
| 150 |
})
|