feat(auth): add country and language support to user registration
Browse files- Linkedin_poster_dev +1 -1
- backend/api/auth.py +85 -40
- backend/api/posts.py +97 -86
- backend/config.py +16 -15
- backend/services/auth_service.py +44 -24
- backend/services/content_service.py +65 -2
- backend/utils/country_language_data.py +265 -0
- docu_code/My_data_base_schema_.txt +2 -0
- frontend/src/pages/Register.jsx +135 -35
- frontend/src/services/authService.js +3 -1
- frontend/src/store/reducers/authSlice.js +54 -52
Linkedin_poster_dev
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
Subproject commit
|
|
|
|
| 1 |
+
Subproject commit 09381cacceca11c302c5d8e056a8bd7b9121a09d
|
backend/api/auth.py
CHANGED
|
@@ -2,6 +2,7 @@ from flask import Blueprint, request, jsonify, current_app
|
|
| 2 |
from flask_jwt_extended import jwt_required, get_jwt_identity
|
| 3 |
from backend.services.auth_service import register_user, login_user, get_user_by_id, request_password_reset, reset_user_password
|
| 4 |
from backend.models.user import User
|
|
|
|
| 5 |
|
| 6 |
auth_bp = Blueprint('auth', __name__)
|
| 7 |
|
|
@@ -19,44 +20,67 @@ def handle_register_options():
|
|
| 19 |
def register():
|
| 20 |
"""
|
| 21 |
Register a new user.
|
| 22 |
-
|
| 23 |
Request Body:
|
| 24 |
email (str): User email
|
| 25 |
password (str): User password
|
| 26 |
-
|
|
|
|
|
|
|
| 27 |
Returns:
|
| 28 |
JSON: Registration result
|
| 29 |
"""
|
| 30 |
try:
|
| 31 |
data = request.get_json()
|
| 32 |
-
|
| 33 |
# Validate required fields
|
| 34 |
if not data or not all(k in data for k in ('email', 'password')):
|
| 35 |
return jsonify({
|
| 36 |
'success': False,
|
| 37 |
'message': 'Email and password are required'
|
| 38 |
}), 400
|
| 39 |
-
|
| 40 |
email = data['email']
|
| 41 |
password = data['password']
|
| 42 |
-
|
|
|
|
|
|
|
| 43 |
# Note: confirm_password validation is removed as Supabase handles password confirmation automatically
|
| 44 |
-
|
| 45 |
# Validate password length
|
| 46 |
if len(password) < 8:
|
| 47 |
return jsonify({
|
| 48 |
'success': False,
|
| 49 |
'message': 'Password must be at least 8 characters long'
|
| 50 |
}), 400
|
| 51 |
-
|
| 52 |
-
#
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
if result['success']:
|
| 56 |
return jsonify(result), 201
|
| 57 |
else:
|
| 58 |
return jsonify(result), 400
|
| 59 |
-
|
| 60 |
except Exception as e:
|
| 61 |
current_app.logger.error(f"Registration error: {str(e)}")
|
| 62 |
return jsonify({
|
|
@@ -76,12 +100,12 @@ def handle_login_options():
|
|
| 76 |
def login():
|
| 77 |
"""
|
| 78 |
Authenticate and login a user.
|
| 79 |
-
|
| 80 |
Request Body:
|
| 81 |
email (str): User email
|
| 82 |
password (str): User password
|
| 83 |
remember_me (bool): Remember me flag for extended session (optional)
|
| 84 |
-
|
| 85 |
Returns:
|
| 86 |
JSON: Login result with JWT token
|
| 87 |
"""
|
|
@@ -90,9 +114,9 @@ def login():
|
|
| 90 |
current_app.logger.info(f"Login request received from {request.remote_addr}")
|
| 91 |
current_app.logger.info(f"Request headers: {dict(request.headers)}")
|
| 92 |
current_app.logger.info(f"Request data: {request.get_json()}")
|
| 93 |
-
|
| 94 |
data = request.get_json()
|
| 95 |
-
|
| 96 |
# Validate required fields
|
| 97 |
if not data or not all(k in data for k in ('email', 'password')):
|
| 98 |
current_app.logger.warning("Login failed: Missing email or password")
|
|
@@ -100,14 +124,14 @@ def login():
|
|
| 100 |
'success': False,
|
| 101 |
'message': 'Email and password are required'
|
| 102 |
}), 400
|
| 103 |
-
|
| 104 |
email = data['email']
|
| 105 |
password = data['password']
|
| 106 |
remember_me = data.get('remember_me', False)
|
| 107 |
-
|
| 108 |
# Login user
|
| 109 |
result = login_user(email, password, remember_me)
|
| 110 |
-
|
| 111 |
if result['success']:
|
| 112 |
# Set CORS headers explicitly
|
| 113 |
response_data = jsonify(result)
|
|
@@ -118,7 +142,7 @@ def login():
|
|
| 118 |
else:
|
| 119 |
current_app.logger.warning(f"Login failed for user {email}: {result.get('message', 'Unknown error')}")
|
| 120 |
return jsonify(result), 401
|
| 121 |
-
|
| 122 |
except Exception as e:
|
| 123 |
current_app.logger.error(f"Login error: {str(e)}", exc_info=True)
|
| 124 |
return jsonify({
|
|
@@ -136,7 +160,7 @@ def handle_logout_options():
|
|
| 136 |
def logout():
|
| 137 |
"""
|
| 138 |
Logout current user.
|
| 139 |
-
|
| 140 |
Returns:
|
| 141 |
JSON: Logout result
|
| 142 |
"""
|
|
@@ -145,7 +169,7 @@ def logout():
|
|
| 145 |
'success': True,
|
| 146 |
'message': 'Logged out successfully'
|
| 147 |
}), 200
|
| 148 |
-
|
| 149 |
except Exception as e:
|
| 150 |
current_app.logger.error(f"Logout error: {str(e)}")
|
| 151 |
return jsonify({
|
|
@@ -163,14 +187,14 @@ def handle_user_options():
|
|
| 163 |
def get_current_user():
|
| 164 |
"""
|
| 165 |
Get current authenticated user.
|
| 166 |
-
|
| 167 |
Returns:
|
| 168 |
JSON: Current user data
|
| 169 |
"""
|
| 170 |
try:
|
| 171 |
user_id = get_jwt_identity()
|
| 172 |
user_data = get_user_by_id(user_id)
|
| 173 |
-
|
| 174 |
if user_data:
|
| 175 |
return jsonify({
|
| 176 |
'success': True,
|
|
@@ -181,7 +205,7 @@ def get_current_user():
|
|
| 181 |
'success': False,
|
| 182 |
'message': 'User not found'
|
| 183 |
}), 404
|
| 184 |
-
|
| 185 |
except Exception as e:
|
| 186 |
current_app.logger.error(f"Get user error: {str(e)}")
|
| 187 |
return jsonify({
|
|
@@ -189,6 +213,27 @@ def get_current_user():
|
|
| 189 |
'message': 'An error occurred while fetching user data'
|
| 190 |
}), 500
|
| 191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
@auth_bp.route('/forgot-password', methods=['OPTIONS'])
|
| 194 |
def handle_forgot_password_options():
|
|
@@ -200,33 +245,33 @@ def handle_forgot_password_options():
|
|
| 200 |
def forgot_password():
|
| 201 |
"""
|
| 202 |
Request password reset for a user.
|
| 203 |
-
|
| 204 |
Request Body:
|
| 205 |
email (str): User email
|
| 206 |
-
|
| 207 |
Returns:
|
| 208 |
JSON: Password reset request result
|
| 209 |
"""
|
| 210 |
try:
|
| 211 |
data = request.get_json()
|
| 212 |
-
|
| 213 |
# Validate required fields
|
| 214 |
if not data or 'email' not in data:
|
| 215 |
return jsonify({
|
| 216 |
'success': False,
|
| 217 |
'message': 'Email is required'
|
| 218 |
}), 400
|
| 219 |
-
|
| 220 |
email = data['email']
|
| 221 |
-
|
| 222 |
# Request password reset
|
| 223 |
result = request_password_reset(current_app.supabase, email)
|
| 224 |
-
|
| 225 |
if result['success']:
|
| 226 |
return jsonify(result), 200
|
| 227 |
else:
|
| 228 |
return jsonify(result), 400
|
| 229 |
-
|
| 230 |
except Exception as e:
|
| 231 |
current_app.logger.error(f"Forgot password error: {str(e)}")
|
| 232 |
return jsonify({
|
|
@@ -260,44 +305,44 @@ def show_reset_password_form():
|
|
| 260 |
def reset_password():
|
| 261 |
"""
|
| 262 |
Reset user password with token.
|
| 263 |
-
|
| 264 |
Request Body:
|
| 265 |
token (str): Password reset token
|
| 266 |
password (str): New password
|
| 267 |
-
|
| 268 |
Returns:
|
| 269 |
JSON: Password reset result
|
| 270 |
"""
|
| 271 |
try:
|
| 272 |
data = request.get_json()
|
| 273 |
-
|
| 274 |
# Validate required fields
|
| 275 |
if not data or not all(k in data for k in ('token', 'password')):
|
| 276 |
return jsonify({
|
| 277 |
'success': False,
|
| 278 |
'message': 'Token and password are required'
|
| 279 |
}), 400
|
| 280 |
-
|
| 281 |
token = data['token']
|
| 282 |
password = data['password']
|
| 283 |
-
|
| 284 |
# Note: confirm_password validation is removed as Supabase handles password confirmation automatically
|
| 285 |
-
|
| 286 |
# Validate password length
|
| 287 |
if len(password) < 8:
|
| 288 |
return jsonify({
|
| 289 |
'success': False,
|
| 290 |
'message': 'Password must be at least 8 characters long'
|
| 291 |
}), 400
|
| 292 |
-
|
| 293 |
# Reset password
|
| 294 |
result = reset_user_password(current_app.supabase, token, password)
|
| 295 |
-
|
| 296 |
if result['success']:
|
| 297 |
return jsonify(result), 200
|
| 298 |
else:
|
| 299 |
return jsonify(result), 400
|
| 300 |
-
|
| 301 |
except Exception as e:
|
| 302 |
current_app.logger.error(f"Reset password error: {str(e)}")
|
| 303 |
return jsonify({
|
|
|
|
| 2 |
from flask_jwt_extended import jwt_required, get_jwt_identity
|
| 3 |
from backend.services.auth_service import register_user, login_user, get_user_by_id, request_password_reset, reset_user_password
|
| 4 |
from backend.models.user import User
|
| 5 |
+
from backend.utils.country_language_data import COUNTRIES, LANGUAGES
|
| 6 |
|
| 7 |
auth_bp = Blueprint('auth', __name__)
|
| 8 |
|
|
|
|
| 20 |
def register():
|
| 21 |
"""
|
| 22 |
Register a new user.
|
| 23 |
+
|
| 24 |
Request Body:
|
| 25 |
email (str): User email
|
| 26 |
password (str): User password
|
| 27 |
+
country (str, optional): User country (ISO 3166-1 alpha-2 code)
|
| 28 |
+
language (str, optional): User language (ISO 639-1 code)
|
| 29 |
+
|
| 30 |
Returns:
|
| 31 |
JSON: Registration result
|
| 32 |
"""
|
| 33 |
try:
|
| 34 |
data = request.get_json()
|
| 35 |
+
|
| 36 |
# Validate required fields
|
| 37 |
if not data or not all(k in data for k in ('email', 'password')):
|
| 38 |
return jsonify({
|
| 39 |
'success': False,
|
| 40 |
'message': 'Email and password are required'
|
| 41 |
}), 400
|
| 42 |
+
|
| 43 |
email = data['email']
|
| 44 |
password = data['password']
|
| 45 |
+
country = data.get('country') # Optional: User country (ISO 3166-1 alpha-2 code)
|
| 46 |
+
language = data.get('language') # Optional: User language (ISO 639-1 code)
|
| 47 |
+
|
| 48 |
# Note: confirm_password validation is removed as Supabase handles password confirmation automatically
|
| 49 |
+
|
| 50 |
# Validate password length
|
| 51 |
if len(password) < 8:
|
| 52 |
return jsonify({
|
| 53 |
'success': False,
|
| 54 |
'message': 'Password must be at least 8 characters long'
|
| 55 |
}), 400
|
| 56 |
+
|
| 57 |
+
# Optional: Validate country and language parameters if provided
|
| 58 |
+
if country:
|
| 59 |
+
# Validate if country is a valid ISO 3166-1 alpha-2 code
|
| 60 |
+
# For now, we'll just check that it's a 2-character string
|
| 61 |
+
if not isinstance(country, str) or len(country) != 2:
|
| 62 |
+
return jsonify({
|
| 63 |
+
'success': False,
|
| 64 |
+
'message': 'Country must be a valid ISO 3166-1 alpha-2 code (2 characters)'
|
| 65 |
+
}), 400
|
| 66 |
+
|
| 67 |
+
if language:
|
| 68 |
+
# Validate if language is a valid ISO 639-1 code
|
| 69 |
+
# For now, we'll just check that it's a 2-character string
|
| 70 |
+
if not isinstance(language, str) or len(language) != 2:
|
| 71 |
+
return jsonify({
|
| 72 |
+
'success': False,
|
| 73 |
+
'message': 'Language must be a valid ISO 639-1 code (2 characters)'
|
| 74 |
+
}), 400
|
| 75 |
+
|
| 76 |
+
# Register user with preferences
|
| 77 |
+
result = register_user(email, password, country, language)
|
| 78 |
+
|
| 79 |
if result['success']:
|
| 80 |
return jsonify(result), 201
|
| 81 |
else:
|
| 82 |
return jsonify(result), 400
|
| 83 |
+
|
| 84 |
except Exception as e:
|
| 85 |
current_app.logger.error(f"Registration error: {str(e)}")
|
| 86 |
return jsonify({
|
|
|
|
| 100 |
def login():
|
| 101 |
"""
|
| 102 |
Authenticate and login a user.
|
| 103 |
+
|
| 104 |
Request Body:
|
| 105 |
email (str): User email
|
| 106 |
password (str): User password
|
| 107 |
remember_me (bool): Remember me flag for extended session (optional)
|
| 108 |
+
|
| 109 |
Returns:
|
| 110 |
JSON: Login result with JWT token
|
| 111 |
"""
|
|
|
|
| 114 |
current_app.logger.info(f"Login request received from {request.remote_addr}")
|
| 115 |
current_app.logger.info(f"Request headers: {dict(request.headers)}")
|
| 116 |
current_app.logger.info(f"Request data: {request.get_json()}")
|
| 117 |
+
|
| 118 |
data = request.get_json()
|
| 119 |
+
|
| 120 |
# Validate required fields
|
| 121 |
if not data or not all(k in data for k in ('email', 'password')):
|
| 122 |
current_app.logger.warning("Login failed: Missing email or password")
|
|
|
|
| 124 |
'success': False,
|
| 125 |
'message': 'Email and password are required'
|
| 126 |
}), 400
|
| 127 |
+
|
| 128 |
email = data['email']
|
| 129 |
password = data['password']
|
| 130 |
remember_me = data.get('remember_me', False)
|
| 131 |
+
|
| 132 |
# Login user
|
| 133 |
result = login_user(email, password, remember_me)
|
| 134 |
+
|
| 135 |
if result['success']:
|
| 136 |
# Set CORS headers explicitly
|
| 137 |
response_data = jsonify(result)
|
|
|
|
| 142 |
else:
|
| 143 |
current_app.logger.warning(f"Login failed for user {email}: {result.get('message', 'Unknown error')}")
|
| 144 |
return jsonify(result), 401
|
| 145 |
+
|
| 146 |
except Exception as e:
|
| 147 |
current_app.logger.error(f"Login error: {str(e)}", exc_info=True)
|
| 148 |
return jsonify({
|
|
|
|
| 160 |
def logout():
|
| 161 |
"""
|
| 162 |
Logout current user.
|
| 163 |
+
|
| 164 |
Returns:
|
| 165 |
JSON: Logout result
|
| 166 |
"""
|
|
|
|
| 169 |
'success': True,
|
| 170 |
'message': 'Logged out successfully'
|
| 171 |
}), 200
|
| 172 |
+
|
| 173 |
except Exception as e:
|
| 174 |
current_app.logger.error(f"Logout error: {str(e)}")
|
| 175 |
return jsonify({
|
|
|
|
| 187 |
def get_current_user():
|
| 188 |
"""
|
| 189 |
Get current authenticated user.
|
| 190 |
+
|
| 191 |
Returns:
|
| 192 |
JSON: Current user data
|
| 193 |
"""
|
| 194 |
try:
|
| 195 |
user_id = get_jwt_identity()
|
| 196 |
user_data = get_user_by_id(user_id)
|
| 197 |
+
|
| 198 |
if user_data:
|
| 199 |
return jsonify({
|
| 200 |
'success': True,
|
|
|
|
| 205 |
'success': False,
|
| 206 |
'message': 'User not found'
|
| 207 |
}), 404
|
| 208 |
+
|
| 209 |
except Exception as e:
|
| 210 |
current_app.logger.error(f"Get user error: {str(e)}")
|
| 211 |
return jsonify({
|
|
|
|
| 213 |
'message': 'An error occurred while fetching user data'
|
| 214 |
}), 500
|
| 215 |
|
| 216 |
+
@auth_bp.route('/registration-options', methods=['GET'])
|
| 217 |
+
def get_registration_options():
|
| 218 |
+
"""
|
| 219 |
+
Get registration options including countries and languages.
|
| 220 |
+
|
| 221 |
+
Returns:
|
| 222 |
+
JSON: Registration options
|
| 223 |
+
"""
|
| 224 |
+
try:
|
| 225 |
+
return jsonify({
|
| 226 |
+
'success': True,
|
| 227 |
+
'countries': COUNTRIES,
|
| 228 |
+
'languages': LANGUAGES
|
| 229 |
+
}), 200
|
| 230 |
+
|
| 231 |
+
except Exception as e:
|
| 232 |
+
current_app.logger.error(f"Get registration options error: {str(e)}")
|
| 233 |
+
return jsonify({
|
| 234 |
+
'success': False,
|
| 235 |
+
'message': 'An error occurred while fetching registration options'
|
| 236 |
+
}), 500
|
| 237 |
|
| 238 |
@auth_bp.route('/forgot-password', methods=['OPTIONS'])
|
| 239 |
def handle_forgot_password_options():
|
|
|
|
| 245 |
def forgot_password():
|
| 246 |
"""
|
| 247 |
Request password reset for a user.
|
| 248 |
+
|
| 249 |
Request Body:
|
| 250 |
email (str): User email
|
| 251 |
+
|
| 252 |
Returns:
|
| 253 |
JSON: Password reset request result
|
| 254 |
"""
|
| 255 |
try:
|
| 256 |
data = request.get_json()
|
| 257 |
+
|
| 258 |
# Validate required fields
|
| 259 |
if not data or 'email' not in data:
|
| 260 |
return jsonify({
|
| 261 |
'success': False,
|
| 262 |
'message': 'Email is required'
|
| 263 |
}), 400
|
| 264 |
+
|
| 265 |
email = data['email']
|
| 266 |
+
|
| 267 |
# Request password reset
|
| 268 |
result = request_password_reset(current_app.supabase, email)
|
| 269 |
+
|
| 270 |
if result['success']:
|
| 271 |
return jsonify(result), 200
|
| 272 |
else:
|
| 273 |
return jsonify(result), 400
|
| 274 |
+
|
| 275 |
except Exception as e:
|
| 276 |
current_app.logger.error(f"Forgot password error: {str(e)}")
|
| 277 |
return jsonify({
|
|
|
|
| 305 |
def reset_password():
|
| 306 |
"""
|
| 307 |
Reset user password with token.
|
| 308 |
+
|
| 309 |
Request Body:
|
| 310 |
token (str): Password reset token
|
| 311 |
password (str): New password
|
| 312 |
+
|
| 313 |
Returns:
|
| 314 |
JSON: Password reset result
|
| 315 |
"""
|
| 316 |
try:
|
| 317 |
data = request.get_json()
|
| 318 |
+
|
| 319 |
# Validate required fields
|
| 320 |
if not data or not all(k in data for k in ('token', 'password')):
|
| 321 |
return jsonify({
|
| 322 |
'success': False,
|
| 323 |
'message': 'Token and password are required'
|
| 324 |
}), 400
|
| 325 |
+
|
| 326 |
token = data['token']
|
| 327 |
password = data['password']
|
| 328 |
+
|
| 329 |
# Note: confirm_password validation is removed as Supabase handles password confirmation automatically
|
| 330 |
+
|
| 331 |
# Validate password length
|
| 332 |
if len(password) < 8:
|
| 333 |
return jsonify({
|
| 334 |
'success': False,
|
| 335 |
'message': 'Password must be at least 8 characters long'
|
| 336 |
}), 400
|
| 337 |
+
|
| 338 |
# Reset password
|
| 339 |
result = reset_user_password(current_app.supabase, token, password)
|
| 340 |
+
|
| 341 |
if result['success']:
|
| 342 |
return jsonify(result), 200
|
| 343 |
else:
|
| 344 |
return jsonify(result), 400
|
| 345 |
+
|
| 346 |
except Exception as e:
|
| 347 |
current_app.logger.error(f"Reset password error: {str(e)}")
|
| 348 |
return jsonify({
|
backend/api/posts.py
CHANGED
|
@@ -21,7 +21,7 @@ def safe_log_message(message):
|
|
| 21 |
else:
|
| 22 |
# For non-strings, convert to string first
|
| 23 |
safe_message = str(message)
|
| 24 |
-
|
| 25 |
# Log to app logger instead of print
|
| 26 |
current_app.logger.debug(safe_message)
|
| 27 |
except Exception as e:
|
|
@@ -38,17 +38,17 @@ def handle_options():
|
|
| 38 |
def get_posts():
|
| 39 |
"""
|
| 40 |
Get all posts for the current user.
|
| 41 |
-
|
| 42 |
Query Parameters:
|
| 43 |
published (bool): Filter by published status
|
| 44 |
-
|
| 45 |
Returns:
|
| 46 |
JSON: List of posts
|
| 47 |
"""
|
| 48 |
try:
|
| 49 |
user_id = get_jwt_identity()
|
| 50 |
published = request.args.get('published', type=bool)
|
| 51 |
-
|
| 52 |
# Check if Supabase client is initialized
|
| 53 |
if not hasattr(current_app, 'supabase') or current_app.supabase is None:
|
| 54 |
# Add CORS headers to error response
|
|
@@ -59,26 +59,26 @@ def get_posts():
|
|
| 59 |
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
|
| 60 |
response_data.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 61 |
return response_data, 500
|
| 62 |
-
|
| 63 |
# Build query
|
| 64 |
query = (
|
| 65 |
current_app.supabase
|
| 66 |
.table("Post_content")
|
| 67 |
.select("*, Social_network(id_utilisateur)")
|
| 68 |
)
|
| 69 |
-
|
| 70 |
# Apply published filter if specified
|
| 71 |
if published is not None:
|
| 72 |
query = query.eq("is_published", published)
|
| 73 |
-
|
| 74 |
response = query.execute()
|
| 75 |
-
|
| 76 |
# Filter posts for the current user
|
| 77 |
user_posts = [
|
| 78 |
post for post in response.data
|
| 79 |
if post.get('Social_network', {}).get('id_utilisateur') == user_id
|
| 80 |
] if response.data else []
|
| 81 |
-
|
| 82 |
# Add CORS headers explicitly
|
| 83 |
response_data = jsonify({
|
| 84 |
'success': True,
|
|
@@ -87,7 +87,7 @@ def get_posts():
|
|
| 87 |
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
|
| 88 |
response_data.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 89 |
return response_data, 200
|
| 90 |
-
|
| 91 |
except Exception as e:
|
| 92 |
error_message = str(e)
|
| 93 |
safe_log_message(f"Get posts error: {error_message}")
|
|
@@ -103,7 +103,7 @@ def get_posts():
|
|
| 103 |
def _generate_post_task(user_id, job_id, job_store, hugging_key):
|
| 104 |
"""
|
| 105 |
Background task to generate post content.
|
| 106 |
-
|
| 107 |
Args:
|
| 108 |
user_id (str): User ID for personalization
|
| 109 |
job_id (str): Job ID to update status in job store
|
|
@@ -117,12 +117,18 @@ def _generate_post_task(user_id, job_id, job_store, hugging_key):
|
|
| 117 |
'result': None,
|
| 118 |
'error': None
|
| 119 |
}
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
| 121 |
# Generate content using content service
|
| 122 |
# Pass the Hugging Face key directly to the service
|
| 123 |
content_service = ContentService(hugging_key=hugging_key)
|
|
|
|
|
|
|
| 124 |
generated_result = content_service.generate_post_content(user_id)
|
| 125 |
-
|
|
|
|
| 126 |
# Handle the case where generated_result might be a tuple, list, or string
|
| 127 |
# image_data could be bytes (from base64) or a string (URL)
|
| 128 |
if isinstance(generated_result, (tuple, list)) and len(generated_result) >= 2:
|
|
@@ -134,7 +140,7 @@ def _generate_post_task(user_id, job_id, job_store, hugging_key):
|
|
| 134 |
else:
|
| 135 |
generated_content = generated_result
|
| 136 |
image_data = None
|
| 137 |
-
|
| 138 |
# Update job status to completed with result
|
| 139 |
job_store[job_id] = {
|
| 140 |
'status': 'completed',
|
|
@@ -144,7 +150,9 @@ def _generate_post_task(user_id, job_id, job_store, hugging_key):
|
|
| 144 |
},
|
| 145 |
'error': None
|
| 146 |
}
|
| 147 |
-
|
|
|
|
|
|
|
| 148 |
except Exception as e:
|
| 149 |
error_message = str(e)
|
| 150 |
safe_log_message(f"Generate post background task error: {error_message}")
|
|
@@ -154,56 +162,59 @@ def _generate_post_task(user_id, job_id, job_store, hugging_key):
|
|
| 154 |
'result': None,
|
| 155 |
'error': error_message
|
| 156 |
}
|
|
|
|
| 157 |
|
| 158 |
@posts_bp.route('/generate', methods=['POST'])
|
| 159 |
@jwt_required()
|
| 160 |
def generate_post():
|
| 161 |
"""
|
| 162 |
Generate a new post using AI asynchronously.
|
| 163 |
-
|
| 164 |
Request Body:
|
| 165 |
user_id (str): User ID (optional, defaults to current user)
|
| 166 |
-
|
| 167 |
Returns:
|
| 168 |
JSON: Job ID for polling
|
| 169 |
"""
|
| 170 |
try:
|
| 171 |
current_user_id = get_jwt_identity()
|
| 172 |
data = request.get_json()
|
| 173 |
-
|
| 174 |
# Use provided user_id or default to current user
|
| 175 |
user_id = data.get('user_id', current_user_id)
|
| 176 |
-
|
| 177 |
# Verify user authorization (can only generate for self unless admin)
|
| 178 |
if user_id != current_user_id:
|
| 179 |
return jsonify({
|
| 180 |
'success': False,
|
| 181 |
'message': 'Unauthorized to generate posts for other users'
|
| 182 |
}), 403
|
| 183 |
-
|
| 184 |
# Create a job ID
|
| 185 |
job_id = str(uuid.uuid4())
|
| 186 |
-
|
| 187 |
# Initialize job status
|
| 188 |
current_app.job_store[job_id] = {
|
| 189 |
'status': 'pending',
|
| 190 |
'result': None,
|
| 191 |
'error': None
|
| 192 |
}
|
| 193 |
-
|
| 194 |
# Get Hugging Face key
|
| 195 |
hugging_key = current_app.config['HUGGING_KEY']
|
| 196 |
-
|
|
|
|
| 197 |
# Submit the background task, passing all necessary data
|
| 198 |
-
current_app.executor.submit(_generate_post_task, user_id, job_id, current_app.job_store, hugging_key)
|
| 199 |
-
|
|
|
|
| 200 |
# Return job ID immediately
|
| 201 |
return jsonify({
|
| 202 |
'success': True,
|
| 203 |
'job_id': job_id,
|
| 204 |
'message': 'Post generation started'
|
| 205 |
}), 202 # 202 Accepted
|
| 206 |
-
|
| 207 |
except Exception as e:
|
| 208 |
error_message = str(e)
|
| 209 |
safe_log_message(f"Generate post error: {error_message}")
|
|
@@ -217,30 +228,30 @@ def generate_post():
|
|
| 217 |
def get_job_status(job_id):
|
| 218 |
"""
|
| 219 |
Get the status of a post generation job.
|
| 220 |
-
|
| 221 |
Path Parameters:
|
| 222 |
job_id (str): Job ID
|
| 223 |
-
|
| 224 |
Returns:
|
| 225 |
JSON: Job status and result if completed
|
| 226 |
"""
|
| 227 |
try:
|
| 228 |
# Get job from store
|
| 229 |
job = current_app.job_store.get(job_id)
|
| 230 |
-
|
| 231 |
if not job:
|
| 232 |
return jsonify({
|
| 233 |
'success': False,
|
| 234 |
'message': 'Job not found'
|
| 235 |
}), 404
|
| 236 |
-
|
| 237 |
# Prepare response
|
| 238 |
response_data = {
|
| 239 |
'success': True,
|
| 240 |
'job_id': job_id,
|
| 241 |
'status': job['status']
|
| 242 |
}
|
| 243 |
-
|
| 244 |
# Include result or error if available
|
| 245 |
if job['status'] == 'completed':
|
| 246 |
# Handle the new structure of the result
|
|
@@ -305,9 +316,9 @@ def get_job_status(job_id):
|
|
| 305 |
response_data['has_image_data'] = False
|
| 306 |
elif job['status'] == 'failed':
|
| 307 |
response_data['error'] = job['error']
|
| 308 |
-
|
| 309 |
return jsonify(response_data), 200
|
| 310 |
-
|
| 311 |
except Exception as e:
|
| 312 |
error_message = str(e)
|
| 313 |
safe_log_message(f"Get job status error: {error_message}")
|
|
@@ -320,23 +331,23 @@ def get_job_status(job_id):
|
|
| 320 |
def get_job_image(job_id):
|
| 321 |
"""
|
| 322 |
Serve image file for a completed job.
|
| 323 |
-
|
| 324 |
Path Parameters:
|
| 325 |
job_id (str): Job ID
|
| 326 |
-
|
| 327 |
Returns:
|
| 328 |
Image file
|
| 329 |
"""
|
| 330 |
try:
|
| 331 |
# Get job from store
|
| 332 |
job = current_app.job_store.get(job_id)
|
| 333 |
-
|
| 334 |
if not job:
|
| 335 |
return jsonify({
|
| 336 |
'success': False,
|
| 337 |
'message': 'Job not found'
|
| 338 |
}), 404
|
| 339 |
-
|
| 340 |
# Check if job has an image file path
|
| 341 |
image_file_path = job.get('image_file_path')
|
| 342 |
if not image_file_path or not os.path.exists(image_file_path):
|
|
@@ -344,10 +355,10 @@ def get_job_image(job_id):
|
|
| 344 |
'success': False,
|
| 345 |
'message': 'Image not found'
|
| 346 |
}), 404
|
| 347 |
-
|
| 348 |
# Serve the image file
|
| 349 |
return send_file(image_file_path)
|
| 350 |
-
|
| 351 |
except Exception as e:
|
| 352 |
error_message = str(e)
|
| 353 |
safe_log_message(f"Get job image error: {error_message}")
|
|
@@ -366,30 +377,30 @@ def handle_publish_direct_options():
|
|
| 366 |
def publish_post_direct():
|
| 367 |
"""
|
| 368 |
Publish a post directly to social media and save to database.
|
| 369 |
-
|
| 370 |
Request Body:
|
| 371 |
social_account_id (str): Social account ID
|
| 372 |
text_content (str): Post text content
|
| 373 |
image_content_url (str, optional): Image URL
|
| 374 |
scheduled_at (str, optional): Scheduled time in ISO format
|
| 375 |
-
|
| 376 |
Returns:
|
| 377 |
JSON: Publish post result
|
| 378 |
"""
|
| 379 |
try:
|
| 380 |
user_id = get_jwt_identity()
|
| 381 |
data = request.get_json()
|
| 382 |
-
|
| 383 |
# Validate required fields
|
| 384 |
social_account_id = data.get('social_account_id')
|
| 385 |
text_content = data.get('text_content')
|
| 386 |
-
|
| 387 |
if not social_account_id or not text_content:
|
| 388 |
return jsonify({
|
| 389 |
'success': False,
|
| 390 |
'message': 'social_account_id and text_content are required'
|
| 391 |
}), 400
|
| 392 |
-
|
| 393 |
# Verify the social account belongs to the user
|
| 394 |
account_response = (
|
| 395 |
current_app.supabase
|
|
@@ -398,33 +409,33 @@ def publish_post_direct():
|
|
| 398 |
.eq("id", social_account_id)
|
| 399 |
.execute()
|
| 400 |
)
|
| 401 |
-
|
| 402 |
if not account_response.data:
|
| 403 |
return jsonify({
|
| 404 |
'success': False,
|
| 405 |
'message': 'Social account not found'
|
| 406 |
}), 404
|
| 407 |
-
|
| 408 |
account = account_response.data[0]
|
| 409 |
if account.get('id_utilisateur') != user_id:
|
| 410 |
return jsonify({
|
| 411 |
'success': False,
|
| 412 |
'message': 'Unauthorized to use this social account'
|
| 413 |
}), 403
|
| 414 |
-
|
| 415 |
# Get account details
|
| 416 |
access_token = account.get('token')
|
| 417 |
user_sub = account.get('sub')
|
| 418 |
-
|
| 419 |
if not access_token or not user_sub:
|
| 420 |
return jsonify({
|
| 421 |
'success': False,
|
| 422 |
'message': 'Social account not properly configured'
|
| 423 |
}), 400
|
| 424 |
-
|
| 425 |
# Get optional fields
|
| 426 |
image_data = data.get('image_content_url') # This could be bytes or a URL string
|
| 427 |
-
|
| 428 |
# Handle image data - if it's bytes, we need to convert it for LinkedIn
|
| 429 |
image_url_for_linkedin = None
|
| 430 |
if image_data:
|
|
@@ -436,27 +447,27 @@ def publish_post_direct():
|
|
| 436 |
else:
|
| 437 |
# If it's a string, assume it's a URL
|
| 438 |
image_url_for_linkedin = image_data
|
| 439 |
-
|
| 440 |
# Publish to LinkedIn
|
| 441 |
linkedin_service = LinkedInService()
|
| 442 |
publish_response = linkedin_service.publish_post(
|
| 443 |
access_token, user_sub, text_content, image_url_for_linkedin
|
| 444 |
)
|
| 445 |
-
|
| 446 |
# Save to database as published
|
| 447 |
post_data = {
|
| 448 |
'id_social': social_account_id,
|
| 449 |
'Text_content': text_content,
|
| 450 |
'is_published': True
|
| 451 |
}
|
| 452 |
-
|
| 453 |
# Add optional fields if provided
|
| 454 |
if image_data:
|
| 455 |
post_data['image_content_url'] = ensure_bytes_format(image_data)
|
| 456 |
-
|
| 457 |
if 'scheduled_at' in data:
|
| 458 |
post_data['scheduled_at'] = data['scheduled_at']
|
| 459 |
-
|
| 460 |
# Insert post into database
|
| 461 |
response = (
|
| 462 |
current_app.supabase
|
|
@@ -464,7 +475,7 @@ def publish_post_direct():
|
|
| 464 |
.insert(post_data)
|
| 465 |
.execute()
|
| 466 |
)
|
| 467 |
-
|
| 468 |
if response.data:
|
| 469 |
# Add CORS headers explicitly
|
| 470 |
response_data = jsonify({
|
|
@@ -485,7 +496,7 @@ def publish_post_direct():
|
|
| 485 |
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
|
| 486 |
response_data.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 487 |
return response_data, 500
|
| 488 |
-
|
| 489 |
except Exception as e:
|
| 490 |
error_message = str(e)
|
| 491 |
safe_log_message(f"[Post] Publish post directly error: {error_message}")
|
|
@@ -508,31 +519,31 @@ def handle_post_options(post_id):
|
|
| 508 |
def create_post():
|
| 509 |
"""
|
| 510 |
Create a new post.
|
| 511 |
-
|
| 512 |
Request Body:
|
| 513 |
social_account_id (str): Social account ID
|
| 514 |
text_content (str): Post text content
|
| 515 |
image_content_url (str, optional): Image URL
|
| 516 |
scheduled_at (str, optional): Scheduled time in ISO format
|
| 517 |
is_published (bool, optional): Whether the post is published (defaults to True)
|
| 518 |
-
|
| 519 |
Returns:
|
| 520 |
JSON: Created post data
|
| 521 |
"""
|
| 522 |
try:
|
| 523 |
user_id = get_jwt_identity()
|
| 524 |
data = request.get_json()
|
| 525 |
-
|
| 526 |
# Validate required fields
|
| 527 |
social_account_id = data.get('social_account_id')
|
| 528 |
text_content = data.get('text_content')
|
| 529 |
-
|
| 530 |
if not social_account_id or not text_content:
|
| 531 |
return jsonify({
|
| 532 |
'success': False,
|
| 533 |
'message': 'social_account_id and text_content are required'
|
| 534 |
}), 400
|
| 535 |
-
|
| 536 |
# Verify the social account belongs to the user
|
| 537 |
account_response = (
|
| 538 |
current_app.supabase
|
|
@@ -541,36 +552,36 @@ def create_post():
|
|
| 541 |
.eq("id", social_account_id)
|
| 542 |
.execute()
|
| 543 |
)
|
| 544 |
-
|
| 545 |
if not account_response.data:
|
| 546 |
return jsonify({
|
| 547 |
'success': False,
|
| 548 |
'message': 'Social account not found'
|
| 549 |
}), 404
|
| 550 |
-
|
| 551 |
if account_response.data[0].get('id_utilisateur') != user_id:
|
| 552 |
return jsonify({
|
| 553 |
'success': False,
|
| 554 |
'message': 'Unauthorized to use this social account'
|
| 555 |
}), 403
|
| 556 |
-
|
| 557 |
# Prepare post data - always mark as published
|
| 558 |
post_data = {
|
| 559 |
'id_social': social_account_id,
|
| 560 |
'Text_content': text_content,
|
| 561 |
'is_published': data.get('is_published', True) # Default to True
|
| 562 |
}
|
| 563 |
-
|
| 564 |
# Handle image data - could be bytes or a URL string
|
| 565 |
image_data = data.get('image_content_url')
|
| 566 |
-
|
| 567 |
# Add optional fields if provided
|
| 568 |
if image_data is not None:
|
| 569 |
post_data['image_content_url'] = ensure_bytes_format(image_data)
|
| 570 |
-
|
| 571 |
if 'scheduled_at' in data:
|
| 572 |
post_data['scheduled_at'] = data['scheduled_at']
|
| 573 |
-
|
| 574 |
# Insert post into database
|
| 575 |
response = (
|
| 576 |
current_app.supabase
|
|
@@ -578,7 +589,7 @@ def create_post():
|
|
| 578 |
.insert(post_data)
|
| 579 |
.execute()
|
| 580 |
)
|
| 581 |
-
|
| 582 |
if response.data:
|
| 583 |
# Add CORS headers explicitly
|
| 584 |
response_data = jsonify({
|
|
@@ -597,7 +608,7 @@ def create_post():
|
|
| 597 |
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
|
| 598 |
response_data.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 599 |
return response_data, 500
|
| 600 |
-
|
| 601 |
except Exception as e:
|
| 602 |
error_message = str(e)
|
| 603 |
safe_log_message(f"[Post] Create post error: {error_message}")
|
|
@@ -615,16 +626,16 @@ def create_post():
|
|
| 615 |
def delete_post(post_id):
|
| 616 |
"""
|
| 617 |
Delete a post.
|
| 618 |
-
|
| 619 |
Path Parameters:
|
| 620 |
post_id (str): Post ID
|
| 621 |
-
|
| 622 |
Returns:
|
| 623 |
JSON: Delete post result
|
| 624 |
"""
|
| 625 |
try:
|
| 626 |
user_id = get_jwt_identity()
|
| 627 |
-
|
| 628 |
# Verify the post belongs to the user
|
| 629 |
response = (
|
| 630 |
current_app.supabase
|
|
@@ -633,20 +644,20 @@ def delete_post(post_id):
|
|
| 633 |
.eq("id", post_id)
|
| 634 |
.execute()
|
| 635 |
)
|
| 636 |
-
|
| 637 |
if not response.data:
|
| 638 |
return jsonify({
|
| 639 |
'success': False,
|
| 640 |
'message': 'Post not found'
|
| 641 |
}), 404
|
| 642 |
-
|
| 643 |
post = response.data[0]
|
| 644 |
if post.get('Social_network', {}).get('id_utilisateur') != user_id:
|
| 645 |
return jsonify({
|
| 646 |
'success': False,
|
| 647 |
'message': 'Unauthorized to delete this post'
|
| 648 |
}), 403
|
| 649 |
-
|
| 650 |
# Delete post from Supabase
|
| 651 |
delete_response = (
|
| 652 |
current_app.supabase
|
|
@@ -655,7 +666,7 @@ def delete_post(post_id):
|
|
| 655 |
.eq("id", post_id)
|
| 656 |
.execute()
|
| 657 |
)
|
| 658 |
-
|
| 659 |
if delete_response.data:
|
| 660 |
return jsonify({
|
| 661 |
'success': True,
|
|
@@ -666,7 +677,7 @@ def delete_post(post_id):
|
|
| 666 |
'success': False,
|
| 667 |
'message': 'Failed to delete post'
|
| 668 |
}), 500
|
| 669 |
-
|
| 670 |
except Exception as e:
|
| 671 |
error_message = str(e)
|
| 672 |
safe_log_message(f"Delete post error: {error_message}")
|
|
@@ -680,18 +691,18 @@ def delete_post(post_id):
|
|
| 680 |
def keyword_analysis():
|
| 681 |
"""
|
| 682 |
Analyze keyword frequency in RSS feeds and posts.
|
| 683 |
-
|
| 684 |
Request Body:
|
| 685 |
keyword (str): The keyword to analyze
|
| 686 |
date_range (str, optional): Date range for analysis (daily, weekly, monthly)
|
| 687 |
-
|
| 688 |
Returns:
|
| 689 |
JSON: Keyword frequency analysis data
|
| 690 |
"""
|
| 691 |
try:
|
| 692 |
user_id = get_jwt_identity()
|
| 693 |
data = request.get_json()
|
| 694 |
-
|
| 695 |
# Validate required fields
|
| 696 |
keyword = data.get('keyword')
|
| 697 |
if not keyword:
|
|
@@ -699,14 +710,14 @@ def keyword_analysis():
|
|
| 699 |
'success': False,
|
| 700 |
'message': 'Keyword is required'
|
| 701 |
}), 400
|
| 702 |
-
|
| 703 |
# Get date range (default to all available data)
|
| 704 |
date_range = data.get('date_range', 'monthly')
|
| 705 |
-
|
| 706 |
# Use ContentService to perform keyword analysis
|
| 707 |
content_service = current_app.content_service
|
| 708 |
analysis_data = content_service.analyze_keyword_frequency(keyword, user_id, date_range)
|
| 709 |
-
|
| 710 |
# Add CORS headers explicitly
|
| 711 |
response_data = jsonify({
|
| 712 |
'success': True,
|
|
@@ -717,7 +728,7 @@ def keyword_analysis():
|
|
| 717 |
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
|
| 718 |
response_data.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 719 |
return response_data, 200
|
| 720 |
-
|
| 721 |
except Exception as e:
|
| 722 |
error_message = str(e)
|
| 723 |
safe_log_message(f"Keyword analysis error: {error_message}")
|
|
|
|
| 21 |
else:
|
| 22 |
# For non-strings, convert to string first
|
| 23 |
safe_message = str(message)
|
| 24 |
+
|
| 25 |
# Log to app logger instead of print
|
| 26 |
current_app.logger.debug(safe_message)
|
| 27 |
except Exception as e:
|
|
|
|
| 38 |
def get_posts():
|
| 39 |
"""
|
| 40 |
Get all posts for the current user.
|
| 41 |
+
|
| 42 |
Query Parameters:
|
| 43 |
published (bool): Filter by published status
|
| 44 |
+
|
| 45 |
Returns:
|
| 46 |
JSON: List of posts
|
| 47 |
"""
|
| 48 |
try:
|
| 49 |
user_id = get_jwt_identity()
|
| 50 |
published = request.args.get('published', type=bool)
|
| 51 |
+
|
| 52 |
# Check if Supabase client is initialized
|
| 53 |
if not hasattr(current_app, 'supabase') or current_app.supabase is None:
|
| 54 |
# Add CORS headers to error response
|
|
|
|
| 59 |
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
|
| 60 |
response_data.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 61 |
return response_data, 500
|
| 62 |
+
|
| 63 |
# Build query
|
| 64 |
query = (
|
| 65 |
current_app.supabase
|
| 66 |
.table("Post_content")
|
| 67 |
.select("*, Social_network(id_utilisateur)")
|
| 68 |
)
|
| 69 |
+
|
| 70 |
# Apply published filter if specified
|
| 71 |
if published is not None:
|
| 72 |
query = query.eq("is_published", published)
|
| 73 |
+
|
| 74 |
response = query.execute()
|
| 75 |
+
|
| 76 |
# Filter posts for the current user
|
| 77 |
user_posts = [
|
| 78 |
post for post in response.data
|
| 79 |
if post.get('Social_network', {}).get('id_utilisateur') == user_id
|
| 80 |
] if response.data else []
|
| 81 |
+
|
| 82 |
# Add CORS headers explicitly
|
| 83 |
response_data = jsonify({
|
| 84 |
'success': True,
|
|
|
|
| 87 |
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
|
| 88 |
response_data.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 89 |
return response_data, 200
|
| 90 |
+
|
| 91 |
except Exception as e:
|
| 92 |
error_message = str(e)
|
| 93 |
safe_log_message(f"Get posts error: {error_message}")
|
|
|
|
| 103 |
def _generate_post_task(user_id, job_id, job_store, hugging_key):
|
| 104 |
"""
|
| 105 |
Background task to generate post content.
|
| 106 |
+
|
| 107 |
Args:
|
| 108 |
user_id (str): User ID for personalization
|
| 109 |
job_id (str): Job ID to update status in job store
|
|
|
|
| 117 |
'result': None,
|
| 118 |
'error': None
|
| 119 |
}
|
| 120 |
+
|
| 121 |
+
# Log that the background task has started
|
| 122 |
+
current_app.logger.info(f"Starting background post generation task for user_id: {user_id}, job_id: {job_id}")
|
| 123 |
+
|
| 124 |
# Generate content using content service
|
| 125 |
# Pass the Hugging Face key directly to the service
|
| 126 |
content_service = ContentService(hugging_key=hugging_key)
|
| 127 |
+
current_app.logger.info(f"ContentService created successfully for user_id: {user_id}")
|
| 128 |
+
|
| 129 |
generated_result = content_service.generate_post_content(user_id)
|
| 130 |
+
current_app.logger.info(f"Content generation completed successfully for user_id: {user_id}, result type: {type(generated_result)}")
|
| 131 |
+
|
| 132 |
# Handle the case where generated_result might be a tuple, list, or string
|
| 133 |
# image_data could be bytes (from base64) or a string (URL)
|
| 134 |
if isinstance(generated_result, (tuple, list)) and len(generated_result) >= 2:
|
|
|
|
| 140 |
else:
|
| 141 |
generated_content = generated_result
|
| 142 |
image_data = None
|
| 143 |
+
|
| 144 |
# Update job status to completed with result
|
| 145 |
job_store[job_id] = {
|
| 146 |
'status': 'completed',
|
|
|
|
| 150 |
},
|
| 151 |
'error': None
|
| 152 |
}
|
| 153 |
+
|
| 154 |
+
current_app.logger.info(f"Background task completed for job_id: {job_id}, status set to completed")
|
| 155 |
+
|
| 156 |
except Exception as e:
|
| 157 |
error_message = str(e)
|
| 158 |
safe_log_message(f"Generate post background task error: {error_message}")
|
|
|
|
| 162 |
'result': None,
|
| 163 |
'error': error_message
|
| 164 |
}
|
| 165 |
+
current_app.logger.error(f"Background task failed for job_id: {job_id}, error: {error_message}")
|
| 166 |
|
| 167 |
@posts_bp.route('/generate', methods=['POST'])
|
| 168 |
@jwt_required()
|
| 169 |
def generate_post():
|
| 170 |
"""
|
| 171 |
Generate a new post using AI asynchronously.
|
| 172 |
+
|
| 173 |
Request Body:
|
| 174 |
user_id (str): User ID (optional, defaults to current user)
|
| 175 |
+
|
| 176 |
Returns:
|
| 177 |
JSON: Job ID for polling
|
| 178 |
"""
|
| 179 |
try:
|
| 180 |
current_user_id = get_jwt_identity()
|
| 181 |
data = request.get_json()
|
| 182 |
+
|
| 183 |
# Use provided user_id or default to current user
|
| 184 |
user_id = data.get('user_id', current_user_id)
|
| 185 |
+
|
| 186 |
# Verify user authorization (can only generate for self unless admin)
|
| 187 |
if user_id != current_user_id:
|
| 188 |
return jsonify({
|
| 189 |
'success': False,
|
| 190 |
'message': 'Unauthorized to generate posts for other users'
|
| 191 |
}), 403
|
| 192 |
+
|
| 193 |
# Create a job ID
|
| 194 |
job_id = str(uuid.uuid4())
|
| 195 |
+
|
| 196 |
# Initialize job status
|
| 197 |
current_app.job_store[job_id] = {
|
| 198 |
'status': 'pending',
|
| 199 |
'result': None,
|
| 200 |
'error': None
|
| 201 |
}
|
| 202 |
+
|
| 203 |
# Get Hugging Face key
|
| 204 |
hugging_key = current_app.config['HUGGING_KEY']
|
| 205 |
+
current_app.logger.info(f"About to submit background task for user_id: {user_id}, job_id: {job_id}, hugging_key present: {bool(hugging_key)}")
|
| 206 |
+
|
| 207 |
# Submit the background task, passing all necessary data
|
| 208 |
+
future = current_app.executor.submit(_generate_post_task, user_id, job_id, current_app.job_store, hugging_key)
|
| 209 |
+
current_app.logger.info(f"Background task submitted successfully, future object: {future}")
|
| 210 |
+
|
| 211 |
# Return job ID immediately
|
| 212 |
return jsonify({
|
| 213 |
'success': True,
|
| 214 |
'job_id': job_id,
|
| 215 |
'message': 'Post generation started'
|
| 216 |
}), 202 # 202 Accepted
|
| 217 |
+
|
| 218 |
except Exception as e:
|
| 219 |
error_message = str(e)
|
| 220 |
safe_log_message(f"Generate post error: {error_message}")
|
|
|
|
| 228 |
def get_job_status(job_id):
|
| 229 |
"""
|
| 230 |
Get the status of a post generation job.
|
| 231 |
+
|
| 232 |
Path Parameters:
|
| 233 |
job_id (str): Job ID
|
| 234 |
+
|
| 235 |
Returns:
|
| 236 |
JSON: Job status and result if completed
|
| 237 |
"""
|
| 238 |
try:
|
| 239 |
# Get job from store
|
| 240 |
job = current_app.job_store.get(job_id)
|
| 241 |
+
|
| 242 |
if not job:
|
| 243 |
return jsonify({
|
| 244 |
'success': False,
|
| 245 |
'message': 'Job not found'
|
| 246 |
}), 404
|
| 247 |
+
|
| 248 |
# Prepare response
|
| 249 |
response_data = {
|
| 250 |
'success': True,
|
| 251 |
'job_id': job_id,
|
| 252 |
'status': job['status']
|
| 253 |
}
|
| 254 |
+
|
| 255 |
# Include result or error if available
|
| 256 |
if job['status'] == 'completed':
|
| 257 |
# Handle the new structure of the result
|
|
|
|
| 316 |
response_data['has_image_data'] = False
|
| 317 |
elif job['status'] == 'failed':
|
| 318 |
response_data['error'] = job['error']
|
| 319 |
+
|
| 320 |
return jsonify(response_data), 200
|
| 321 |
+
|
| 322 |
except Exception as e:
|
| 323 |
error_message = str(e)
|
| 324 |
safe_log_message(f"Get job status error: {error_message}")
|
|
|
|
| 331 |
def get_job_image(job_id):
|
| 332 |
"""
|
| 333 |
Serve image file for a completed job.
|
| 334 |
+
|
| 335 |
Path Parameters:
|
| 336 |
job_id (str): Job ID
|
| 337 |
+
|
| 338 |
Returns:
|
| 339 |
Image file
|
| 340 |
"""
|
| 341 |
try:
|
| 342 |
# Get job from store
|
| 343 |
job = current_app.job_store.get(job_id)
|
| 344 |
+
|
| 345 |
if not job:
|
| 346 |
return jsonify({
|
| 347 |
'success': False,
|
| 348 |
'message': 'Job not found'
|
| 349 |
}), 404
|
| 350 |
+
|
| 351 |
# Check if job has an image file path
|
| 352 |
image_file_path = job.get('image_file_path')
|
| 353 |
if not image_file_path or not os.path.exists(image_file_path):
|
|
|
|
| 355 |
'success': False,
|
| 356 |
'message': 'Image not found'
|
| 357 |
}), 404
|
| 358 |
+
|
| 359 |
# Serve the image file
|
| 360 |
return send_file(image_file_path)
|
| 361 |
+
|
| 362 |
except Exception as e:
|
| 363 |
error_message = str(e)
|
| 364 |
safe_log_message(f"Get job image error: {error_message}")
|
|
|
|
| 377 |
def publish_post_direct():
|
| 378 |
"""
|
| 379 |
Publish a post directly to social media and save to database.
|
| 380 |
+
|
| 381 |
Request Body:
|
| 382 |
social_account_id (str): Social account ID
|
| 383 |
text_content (str): Post text content
|
| 384 |
image_content_url (str, optional): Image URL
|
| 385 |
scheduled_at (str, optional): Scheduled time in ISO format
|
| 386 |
+
|
| 387 |
Returns:
|
| 388 |
JSON: Publish post result
|
| 389 |
"""
|
| 390 |
try:
|
| 391 |
user_id = get_jwt_identity()
|
| 392 |
data = request.get_json()
|
| 393 |
+
|
| 394 |
# Validate required fields
|
| 395 |
social_account_id = data.get('social_account_id')
|
| 396 |
text_content = data.get('text_content')
|
| 397 |
+
|
| 398 |
if not social_account_id or not text_content:
|
| 399 |
return jsonify({
|
| 400 |
'success': False,
|
| 401 |
'message': 'social_account_id and text_content are required'
|
| 402 |
}), 400
|
| 403 |
+
|
| 404 |
# Verify the social account belongs to the user
|
| 405 |
account_response = (
|
| 406 |
current_app.supabase
|
|
|
|
| 409 |
.eq("id", social_account_id)
|
| 410 |
.execute()
|
| 411 |
)
|
| 412 |
+
|
| 413 |
if not account_response.data:
|
| 414 |
return jsonify({
|
| 415 |
'success': False,
|
| 416 |
'message': 'Social account not found'
|
| 417 |
}), 404
|
| 418 |
+
|
| 419 |
account = account_response.data[0]
|
| 420 |
if account.get('id_utilisateur') != user_id:
|
| 421 |
return jsonify({
|
| 422 |
'success': False,
|
| 423 |
'message': 'Unauthorized to use this social account'
|
| 424 |
}), 403
|
| 425 |
+
|
| 426 |
# Get account details
|
| 427 |
access_token = account.get('token')
|
| 428 |
user_sub = account.get('sub')
|
| 429 |
+
|
| 430 |
if not access_token or not user_sub:
|
| 431 |
return jsonify({
|
| 432 |
'success': False,
|
| 433 |
'message': 'Social account not properly configured'
|
| 434 |
}), 400
|
| 435 |
+
|
| 436 |
# Get optional fields
|
| 437 |
image_data = data.get('image_content_url') # This could be bytes or a URL string
|
| 438 |
+
|
| 439 |
# Handle image data - if it's bytes, we need to convert it for LinkedIn
|
| 440 |
image_url_for_linkedin = None
|
| 441 |
if image_data:
|
|
|
|
| 447 |
else:
|
| 448 |
# If it's a string, assume it's a URL
|
| 449 |
image_url_for_linkedin = image_data
|
| 450 |
+
|
| 451 |
# Publish to LinkedIn
|
| 452 |
linkedin_service = LinkedInService()
|
| 453 |
publish_response = linkedin_service.publish_post(
|
| 454 |
access_token, user_sub, text_content, image_url_for_linkedin
|
| 455 |
)
|
| 456 |
+
|
| 457 |
# Save to database as published
|
| 458 |
post_data = {
|
| 459 |
'id_social': social_account_id,
|
| 460 |
'Text_content': text_content,
|
| 461 |
'is_published': True
|
| 462 |
}
|
| 463 |
+
|
| 464 |
# Add optional fields if provided
|
| 465 |
if image_data:
|
| 466 |
post_data['image_content_url'] = ensure_bytes_format(image_data)
|
| 467 |
+
|
| 468 |
if 'scheduled_at' in data:
|
| 469 |
post_data['scheduled_at'] = data['scheduled_at']
|
| 470 |
+
|
| 471 |
# Insert post into database
|
| 472 |
response = (
|
| 473 |
current_app.supabase
|
|
|
|
| 475 |
.insert(post_data)
|
| 476 |
.execute()
|
| 477 |
)
|
| 478 |
+
|
| 479 |
if response.data:
|
| 480 |
# Add CORS headers explicitly
|
| 481 |
response_data = jsonify({
|
|
|
|
| 496 |
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
|
| 497 |
response_data.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 498 |
return response_data, 500
|
| 499 |
+
|
| 500 |
except Exception as e:
|
| 501 |
error_message = str(e)
|
| 502 |
safe_log_message(f"[Post] Publish post directly error: {error_message}")
|
|
|
|
| 519 |
def create_post():
|
| 520 |
"""
|
| 521 |
Create a new post.
|
| 522 |
+
|
| 523 |
Request Body:
|
| 524 |
social_account_id (str): Social account ID
|
| 525 |
text_content (str): Post text content
|
| 526 |
image_content_url (str, optional): Image URL
|
| 527 |
scheduled_at (str, optional): Scheduled time in ISO format
|
| 528 |
is_published (bool, optional): Whether the post is published (defaults to True)
|
| 529 |
+
|
| 530 |
Returns:
|
| 531 |
JSON: Created post data
|
| 532 |
"""
|
| 533 |
try:
|
| 534 |
user_id = get_jwt_identity()
|
| 535 |
data = request.get_json()
|
| 536 |
+
|
| 537 |
# Validate required fields
|
| 538 |
social_account_id = data.get('social_account_id')
|
| 539 |
text_content = data.get('text_content')
|
| 540 |
+
|
| 541 |
if not social_account_id or not text_content:
|
| 542 |
return jsonify({
|
| 543 |
'success': False,
|
| 544 |
'message': 'social_account_id and text_content are required'
|
| 545 |
}), 400
|
| 546 |
+
|
| 547 |
# Verify the social account belongs to the user
|
| 548 |
account_response = (
|
| 549 |
current_app.supabase
|
|
|
|
| 552 |
.eq("id", social_account_id)
|
| 553 |
.execute()
|
| 554 |
)
|
| 555 |
+
|
| 556 |
if not account_response.data:
|
| 557 |
return jsonify({
|
| 558 |
'success': False,
|
| 559 |
'message': 'Social account not found'
|
| 560 |
}), 404
|
| 561 |
+
|
| 562 |
if account_response.data[0].get('id_utilisateur') != user_id:
|
| 563 |
return jsonify({
|
| 564 |
'success': False,
|
| 565 |
'message': 'Unauthorized to use this social account'
|
| 566 |
}), 403
|
| 567 |
+
|
| 568 |
# Prepare post data - always mark as published
|
| 569 |
post_data = {
|
| 570 |
'id_social': social_account_id,
|
| 571 |
'Text_content': text_content,
|
| 572 |
'is_published': data.get('is_published', True) # Default to True
|
| 573 |
}
|
| 574 |
+
|
| 575 |
# Handle image data - could be bytes or a URL string
|
| 576 |
image_data = data.get('image_content_url')
|
| 577 |
+
|
| 578 |
# Add optional fields if provided
|
| 579 |
if image_data is not None:
|
| 580 |
post_data['image_content_url'] = ensure_bytes_format(image_data)
|
| 581 |
+
|
| 582 |
if 'scheduled_at' in data:
|
| 583 |
post_data['scheduled_at'] = data['scheduled_at']
|
| 584 |
+
|
| 585 |
# Insert post into database
|
| 586 |
response = (
|
| 587 |
current_app.supabase
|
|
|
|
| 589 |
.insert(post_data)
|
| 590 |
.execute()
|
| 591 |
)
|
| 592 |
+
|
| 593 |
if response.data:
|
| 594 |
# Add CORS headers explicitly
|
| 595 |
response_data = jsonify({
|
|
|
|
| 608 |
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
|
| 609 |
response_data.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 610 |
return response_data, 500
|
| 611 |
+
|
| 612 |
except Exception as e:
|
| 613 |
error_message = str(e)
|
| 614 |
safe_log_message(f"[Post] Create post error: {error_message}")
|
|
|
|
| 626 |
def delete_post(post_id):
|
| 627 |
"""
|
| 628 |
Delete a post.
|
| 629 |
+
|
| 630 |
Path Parameters:
|
| 631 |
post_id (str): Post ID
|
| 632 |
+
|
| 633 |
Returns:
|
| 634 |
JSON: Delete post result
|
| 635 |
"""
|
| 636 |
try:
|
| 637 |
user_id = get_jwt_identity()
|
| 638 |
+
|
| 639 |
# Verify the post belongs to the user
|
| 640 |
response = (
|
| 641 |
current_app.supabase
|
|
|
|
| 644 |
.eq("id", post_id)
|
| 645 |
.execute()
|
| 646 |
)
|
| 647 |
+
|
| 648 |
if not response.data:
|
| 649 |
return jsonify({
|
| 650 |
'success': False,
|
| 651 |
'message': 'Post not found'
|
| 652 |
}), 404
|
| 653 |
+
|
| 654 |
post = response.data[0]
|
| 655 |
if post.get('Social_network', {}).get('id_utilisateur') != user_id:
|
| 656 |
return jsonify({
|
| 657 |
'success': False,
|
| 658 |
'message': 'Unauthorized to delete this post'
|
| 659 |
}), 403
|
| 660 |
+
|
| 661 |
# Delete post from Supabase
|
| 662 |
delete_response = (
|
| 663 |
current_app.supabase
|
|
|
|
| 666 |
.eq("id", post_id)
|
| 667 |
.execute()
|
| 668 |
)
|
| 669 |
+
|
| 670 |
if delete_response.data:
|
| 671 |
return jsonify({
|
| 672 |
'success': True,
|
|
|
|
| 677 |
'success': False,
|
| 678 |
'message': 'Failed to delete post'
|
| 679 |
}), 500
|
| 680 |
+
|
| 681 |
except Exception as e:
|
| 682 |
error_message = str(e)
|
| 683 |
safe_log_message(f"Delete post error: {error_message}")
|
|
|
|
| 691 |
def keyword_analysis():
|
| 692 |
"""
|
| 693 |
Analyze keyword frequency in RSS feeds and posts.
|
| 694 |
+
|
| 695 |
Request Body:
|
| 696 |
keyword (str): The keyword to analyze
|
| 697 |
date_range (str, optional): Date range for analysis (daily, weekly, monthly)
|
| 698 |
+
|
| 699 |
Returns:
|
| 700 |
JSON: Keyword frequency analysis data
|
| 701 |
"""
|
| 702 |
try:
|
| 703 |
user_id = get_jwt_identity()
|
| 704 |
data = request.get_json()
|
| 705 |
+
|
| 706 |
# Validate required fields
|
| 707 |
keyword = data.get('keyword')
|
| 708 |
if not keyword:
|
|
|
|
| 710 |
'success': False,
|
| 711 |
'message': 'Keyword is required'
|
| 712 |
}), 400
|
| 713 |
+
|
| 714 |
# Get date range (default to all available data)
|
| 715 |
date_range = data.get('date_range', 'monthly')
|
| 716 |
+
|
| 717 |
# Use ContentService to perform keyword analysis
|
| 718 |
content_service = current_app.content_service
|
| 719 |
analysis_data = content_service.analyze_keyword_frequency(keyword, user_id, date_range)
|
| 720 |
+
|
| 721 |
# Add CORS headers explicitly
|
| 722 |
response_data = jsonify({
|
| 723 |
'success': True,
|
|
|
|
| 728 |
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
|
| 729 |
response_data.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 730 |
return response_data, 200
|
| 731 |
+
|
| 732 |
except Exception as e:
|
| 733 |
error_message = str(e)
|
| 734 |
safe_log_message(f"Keyword analysis error: {error_message}")
|
backend/config.py
CHANGED
|
@@ -11,7 +11,7 @@ def get_system_encoding():
|
|
| 11 |
# Try to get the preferred encoding
|
| 12 |
import locale
|
| 13 |
preferred_encoding = locale.getpreferredencoding(False)
|
| 14 |
-
|
| 15 |
# Ensure it's UTF-8 or a compatible encoding
|
| 16 |
if preferred_encoding.lower() not in ['utf-8', 'utf8', 'utf_8']:
|
| 17 |
# On Windows, try to set UTF-8
|
|
@@ -23,56 +23,57 @@ def get_system_encoding():
|
|
| 23 |
preferred_encoding = 'utf-8'
|
| 24 |
else:
|
| 25 |
preferred_encoding = 'utf-8'
|
| 26 |
-
|
| 27 |
return preferred_encoding
|
| 28 |
except:
|
| 29 |
return 'utf-8'
|
| 30 |
|
| 31 |
class Config:
|
| 32 |
"""Base configuration class."""
|
| 33 |
-
|
| 34 |
# Set default encoding
|
| 35 |
DEFAULT_ENCODING = get_system_encoding()
|
| 36 |
-
|
| 37 |
# Supabase configuration
|
| 38 |
SUPABASE_URL = os.environ.get('SUPABASE_URL') or ''
|
| 39 |
SUPABASE_KEY = os.environ.get('SUPABASE_KEY') or ''
|
| 40 |
-
|
| 41 |
# LinkedIn OAuth configuration
|
| 42 |
CLIENT_ID = os.environ.get('CLIENT_ID') or ''
|
| 43 |
CLIENT_SECRET = os.environ.get('CLIENT_SECRET') or ''
|
| 44 |
REDIRECT_URL = os.environ.get('REDIRECT_URL') or ''
|
| 45 |
-
|
| 46 |
# Hugging Face configuration
|
| 47 |
-
|
| 48 |
-
|
|
|
|
| 49 |
# JWT configuration
|
| 50 |
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') or 'your-secret-key-change-in-production'
|
| 51 |
-
|
| 52 |
# Database configuration
|
| 53 |
DATABASE_URL = os.environ.get('DATABASE_URL') or ''
|
| 54 |
-
|
| 55 |
# Application configuration
|
| 56 |
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key-change-in-production'
|
| 57 |
DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'
|
| 58 |
-
|
| 59 |
# Scheduler configuration
|
| 60 |
SCHEDULER_ENABLED = os.environ.get('SCHEDULER_ENABLED', 'True').lower() == 'true'
|
| 61 |
-
|
| 62 |
# Unicode/Encoding configuration
|
| 63 |
FORCE_UTF8 = os.environ.get('FORCE_UTF8', 'True').lower() == 'true'
|
| 64 |
UNICODE_LOGGING = os.environ.get('UNICODE_LOGGING', 'True').lower() == 'true'
|
| 65 |
-
|
| 66 |
# Environment detection
|
| 67 |
ENVIRONMENT = os.environ.get('ENVIRONMENT', 'development').lower()
|
| 68 |
IS_WINDOWS = platform.system() == 'Windows'
|
| 69 |
IS_DOCKER = os.environ.get('DOCKER_CONTAINER', '').lower() == 'true'
|
| 70 |
-
|
| 71 |
# Set environment-specific encoding settings
|
| 72 |
if FORCE_UTF8:
|
| 73 |
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
| 74 |
os.environ['PYTHONUTF8'] = '1'
|
| 75 |
-
|
| 76 |
# Debug and logging settings
|
| 77 |
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO' if ENVIRONMENT == 'production' else 'DEBUG')
|
| 78 |
UNICODE_SAFE_LOGGING = UNICODE_LOGGING and not IS_WINDOWS
|
|
|
|
| 11 |
# Try to get the preferred encoding
|
| 12 |
import locale
|
| 13 |
preferred_encoding = locale.getpreferredencoding(False)
|
| 14 |
+
|
| 15 |
# Ensure it's UTF-8 or a compatible encoding
|
| 16 |
if preferred_encoding.lower() not in ['utf-8', 'utf8', 'utf_8']:
|
| 17 |
# On Windows, try to set UTF-8
|
|
|
|
| 23 |
preferred_encoding = 'utf-8'
|
| 24 |
else:
|
| 25 |
preferred_encoding = 'utf-8'
|
| 26 |
+
|
| 27 |
return preferred_encoding
|
| 28 |
except:
|
| 29 |
return 'utf-8'
|
| 30 |
|
| 31 |
class Config:
|
| 32 |
"""Base configuration class."""
|
| 33 |
+
|
| 34 |
# Set default encoding
|
| 35 |
DEFAULT_ENCODING = get_system_encoding()
|
| 36 |
+
|
| 37 |
# Supabase configuration
|
| 38 |
SUPABASE_URL = os.environ.get('SUPABASE_URL') or ''
|
| 39 |
SUPABASE_KEY = os.environ.get('SUPABASE_KEY') or ''
|
| 40 |
+
|
| 41 |
# LinkedIn OAuth configuration
|
| 42 |
CLIENT_ID = os.environ.get('CLIENT_ID') or ''
|
| 43 |
CLIENT_SECRET = os.environ.get('CLIENT_SECRET') or ''
|
| 44 |
REDIRECT_URL = os.environ.get('REDIRECT_URL') or ''
|
| 45 |
+
|
| 46 |
# Hugging Face configuration
|
| 47 |
+
# Check for lowercase hugging_key first (for dev), then uppercase HUGGING_KEY (for production)
|
| 48 |
+
HUGGING_KEY = os.environ.get('hugging_key') or os.environ.get('HUGGING_KEY') or ''
|
| 49 |
+
|
| 50 |
# JWT configuration
|
| 51 |
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') or 'your-secret-key-change-in-production'
|
| 52 |
+
|
| 53 |
# Database configuration
|
| 54 |
DATABASE_URL = os.environ.get('DATABASE_URL') or ''
|
| 55 |
+
|
| 56 |
# Application configuration
|
| 57 |
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key-change-in-production'
|
| 58 |
DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'
|
| 59 |
+
|
| 60 |
# Scheduler configuration
|
| 61 |
SCHEDULER_ENABLED = os.environ.get('SCHEDULER_ENABLED', 'True').lower() == 'true'
|
| 62 |
+
|
| 63 |
# Unicode/Encoding configuration
|
| 64 |
FORCE_UTF8 = os.environ.get('FORCE_UTF8', 'True').lower() == 'true'
|
| 65 |
UNICODE_LOGGING = os.environ.get('UNICODE_LOGGING', 'True').lower() == 'true'
|
| 66 |
+
|
| 67 |
# Environment detection
|
| 68 |
ENVIRONMENT = os.environ.get('ENVIRONMENT', 'development').lower()
|
| 69 |
IS_WINDOWS = platform.system() == 'Windows'
|
| 70 |
IS_DOCKER = os.environ.get('DOCKER_CONTAINER', '').lower() == 'true'
|
| 71 |
+
|
| 72 |
# Set environment-specific encoding settings
|
| 73 |
if FORCE_UTF8:
|
| 74 |
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
| 75 |
os.environ['PYTHONUTF8'] = '1'
|
| 76 |
+
|
| 77 |
# Debug and logging settings
|
| 78 |
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO' if ENVIRONMENT == 'production' else 'DEBUG')
|
| 79 |
UNICODE_SAFE_LOGGING = UNICODE_LOGGING and not IS_WINDOWS
|
backend/services/auth_service.py
CHANGED
|
@@ -6,14 +6,16 @@ from supabase import Client
|
|
| 6 |
from backend.models.user import User
|
| 7 |
from backend.utils.database import authenticate_user, create_user
|
| 8 |
|
| 9 |
-
def register_user(email: str, password: str) -> dict:
|
| 10 |
"""
|
| 11 |
Register a new user.
|
| 12 |
-
|
| 13 |
Args:
|
| 14 |
email (str): User email
|
| 15 |
password (str): User password
|
| 16 |
-
|
|
|
|
|
|
|
| 17 |
Returns:
|
| 18 |
dict: Registration result with user data or error message
|
| 19 |
"""
|
|
@@ -33,10 +35,10 @@ def register_user(email: str, password: str) -> dict:
|
|
| 33 |
current_app.logger.warning(f"Failed to check profiles table for email {email}: {str(profile_check_error)}")
|
| 34 |
# Optionally, you could return an error here if you want to be strict about this check
|
| 35 |
# return {'success': False, 'message': 'Unable to process registration at this time. Please try again later.'}
|
| 36 |
-
|
| 37 |
# If no profile found, proceed with Supabase Auth sign up
|
| 38 |
response = create_user(current_app.supabase, email, password)
|
| 39 |
-
|
| 40 |
if response.user:
|
| 41 |
user = User.from_dict({
|
| 42 |
'id': response.user.id,
|
|
@@ -44,7 +46,25 @@ def register_user(email: str, password: str) -> dict:
|
|
| 44 |
'created_at': response.user.created_at,
|
| 45 |
'email_confirmed_at': response.user.email_confirmed_at
|
| 46 |
})
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
# Check if email is confirmed
|
| 49 |
if response.user.email_confirmed_at:
|
| 50 |
# Email is confirmed, user can login immediately
|
|
@@ -71,7 +91,7 @@ def register_user(email: str, password: str) -> dict:
|
|
| 71 |
except Exception as e:
|
| 72 |
# Log the full error for debugging
|
| 73 |
current_app.logger.error(f"Registration error for email {email}: {str(e)}")
|
| 74 |
-
|
| 75 |
# Check if it's a duplicate user error from Supabase Auth
|
| 76 |
error_str = str(e).lower()
|
| 77 |
if 'already registered' in error_str or 'already exists' in error_str:
|
|
@@ -101,19 +121,19 @@ def register_user(email: str, password: str) -> dict:
|
|
| 101 |
def login_user(email: str, password: str, remember_me: bool = False) -> dict:
|
| 102 |
"""
|
| 103 |
Authenticate and login a user.
|
| 104 |
-
|
| 105 |
Args:
|
| 106 |
email (str): User email
|
| 107 |
password (str): User password
|
| 108 |
remember_me (bool): Remember me flag for extended session
|
| 109 |
-
|
| 110 |
Returns:
|
| 111 |
dict: Login result with token and user data or error message
|
| 112 |
"""
|
| 113 |
try:
|
| 114 |
# Authenticate user with Supabase
|
| 115 |
response = authenticate_user(current_app.supabase, email, password)
|
| 116 |
-
|
| 117 |
if response.user:
|
| 118 |
# Check if email is confirmed
|
| 119 |
if not response.user.email_confirmed_at:
|
|
@@ -122,7 +142,7 @@ def login_user(email: str, password: str, remember_me: bool = False) -> dict:
|
|
| 122 |
'message': 'Check your mail to confirm your account',
|
| 123 |
'requires_confirmation': True
|
| 124 |
}
|
| 125 |
-
|
| 126 |
# Set token expiration based on remember me flag
|
| 127 |
if remember_me:
|
| 128 |
# Extended token expiration (7 days)
|
|
@@ -132,7 +152,7 @@ def login_user(email: str, password: str, remember_me: bool = False) -> dict:
|
|
| 132 |
# Standard token expiration (1 hour)
|
| 133 |
expires_delta = timedelta(hours=1)
|
| 134 |
token_type = "session"
|
| 135 |
-
|
| 136 |
# Create JWT token with proper expiration and claims
|
| 137 |
access_token = create_access_token(
|
| 138 |
identity=response.user.id,
|
|
@@ -144,14 +164,14 @@ def login_user(email: str, password: str, remember_me: bool = False) -> dict:
|
|
| 144 |
},
|
| 145 |
expires_delta=expires_delta
|
| 146 |
)
|
| 147 |
-
|
| 148 |
user = User.from_dict({
|
| 149 |
'id': response.user.id,
|
| 150 |
'email': response.user.email,
|
| 151 |
'created_at': response.user.created_at,
|
| 152 |
'email_confirmed_at': response.user.email_confirmed_at
|
| 153 |
})
|
| 154 |
-
|
| 155 |
return {
|
| 156 |
'success': True,
|
| 157 |
'token': access_token,
|
|
@@ -167,7 +187,7 @@ def login_user(email: str, password: str, remember_me: bool = False) -> dict:
|
|
| 167 |
}
|
| 168 |
except Exception as e:
|
| 169 |
current_app.logger.error(f"Login error: {str(e)}")
|
| 170 |
-
|
| 171 |
# Provide more specific error messages
|
| 172 |
error_str = str(e).lower()
|
| 173 |
if 'invalid credentials' in error_str or 'unauthorized' in error_str:
|
|
@@ -213,17 +233,17 @@ def login_user(email: str, password: str, remember_me: bool = False) -> dict:
|
|
| 213 |
def get_user_by_id(user_id: str) -> dict:
|
| 214 |
"""
|
| 215 |
Get user by ID.
|
| 216 |
-
|
| 217 |
Args:
|
| 218 |
user_id (str): User ID
|
| 219 |
-
|
| 220 |
Returns:
|
| 221 |
dict: User data or None if not found
|
| 222 |
"""
|
| 223 |
try:
|
| 224 |
# Get user from Supabase Auth
|
| 225 |
response = current_app.supabase.auth.get_user(user_id)
|
| 226 |
-
|
| 227 |
if response.user:
|
| 228 |
user = User.from_dict({
|
| 229 |
'id': response.user.id,
|
|
@@ -241,18 +261,18 @@ def get_user_by_id(user_id: str) -> dict:
|
|
| 241 |
def request_password_reset(supabase: Client, email: str) -> dict:
|
| 242 |
"""
|
| 243 |
Request password reset for a user.
|
| 244 |
-
|
| 245 |
Args:
|
| 246 |
supabase (Client): Supabase client instance
|
| 247 |
email (str): User email
|
| 248 |
-
|
| 249 |
Returns:
|
| 250 |
dict: Password reset request result
|
| 251 |
"""
|
| 252 |
try:
|
| 253 |
# Request password reset
|
| 254 |
response = supabase.auth.reset_password_for_email(email)
|
| 255 |
-
|
| 256 |
return {
|
| 257 |
'success': True,
|
| 258 |
'message': 'Password reset instructions sent to your email. Please check your inbox.'
|
|
@@ -277,17 +297,17 @@ def reset_user_password(supabase: Client, token: str, new_password: str) -> dict
|
|
| 277 |
"""
|
| 278 |
This function is deprecated. Password reset should be handled directly by the frontend
|
| 279 |
using the Supabase JavaScript client after the user is redirected from the reset email.
|
| 280 |
-
|
| 281 |
The standard Supabase v2 flow is:
|
| 282 |
1. User clicks reset link -> Supabase verifies token and establishes a recovery session.
|
| 283 |
2. User is redirected to the app (e.g., /reset-password).
|
| 284 |
3. Frontend uses supabase.auth.updateUser({ password: newPassword }) directly.
|
| 285 |
-
|
| 286 |
Args:
|
| 287 |
supabase (Client): Supabase client instance
|
| 288 |
token (str): Password reset token (not used in this implementation)
|
| 289 |
new_password (str): New password (not used in this implementation)
|
| 290 |
-
|
| 291 |
Returns:
|
| 292 |
dict: Message indicating this endpoint is deprecated
|
| 293 |
"""
|
|
|
|
| 6 |
from backend.models.user import User
|
| 7 |
from backend.utils.database import authenticate_user, create_user
|
| 8 |
|
| 9 |
+
def register_user(email: str, password: str, country: str = None, language: str = None) -> dict:
|
| 10 |
"""
|
| 11 |
Register a new user.
|
| 12 |
+
|
| 13 |
Args:
|
| 14 |
email (str): User email
|
| 15 |
password (str): User password
|
| 16 |
+
country (str, optional): User country (ISO 3166-1 alpha-2 code)
|
| 17 |
+
language (str, optional): User language (ISO 639-1 code)
|
| 18 |
+
|
| 19 |
Returns:
|
| 20 |
dict: Registration result with user data or error message
|
| 21 |
"""
|
|
|
|
| 35 |
current_app.logger.warning(f"Failed to check profiles table for email {email}: {str(profile_check_error)}")
|
| 36 |
# Optionally, you could return an error here if you want to be strict about this check
|
| 37 |
# return {'success': False, 'message': 'Unable to process registration at this time. Please try again later.'}
|
| 38 |
+
|
| 39 |
# If no profile found, proceed with Supabase Auth sign up
|
| 40 |
response = create_user(current_app.supabase, email, password)
|
| 41 |
+
|
| 42 |
if response.user:
|
| 43 |
user = User.from_dict({
|
| 44 |
'id': response.user.id,
|
|
|
|
| 46 |
'created_at': response.user.created_at,
|
| 47 |
'email_confirmed_at': response.user.email_confirmed_at
|
| 48 |
})
|
| 49 |
+
|
| 50 |
+
# Store user preferences in profiles table if provided
|
| 51 |
+
if country or language:
|
| 52 |
+
try:
|
| 53 |
+
# Prepare update data for dedicated country/language fields
|
| 54 |
+
update_data = {}
|
| 55 |
+
if country:
|
| 56 |
+
update_data['country'] = country
|
| 57 |
+
if language:
|
| 58 |
+
update_data['language'] = language
|
| 59 |
+
|
| 60 |
+
# Update the profiles table with user preferences in dedicated columns
|
| 61 |
+
update_response = current_app.supabase.table("profiles").update(update_data).eq("id", response.user.id).execute()
|
| 62 |
+
|
| 63 |
+
current_app.logger.info(f"User preferences stored for user {response.user.id}: country={country}, language={language}")
|
| 64 |
+
except Exception as profile_update_error:
|
| 65 |
+
# Log the error but don't fail the registration
|
| 66 |
+
current_app.logger.error(f"Failed to store user preferences: {str(profile_update_error)}")
|
| 67 |
+
|
| 68 |
# Check if email is confirmed
|
| 69 |
if response.user.email_confirmed_at:
|
| 70 |
# Email is confirmed, user can login immediately
|
|
|
|
| 91 |
except Exception as e:
|
| 92 |
# Log the full error for debugging
|
| 93 |
current_app.logger.error(f"Registration error for email {email}: {str(e)}")
|
| 94 |
+
|
| 95 |
# Check if it's a duplicate user error from Supabase Auth
|
| 96 |
error_str = str(e).lower()
|
| 97 |
if 'already registered' in error_str or 'already exists' in error_str:
|
|
|
|
| 121 |
def login_user(email: str, password: str, remember_me: bool = False) -> dict:
|
| 122 |
"""
|
| 123 |
Authenticate and login a user.
|
| 124 |
+
|
| 125 |
Args:
|
| 126 |
email (str): User email
|
| 127 |
password (str): User password
|
| 128 |
remember_me (bool): Remember me flag for extended session
|
| 129 |
+
|
| 130 |
Returns:
|
| 131 |
dict: Login result with token and user data or error message
|
| 132 |
"""
|
| 133 |
try:
|
| 134 |
# Authenticate user with Supabase
|
| 135 |
response = authenticate_user(current_app.supabase, email, password)
|
| 136 |
+
|
| 137 |
if response.user:
|
| 138 |
# Check if email is confirmed
|
| 139 |
if not response.user.email_confirmed_at:
|
|
|
|
| 142 |
'message': 'Check your mail to confirm your account',
|
| 143 |
'requires_confirmation': True
|
| 144 |
}
|
| 145 |
+
|
| 146 |
# Set token expiration based on remember me flag
|
| 147 |
if remember_me:
|
| 148 |
# Extended token expiration (7 days)
|
|
|
|
| 152 |
# Standard token expiration (1 hour)
|
| 153 |
expires_delta = timedelta(hours=1)
|
| 154 |
token_type = "session"
|
| 155 |
+
|
| 156 |
# Create JWT token with proper expiration and claims
|
| 157 |
access_token = create_access_token(
|
| 158 |
identity=response.user.id,
|
|
|
|
| 164 |
},
|
| 165 |
expires_delta=expires_delta
|
| 166 |
)
|
| 167 |
+
|
| 168 |
user = User.from_dict({
|
| 169 |
'id': response.user.id,
|
| 170 |
'email': response.user.email,
|
| 171 |
'created_at': response.user.created_at,
|
| 172 |
'email_confirmed_at': response.user.email_confirmed_at
|
| 173 |
})
|
| 174 |
+
|
| 175 |
return {
|
| 176 |
'success': True,
|
| 177 |
'token': access_token,
|
|
|
|
| 187 |
}
|
| 188 |
except Exception as e:
|
| 189 |
current_app.logger.error(f"Login error: {str(e)}")
|
| 190 |
+
|
| 191 |
# Provide more specific error messages
|
| 192 |
error_str = str(e).lower()
|
| 193 |
if 'invalid credentials' in error_str or 'unauthorized' in error_str:
|
|
|
|
| 233 |
def get_user_by_id(user_id: str) -> dict:
|
| 234 |
"""
|
| 235 |
Get user by ID.
|
| 236 |
+
|
| 237 |
Args:
|
| 238 |
user_id (str): User ID
|
| 239 |
+
|
| 240 |
Returns:
|
| 241 |
dict: User data or None if not found
|
| 242 |
"""
|
| 243 |
try:
|
| 244 |
# Get user from Supabase Auth
|
| 245 |
response = current_app.supabase.auth.get_user(user_id)
|
| 246 |
+
|
| 247 |
if response.user:
|
| 248 |
user = User.from_dict({
|
| 249 |
'id': response.user.id,
|
|
|
|
| 261 |
def request_password_reset(supabase: Client, email: str) -> dict:
|
| 262 |
"""
|
| 263 |
Request password reset for a user.
|
| 264 |
+
|
| 265 |
Args:
|
| 266 |
supabase (Client): Supabase client instance
|
| 267 |
email (str): User email
|
| 268 |
+
|
| 269 |
Returns:
|
| 270 |
dict: Password reset request result
|
| 271 |
"""
|
| 272 |
try:
|
| 273 |
# Request password reset
|
| 274 |
response = supabase.auth.reset_password_for_email(email)
|
| 275 |
+
|
| 276 |
return {
|
| 277 |
'success': True,
|
| 278 |
'message': 'Password reset instructions sent to your email. Please check your inbox.'
|
|
|
|
| 297 |
"""
|
| 298 |
This function is deprecated. Password reset should be handled directly by the frontend
|
| 299 |
using the Supabase JavaScript client after the user is redirected from the reset email.
|
| 300 |
+
|
| 301 |
The standard Supabase v2 flow is:
|
| 302 |
1. User clicks reset link -> Supabase verifies token and establishes a recovery session.
|
| 303 |
2. User is redirected to the app (e.g., /reset-password).
|
| 304 |
3. Frontend uses supabase.auth.updateUser({ password: newPassword }) directly.
|
| 305 |
+
|
| 306 |
Args:
|
| 307 |
supabase (Client): Supabase client instance
|
| 308 |
token (str): Password reset token (not used in this implementation)
|
| 309 |
new_password (str): New password (not used in this implementation)
|
| 310 |
+
|
| 311 |
Returns:
|
| 312 |
dict: Message indicating this endpoint is deprecated
|
| 313 |
"""
|
backend/services/content_service.py
CHANGED
|
@@ -139,11 +139,13 @@ class ContentService:
|
|
| 139 |
if self.client is None:
|
| 140 |
self._initialize_client()
|
| 141 |
|
| 142 |
-
|
| 143 |
result = self.client.predict(
|
| 144 |
code=user_id,
|
| 145 |
api_name="/poster_linkedin"
|
| 146 |
)
|
|
|
|
|
|
|
| 147 |
|
| 148 |
# Handle the case where result might be a tuple from Gradio
|
| 149 |
# The Gradio API returns a tuple with (content, image_data)
|
|
@@ -737,4 +739,65 @@ class ContentService:
|
|
| 737 |
f"https://news.google.com/rss/search?q={query_encoded}"
|
| 738 |
f"&hl={language}&gl={country}&ceid={country}:{language}"
|
| 739 |
)
|
| 740 |
-
return url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
if self.client is None:
|
| 140 |
self._initialize_client()
|
| 141 |
|
| 142 |
+
current_app.logger.info(f"Calling Gradio API with user_id: {user_id}")
|
| 143 |
result = self.client.predict(
|
| 144 |
code=user_id,
|
| 145 |
api_name="/poster_linkedin"
|
| 146 |
)
|
| 147 |
+
current_app.logger.info(f"Gradio API response received: {type(result)}")
|
| 148 |
+
|
| 149 |
|
| 150 |
# Handle the case where result might be a tuple from Gradio
|
| 151 |
# The Gradio API returns a tuple with (content, image_data)
|
|
|
|
| 739 |
f"https://news.google.com/rss/search?q={query_encoded}"
|
| 740 |
f"&hl={language}&gl={country}&ceid={country}:{language}"
|
| 741 |
)
|
| 742 |
+
return url
|
| 743 |
+
|
| 744 |
+
def _get_user_preferences(self, user_id: str) -> dict:
|
| 745 |
+
"""
|
| 746 |
+
Get user preferences (country and language) from the Supabase profiles table.
|
| 747 |
+
|
| 748 |
+
Args:
|
| 749 |
+
user_id (str): User ID to fetch preferences for
|
| 750 |
+
|
| 751 |
+
Returns:
|
| 752 |
+
Dict containing country and language, with default values if not found
|
| 753 |
+
"""
|
| 754 |
+
try:
|
| 755 |
+
# Check if Supabase client is initialized
|
| 756 |
+
if not hasattr(current_app, 'supabase') or current_app.supabase is None:
|
| 757 |
+
raise Exception("Database connection not initialized")
|
| 758 |
+
|
| 759 |
+
response = (
|
| 760 |
+
current_app.supabase
|
| 761 |
+
.table("profiles")
|
| 762 |
+
.select("country, language")
|
| 763 |
+
.eq("id", user_id)
|
| 764 |
+
.execute()
|
| 765 |
+
)
|
| 766 |
+
|
| 767 |
+
if response.data:
|
| 768 |
+
profile = response.data[0]
|
| 769 |
+
country = profile.get('country', 'US')
|
| 770 |
+
language = profile.get('language', 'en')
|
| 771 |
+
return {'country': country, 'language': language}
|
| 772 |
+
else:
|
| 773 |
+
# Return default values if no profile found
|
| 774 |
+
return {'country': 'US', 'language': 'en'}
|
| 775 |
+
except Exception as e:
|
| 776 |
+
current_app.logger.error(f"Error fetching user preferences: {str(e)}")
|
| 777 |
+
# Return default values if there's an error
|
| 778 |
+
return {'country': 'US', 'language': 'en'}
|
| 779 |
+
|
| 780 |
+
def _merge_dataframes(self, df1: pd.DataFrame, df2: pd.DataFrame) -> pd.DataFrame:
|
| 781 |
+
"""
|
| 782 |
+
Merge two dataframes removing duplicates based on article URL and sort by date.
|
| 783 |
+
|
| 784 |
+
Args:
|
| 785 |
+
df1 (pd.DataFrame): First dataframe containing articles
|
| 786 |
+
df2 (pd.DataFrame): Second dataframe containing articles
|
| 787 |
+
|
| 788 |
+
Returns:
|
| 789 |
+
pd.DataFrame: Merged dataframe with duplicates removed and sorted by date
|
| 790 |
+
"""
|
| 791 |
+
# Concatenate both dataframes
|
| 792 |
+
combined_df = pd.concat([df1, df2], ignore_index=True)
|
| 793 |
+
|
| 794 |
+
# Remove duplicates based on the 'link' column to avoid duplication
|
| 795 |
+
combined_df = combined_df.drop_duplicates(subset=['link'], keep='first')
|
| 796 |
+
|
| 797 |
+
# Sort by date in descending order (most recent first)
|
| 798 |
+
if 'date' in combined_df.columns:
|
| 799 |
+
combined_df['date'] = pd.to_datetime(combined_df['date'], errors='coerce', utc=True)
|
| 800 |
+
combined_df = combined_df.sort_values(by='date', ascending=False)
|
| 801 |
+
combined_df = combined_df.reset_index(drop=True)
|
| 802 |
+
|
| 803 |
+
return combined_df
|
backend/utils/country_language_data.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Country and Language Data for Registration Form
|
| 3 |
+
|
| 4 |
+
This module provides lists of ISO 3166-1 alpha-2 country codes and ISO 639-1 language codes
|
| 5 |
+
for use in the registration form dropdowns.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
# ISO 3166-1 alpha-2 country codes with their full names
|
| 9 |
+
COUNTRIES = [
|
| 10 |
+
{'code': 'AF', 'name': 'Afghanistan'},
|
| 11 |
+
{'code': 'AX', 'name': 'Åland Islands'},
|
| 12 |
+
{'code': 'AL', 'name': 'Albania'},
|
| 13 |
+
{'code': 'DZ', 'name': 'Algeria'},
|
| 14 |
+
{'code': 'AS', 'name': 'American Samoa'},
|
| 15 |
+
{'code': 'AD', 'name': 'Andorra'},
|
| 16 |
+
{'code': 'AO', 'name': 'Angola'},
|
| 17 |
+
{'code': 'AI', 'name': 'Anguilla'},
|
| 18 |
+
{'code': 'AQ', 'name': 'Antarctica'},
|
| 19 |
+
{'code': 'AG', 'name': 'Antigua and Barbuda'},
|
| 20 |
+
{'code': 'AR', 'name': 'Argentina'},
|
| 21 |
+
{'code': 'AM', 'name': 'Armenia'},
|
| 22 |
+
{'code': 'AW', 'name': 'Aruba'},
|
| 23 |
+
{'code': 'AU', 'name': 'Australia'},
|
| 24 |
+
{'code': 'AT', 'name': 'Austria'},
|
| 25 |
+
{'code': 'AZ', 'name': 'Azerbaijan'},
|
| 26 |
+
{'code': 'BS', 'name': 'Bahamas'},
|
| 27 |
+
{'code': 'BH', 'name': 'Bahrain'},
|
| 28 |
+
{'code': 'BD', 'name': 'Bangladesh'},
|
| 29 |
+
{'code': 'BB', 'name': 'Barbados'},
|
| 30 |
+
{'code': 'BY', 'name': 'Belarus'},
|
| 31 |
+
{'code': 'BE', 'name': 'Belgium'},
|
| 32 |
+
{'code': 'BZ', 'name': 'Belize'},
|
| 33 |
+
{'code': 'BJ', 'name': 'Benin'},
|
| 34 |
+
{'code': 'BM', 'name': 'Bermuda'},
|
| 35 |
+
{'code': 'BT', 'name': 'Bhutan'},
|
| 36 |
+
{'code': 'BO', 'name': 'Bolivia (Plurinational State of)'},
|
| 37 |
+
{'code': 'BQ', 'name': 'Bonaire, Sint Eustatius and Saba'},
|
| 38 |
+
{'code': 'BA', 'name': 'Bosnia and Herzegovina'},
|
| 39 |
+
{'code': 'BW', 'name': 'Botswana'},
|
| 40 |
+
{'code': 'BV', 'name': 'Bouvet Island'},
|
| 41 |
+
{'code': 'BR', 'name': 'Brazil'},
|
| 42 |
+
{'code': 'IO', 'name': 'British Indian Ocean Territory'},
|
| 43 |
+
{'code': 'BN', 'name': 'Brunei Darussalam'},
|
| 44 |
+
{'code': 'BG', 'name': 'Bulgaria'},
|
| 45 |
+
{'code': 'BF', 'name': 'Burkina Faso'},
|
| 46 |
+
{'code': 'BI', 'name': 'Burundi'},
|
| 47 |
+
{'code': 'CV', 'name': 'Cabo Verde'},
|
| 48 |
+
{'code': 'KH', 'name': 'Cambodia'},
|
| 49 |
+
{'code': 'CM', 'name': 'Cameroon'},
|
| 50 |
+
{'code': 'CA', 'name': 'Canada'},
|
| 51 |
+
{'code': 'KY', 'name': 'Cayman Islands'},
|
| 52 |
+
{'code': 'CF', 'name': 'Central African Republic'},
|
| 53 |
+
{'code': 'TD', 'name': 'Chad'},
|
| 54 |
+
{'code': 'CL', 'name': 'Chile'},
|
| 55 |
+
{'code': 'CN', 'name': 'China'},
|
| 56 |
+
{'code': 'CX', 'name': 'Christmas Island'},
|
| 57 |
+
{'code': 'CC', 'name': 'Cocos (Keeling) Islands'},
|
| 58 |
+
{'code': 'CO', 'name': 'Colombia'},
|
| 59 |
+
{'code': 'KM', 'name': 'Comoros'},
|
| 60 |
+
{'code': 'CG', 'name': 'Congo'},
|
| 61 |
+
{'code': 'CD', 'name': 'Congo, Democratic Republic of the'},
|
| 62 |
+
{'code': 'CK', 'name': 'Cook Islands'},
|
| 63 |
+
{'code': 'CR', 'name': 'Costa Rica'},
|
| 64 |
+
{'code': 'CI', 'name': 'Côte d\'Ivoire'},
|
| 65 |
+
{'code': 'HR', 'name': 'Croatia'},
|
| 66 |
+
{'code': 'CU', 'name': 'Cuba'},
|
| 67 |
+
{'code': 'CW', 'name': 'Curaçao'},
|
| 68 |
+
{'code': 'CY', 'name': 'Cyprus'},
|
| 69 |
+
{'code': 'CZ', 'name': 'Czechia'},
|
| 70 |
+
{'code': 'DK', 'name': 'Denmark'},
|
| 71 |
+
{'code': 'DJ', 'name': 'Djibouti'},
|
| 72 |
+
{'code': 'DM', 'name': 'Dominica'},
|
| 73 |
+
{'code': 'DO', 'name': 'Dominican Republic'},
|
| 74 |
+
{'code': 'EC', 'name': 'Ecuador'},
|
| 75 |
+
{'code': 'EG', 'name': 'Egypt'},
|
| 76 |
+
{'code': 'SV', 'name': 'El Salvador'},
|
| 77 |
+
{'code': 'GQ', 'name': 'Equatorial Guinea'},
|
| 78 |
+
{'code': 'ER', 'name': 'Eritrea'},
|
| 79 |
+
{'code': 'EE', 'name': 'Estonia'},
|
| 80 |
+
{'code': 'SZ', 'name': 'Eswatini'},
|
| 81 |
+
{'code': 'ET', 'name': 'Ethiopia'},
|
| 82 |
+
{'code': 'FK', 'name': 'Falkland Islands (Malvinas)'},
|
| 83 |
+
{'code': 'FO', 'name': 'Faroe Islands'},
|
| 84 |
+
{'code': 'FJ', 'name': 'Fiji'},
|
| 85 |
+
{'code': 'FI', 'name': 'Finland'},
|
| 86 |
+
{'code': 'FR', 'name': 'France'},
|
| 87 |
+
{'code': 'GF', 'name': 'French Guiana'},
|
| 88 |
+
{'code': 'PF', 'name': 'French Polynesia'},
|
| 89 |
+
{'code': 'TF', 'name': 'French Southern Territories'},
|
| 90 |
+
{'code': 'GA', 'name': 'Gabon'},
|
| 91 |
+
{'code': 'GM', 'name': 'Gambia'},
|
| 92 |
+
{'code': 'GE', 'name': 'Georgia'},
|
| 93 |
+
{'code': 'DE', 'name': 'Germany'},
|
| 94 |
+
{'code': 'GH', 'name': 'Ghana'},
|
| 95 |
+
{'code': 'GI', 'name': 'Gibraltar'},
|
| 96 |
+
{'code': 'GR', 'name': 'Greece'},
|
| 97 |
+
{'code': 'GL', 'name': 'Greenland'},
|
| 98 |
+
{'code': 'GD', 'name': 'Grenada'},
|
| 99 |
+
{'code': 'GP', 'name': 'Guadeloupe'},
|
| 100 |
+
{'code': 'GU', 'name': 'Guam'},
|
| 101 |
+
{'code': 'GT', 'name': 'Guatemala'},
|
| 102 |
+
{'code': 'GG', 'name': 'Guernsey'},
|
| 103 |
+
{'code': 'GN', 'name': 'Guinea'},
|
| 104 |
+
{'code': 'GW', 'name': 'Guinea-Bissau'},
|
| 105 |
+
{'code': 'GY', 'name': 'Guyana'},
|
| 106 |
+
{'code': 'HT', 'name': 'Haiti'},
|
| 107 |
+
{'code': 'HM', 'name': 'Heard Island and McDonald Islands'},
|
| 108 |
+
{'code': 'VA', 'name': 'Holy See'},
|
| 109 |
+
{'code': 'HN', 'name': 'Honduras'},
|
| 110 |
+
{'code': 'HK', 'name': 'Hong Kong'},
|
| 111 |
+
{'code': 'HU', 'name': 'Hungary'},
|
| 112 |
+
{'code': 'IS', 'name': 'Iceland'},
|
| 113 |
+
{'code': 'IN', 'name': 'India'},
|
| 114 |
+
{'code': 'ID', 'name': 'Indonesia'},
|
| 115 |
+
{'code': 'IR', 'name': 'Iran (Islamic Republic of)'},
|
| 116 |
+
{'code': 'IQ', 'name': 'Iraq'},
|
| 117 |
+
{'code': 'IE', 'name': 'Ireland'},
|
| 118 |
+
{'code': 'IM', 'name': 'Isle of Man'},
|
| 119 |
+
{'code': 'IL', 'name': 'Israel'},
|
| 120 |
+
{'code': 'IT', 'name': 'Italy'},
|
| 121 |
+
{'code': 'JM', 'name': 'Jamaica'},
|
| 122 |
+
{'code': 'JP', 'name': 'Japan'},
|
| 123 |
+
{'code': 'JE', 'name': 'Jersey'},
|
| 124 |
+
{'code': 'JO', 'name': 'Jordan'},
|
| 125 |
+
{'code': 'KZ', 'name': 'Kazakhstan'},
|
| 126 |
+
{'code': 'KE', 'name': 'Kenya'},
|
| 127 |
+
{'code': 'KI', 'name': 'Kiribati'},
|
| 128 |
+
{'code': 'KP', 'name': 'Korea (Democratic People\'s Republic of)'},
|
| 129 |
+
{'code': 'KR', 'name': 'Korea, Republic of'},
|
| 130 |
+
{'code': 'KW', 'name': 'Kuwait'},
|
| 131 |
+
{'code': 'KG', 'name': 'Kyrgyzstan'},
|
| 132 |
+
{'code': 'LA', 'name': 'Lao People\'s Democratic Republic'},
|
| 133 |
+
{'code': 'LV', 'name': 'Latvia'},
|
| 134 |
+
{'code': 'LB', 'name': 'Lebanon'},
|
| 135 |
+
{'code': 'LS', 'name': 'Lesotho'},
|
| 136 |
+
{'code': 'LR', 'name': 'Liberia'},
|
| 137 |
+
{'code': 'LY', 'name': 'Libya'},
|
| 138 |
+
{'code': 'LI', 'name': 'Liechtenstein'},
|
| 139 |
+
{'code': 'LT', 'name': 'Lithuania'},
|
| 140 |
+
{'code': 'LU', 'name': 'Luxembourg'},
|
| 141 |
+
{'code': 'MO', 'name': 'Macao'},
|
| 142 |
+
{'code': 'MG', 'name': 'Madagascar'},
|
| 143 |
+
{'code': 'MW', 'name': 'Malawi'},
|
| 144 |
+
{'code': 'MY', 'name': 'Malaysia'},
|
| 145 |
+
{'code': 'MV', 'name': 'Maldives'},
|
| 146 |
+
{'code': 'ML', 'name': 'Mali'},
|
| 147 |
+
{'code': 'MT', 'name': 'Malta'},
|
| 148 |
+
{'code': 'MH', 'name': 'Marshall Islands'},
|
| 149 |
+
{'code': 'MQ', 'name': 'Martinique'},
|
| 150 |
+
{'code': 'MR', 'name': 'Mauritania'},
|
| 151 |
+
{'code': 'MU', 'name': 'Mauritius'},
|
| 152 |
+
{'code': 'YT', 'name': 'Mayotte'},
|
| 153 |
+
{'code': 'MX', 'name': 'Mexico'},
|
| 154 |
+
{'code': 'FM', 'name': 'Micronesia (Federated States of)'},
|
| 155 |
+
{'code': 'MD', 'name': 'Moldova, Republic of'},
|
| 156 |
+
{'code': 'MC', 'name': 'Monaco'},
|
| 157 |
+
{'code': 'MN', 'name': 'Mongolia'},
|
| 158 |
+
{'code': 'ME', 'name': 'Montenegro'},
|
| 159 |
+
{'code': 'MS', 'name': 'Montserrat'},
|
| 160 |
+
{'code': 'MA', 'name': 'Morocco'},
|
| 161 |
+
{'code': 'MZ', 'name': 'Mozambique'},
|
| 162 |
+
{'code': 'MM', 'name': 'Myanmar'},
|
| 163 |
+
{'code': 'NA', 'name': 'Namibia'},
|
| 164 |
+
{'code': 'NR', 'name': 'Nauru'},
|
| 165 |
+
{'code': 'NP', 'name': 'Nepal'},
|
| 166 |
+
{'code': 'NL', 'name': 'Netherlands'},
|
| 167 |
+
{'code': 'NC', 'name': 'New Caledonia'},
|
| 168 |
+
{'code': 'NZ', 'name': 'New Zealand'},
|
| 169 |
+
{'code': 'NI', 'name': 'Nicaragua'},
|
| 170 |
+
{'code': 'NE', 'name': 'Niger'},
|
| 171 |
+
{'code': 'NG', 'name': 'Nigeria'},
|
| 172 |
+
{'code': 'NU', 'name': 'Niue'},
|
| 173 |
+
{'code': 'NF', 'name': 'Norfolk Island'},
|
| 174 |
+
{'code': 'MK', 'name': 'North Macedonia'},
|
| 175 |
+
{'code': 'MP', 'name': 'Northern Mariana Islands'},
|
| 176 |
+
{'code': 'NO', 'name': 'Norway'},
|
| 177 |
+
{'code': 'OM', 'name': 'Oman'},
|
| 178 |
+
{'code': 'PK', 'name': 'Pakistan'},
|
| 179 |
+
{'code': 'PW', 'name': 'Palau'},
|
| 180 |
+
{'code': 'PS', 'name': 'Palestine, State of'},
|
| 181 |
+
{'code': 'PA', 'name': 'Panama'},
|
| 182 |
+
{'code': 'PG', 'name': 'Papua New Guinea'},
|
| 183 |
+
{'code': 'PY', 'name': 'Paraguay'},
|
| 184 |
+
{'code': 'PE', 'name': 'Peru'},
|
| 185 |
+
{'code': 'PH', 'name': 'Philippines'},
|
| 186 |
+
{'code': 'PN', 'name': 'Pitcairn'},
|
| 187 |
+
{'code': 'PL', 'name': 'Poland'},
|
| 188 |
+
{'code': 'PT', 'name': 'Portugal'},
|
| 189 |
+
{'code': 'PR', 'name': 'Puerto Rico'},
|
| 190 |
+
{'code': 'QA', 'name': 'Qatar'},
|
| 191 |
+
{'code': 'RE', 'name': 'Réunion'},
|
| 192 |
+
{'code': 'RO', 'name': 'Romania'},
|
| 193 |
+
{'code': 'RU', 'name': 'Russian Federation'},
|
| 194 |
+
{'code': 'RW', 'name': 'Rwanda'},
|
| 195 |
+
{'code': 'BL', 'name': 'Saint Barthélemy'},
|
| 196 |
+
{'code': 'SH', 'name': 'Saint Helena, Ascension and Tristan da Cunha'},
|
| 197 |
+
{'code': 'KN', 'name': 'Saint Kitts and Nevis'},
|
| 198 |
+
{'code': 'LC', 'name': 'Saint Lucia'},
|
| 199 |
+
{'code': 'MF', 'name': 'Saint Martin (French part)'},
|
| 200 |
+
{'code': 'PM', 'name': 'Saint Pierre and Miquelon'},
|
| 201 |
+
{'code': 'VC', 'name': 'Saint Vincent and the Grenadines'},
|
| 202 |
+
{'code': 'WS', 'name': 'Samoa'},
|
| 203 |
+
{'code': 'SM', 'name': 'San Marino'},
|
| 204 |
+
{'code': 'ST', 'name': 'Sao Tome and Principe'},
|
| 205 |
+
{'code': 'SA', 'name': 'Saudi Arabia'},
|
| 206 |
+
{'code': 'SN', 'name': 'Senegal'},
|
| 207 |
+
{'code': 'RS', 'name': 'Serbia'},
|
| 208 |
+
{'code': 'SC', 'name': 'Seychelles'},
|
| 209 |
+
{'code': 'SL', 'name': 'Sierra Leone'},
|
| 210 |
+
{'code': 'SG', 'name': 'Singapore'},
|
| 211 |
+
{'code': 'SX', 'name': 'Sint Maarten (Dutch part)'},
|
| 212 |
+
{'code': 'SK', 'name': 'Slovakia'},
|
| 213 |
+
{'code': 'SI', 'name': 'Slovenia'},
|
| 214 |
+
{'code': 'SB', 'name': 'Solomon Islands'},
|
| 215 |
+
{'code': 'SO', 'name': 'Somalia'},
|
| 216 |
+
{'code': 'ZA', 'name': 'South Africa'},
|
| 217 |
+
{'code': 'GS', 'name': 'South Georgia and the South Sandwich Islands'},
|
| 218 |
+
{'code': 'SS', 'name': 'South Sudan'},
|
| 219 |
+
{'code': 'ES', 'name': 'Spain'},
|
| 220 |
+
{'code': 'LK', 'name': 'Sri Lanka'},
|
| 221 |
+
{'code': 'SD', 'name': 'Sudan'},
|
| 222 |
+
{'code': 'SR', 'name': 'Suriname'},
|
| 223 |
+
{'code': 'SJ', 'name': 'Svalbard and Jan Mayen'},
|
| 224 |
+
{'code': 'SE', 'name': 'Sweden'},
|
| 225 |
+
{'code': 'CH', 'name': 'Switzerland'},
|
| 226 |
+
{'code': 'SY', 'name': 'Syrian Arab Republic'},
|
| 227 |
+
{'code': 'TW', 'name': 'Taiwan, Province of China'},
|
| 228 |
+
{'code': 'TJ', 'name': 'Tajikistan'},
|
| 229 |
+
{'code': 'TZ', 'name': 'Tanzania, United Republic of'},
|
| 230 |
+
{'code': 'TH', 'name': 'Thailand'},
|
| 231 |
+
{'code': 'TL', 'name': 'Timor-Leste'},
|
| 232 |
+
{'code': 'TG', 'name': 'Togo'},
|
| 233 |
+
{'code': 'TK', 'name': 'Tokelau'},
|
| 234 |
+
{'code': 'TO', 'name': 'Tonga'},
|
| 235 |
+
{'code': 'TT', 'name': 'Trinidad and Tobago'},
|
| 236 |
+
{'code': 'TN', 'name': 'Tunisia'},
|
| 237 |
+
{'code': 'TR', 'name': 'Turkey'},
|
| 238 |
+
{'code': 'TM', 'name': 'Turkmenistan'},
|
| 239 |
+
{'code': 'TC', 'name': 'Turks and Caicos Islands'},
|
| 240 |
+
{'code': 'TV', 'name': 'Tuvalu'},
|
| 241 |
+
{'code': 'UG', 'name': 'Uganda'},
|
| 242 |
+
{'code': 'UA', 'name': 'Ukraine'},
|
| 243 |
+
{'code': 'AE', 'name': 'United Arab Emirates'},
|
| 244 |
+
{'code': 'GB', 'name': 'United Kingdom of Great Britain and Northern Ireland'},
|
| 245 |
+
{'code': 'US', 'name': 'United States of America'},
|
| 246 |
+
{'code': 'UM', 'name': 'United States Minor Outlying Islands'},
|
| 247 |
+
{'code': 'UY', 'name': 'Uruguay'},
|
| 248 |
+
{'code': 'UZ', 'name': 'Uzbekistan'},
|
| 249 |
+
{'code': 'VU', 'name': 'Vanuatu'},
|
| 250 |
+
{'code': 'VE', 'name': 'Venezuela (Bolivarian Republic of)'},
|
| 251 |
+
{'code': 'VN', 'name': 'Viet Nam'},
|
| 252 |
+
{'code': 'VG', 'name': 'Virgin Islands (British)'},
|
| 253 |
+
{'code': 'VI', 'name': 'Virgin Islands (U.S.)'},
|
| 254 |
+
{'code': 'WF', 'name': 'Wallis and Futuna'},
|
| 255 |
+
{'code': 'EH', 'name': 'Western Sahara'},
|
| 256 |
+
{'code': 'YE', 'name': 'Yemen'},
|
| 257 |
+
{'code': 'ZM', 'name': 'Zambia'},
|
| 258 |
+
{'code': 'ZW', 'name': 'Zimbabwe'},
|
| 259 |
+
]
|
| 260 |
+
|
| 261 |
+
# ISO 639-1 language codes with their full names - focusing on English and French as specified in the requirements
|
| 262 |
+
LANGUAGES = [
|
| 263 |
+
{'code': 'en', 'name': 'English'},
|
| 264 |
+
{'code': 'fr', 'name': 'French'},
|
| 265 |
+
]
|
docu_code/My_data_base_schema_.txt
CHANGED
|
@@ -56,6 +56,8 @@ CREATE TABLE public.profiles (
|
|
| 56 |
raw_user_meta jsonb,
|
| 57 |
created_at timestamp with time zone DEFAULT now(),
|
| 58 |
updated_at timestamp with time zone,
|
|
|
|
|
|
|
| 59 |
CONSTRAINT profiles_pkey PRIMARY KEY (id),
|
| 60 |
CONSTRAINT profiles_id_fkey FOREIGN KEY (id) REFERENCES auth.users(id)
|
| 61 |
);
|
|
|
|
| 56 |
raw_user_meta jsonb,
|
| 57 |
created_at timestamp with time zone DEFAULT now(),
|
| 58 |
updated_at timestamp with time zone,
|
| 59 |
+
country character varying,
|
| 60 |
+
language character varying,
|
| 61 |
CONSTRAINT profiles_pkey PRIMARY KEY (id),
|
| 62 |
CONSTRAINT profiles_id_fkey FOREIGN KEY (id) REFERENCES auth.users(id)
|
| 63 |
);
|
frontend/src/pages/Register.jsx
CHANGED
|
@@ -9,90 +9,122 @@ const Register = () => {
|
|
| 9 |
const { isAuthenticated, loading, error } = useSelector(state => state.auth);
|
| 10 |
// Convert string loading state to boolean for the button
|
| 11 |
const isLoading = loading === 'pending';
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
const [formData, setFormData] = useState({
|
| 14 |
email: '',
|
| 15 |
password: '',
|
| 16 |
-
confirmPassword: ''
|
|
|
|
|
|
|
| 17 |
});
|
| 18 |
-
|
| 19 |
const [showPassword, setShowPassword] = useState(false);
|
| 20 |
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
| 21 |
const [passwordStrength, setPasswordStrength] = useState(0);
|
| 22 |
const [isFocused, setIsFocused] = useState({
|
| 23 |
email: false,
|
| 24 |
password: false,
|
| 25 |
-
confirmPassword: false
|
|
|
|
|
|
|
| 26 |
});
|
| 27 |
-
|
| 28 |
useEffect(() => {
|
| 29 |
if (isAuthenticated) {
|
| 30 |
navigate('/dashboard');
|
| 31 |
}
|
| 32 |
-
|
| 33 |
// Clear any existing errors when component mounts
|
| 34 |
dispatch(clearError());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
}, [isAuthenticated, navigate, dispatch]);
|
| 36 |
-
|
| 37 |
const handleChange = (e) => {
|
| 38 |
const { name, value } = e.target;
|
| 39 |
setFormData({
|
| 40 |
...formData,
|
| 41 |
[name]: value
|
| 42 |
});
|
| 43 |
-
|
| 44 |
// Calculate password strength
|
| 45 |
if (name === 'password') {
|
| 46 |
calculatePasswordStrength(value);
|
| 47 |
}
|
| 48 |
};
|
| 49 |
-
|
| 50 |
const calculatePasswordStrength = (password) => {
|
| 51 |
let strength = 0;
|
| 52 |
-
|
| 53 |
// Length check
|
| 54 |
if (password.length >= 8) strength += 1;
|
| 55 |
if (password.length >= 12) strength += 1;
|
| 56 |
-
|
| 57 |
// Character variety checks
|
| 58 |
if (/[a-z]/.test(password)) strength += 1;
|
| 59 |
if (/[A-Z]/.test(password)) strength += 1;
|
| 60 |
if (/[0-9]/.test(password)) strength += 1;
|
| 61 |
if (/[^A-Za-z0-9]/.test(password)) strength += 1;
|
| 62 |
-
|
| 63 |
setPasswordStrength(Math.min(strength, 6));
|
| 64 |
};
|
| 65 |
-
|
| 66 |
const handleFocus = (field) => {
|
| 67 |
setIsFocused({
|
| 68 |
...isFocused,
|
| 69 |
[field]: true
|
| 70 |
});
|
| 71 |
};
|
| 72 |
-
|
| 73 |
const handleBlur = (field) => {
|
| 74 |
setIsFocused({
|
| 75 |
...isFocused,
|
| 76 |
[field]: false
|
| 77 |
});
|
| 78 |
};
|
| 79 |
-
|
| 80 |
const [showSuccess, setShowSuccess] = useState(false);
|
| 81 |
-
|
| 82 |
const handleSubmit = async (e) => {
|
| 83 |
e.preventDefault();
|
| 84 |
-
|
| 85 |
// Basic validation
|
| 86 |
if (formData.password !== formData.confirmPassword) {
|
| 87 |
alert('Passwords do not match');
|
| 88 |
return;
|
| 89 |
}
|
| 90 |
-
|
| 91 |
if (formData.password.length < 8) {
|
| 92 |
alert('Password must be at least 8 characters long');
|
| 93 |
return;
|
| 94 |
}
|
| 95 |
-
|
| 96 |
try {
|
| 97 |
await dispatch(registerUser(formData)).unwrap();
|
| 98 |
// Show success message
|
|
@@ -106,24 +138,24 @@ const Register = () => {
|
|
| 106 |
console.error('Registration failed:', err);
|
| 107 |
}
|
| 108 |
};
|
| 109 |
-
|
| 110 |
const togglePasswordVisibility = () => {
|
| 111 |
setShowPassword(!showPassword);
|
| 112 |
};
|
| 113 |
-
|
| 114 |
const toggleConfirmPasswordVisibility = () => {
|
| 115 |
setShowConfirmPassword(!showConfirmPassword);
|
| 116 |
};
|
| 117 |
-
|
| 118 |
const toggleForm = () => {
|
| 119 |
dispatch(clearError());
|
| 120 |
navigate('/login');
|
| 121 |
};
|
| 122 |
-
|
| 123 |
if (isAuthenticated) {
|
| 124 |
return null; // Redirect handled by useEffect
|
| 125 |
}
|
| 126 |
-
|
| 127 |
return (
|
| 128 |
<div className="min-h-screen bg-gradient-to-br from-primary-50 via-white to-accent-50 flex items-center justify-center p-3 sm:p-4 animate-fade-in">
|
| 129 |
<div className="w-full max-w-sm sm:max-w-md">
|
|
@@ -135,7 +167,7 @@ const Register = () => {
|
|
| 135 |
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-1 sm:mb-2">Create Account</h1>
|
| 136 |
<p className="text-sm sm:text-base text-gray-600">Sign up to get started with Lin</p>
|
| 137 |
</div>
|
| 138 |
-
|
| 139 |
{/* Auth Card */}
|
| 140 |
<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">
|
| 141 |
{/* Success Message */}
|
|
@@ -163,7 +195,7 @@ const Register = () => {
|
|
| 163 |
</div>
|
| 164 |
</div>
|
| 165 |
)}
|
| 166 |
-
|
| 167 |
{/* Register Form */}
|
| 168 |
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-5">
|
| 169 |
{/* Email Field */}
|
|
@@ -198,7 +230,75 @@ const Register = () => {
|
|
| 198 |
</div>
|
| 199 |
</div>
|
| 200 |
</div>
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
{/* Password Field */}
|
| 203 |
<div className="space-y-2">
|
| 204 |
<label htmlFor="password" className="block text-xs sm:text-sm font-semibold text-gray-700">
|
|
@@ -242,7 +342,7 @@ const Register = () => {
|
|
| 242 |
)}
|
| 243 |
</button>
|
| 244 |
</div>
|
| 245 |
-
|
| 246 |
{/* Password Strength Indicator */}
|
| 247 |
{formData.password && (
|
| 248 |
<div className="space-y-1">
|
|
@@ -274,7 +374,7 @@ const Register = () => {
|
|
| 274 |
</div>
|
| 275 |
)}
|
| 276 |
</div>
|
| 277 |
-
|
| 278 |
{/* Confirm Password Field */}
|
| 279 |
<div className="space-y-2">
|
| 280 |
<label htmlFor="confirmPassword" className="block text-xs sm:text-sm font-semibold text-gray-700">
|
|
@@ -322,7 +422,7 @@ const Register = () => {
|
|
| 322 |
<p className="text-red-600 text-xs">Passwords do not match</p>
|
| 323 |
)}
|
| 324 |
</div>
|
| 325 |
-
|
| 326 |
{/* Terms and Conditions */}
|
| 327 |
<div className="flex items-start">
|
| 328 |
<input
|
|
@@ -344,7 +444,7 @@ const Register = () => {
|
|
| 344 |
</a>
|
| 345 |
</label>
|
| 346 |
</div>
|
| 347 |
-
|
| 348 |
{/* Submit Button */}
|
| 349 |
<button
|
| 350 |
type="submit"
|
|
@@ -364,7 +464,7 @@ const Register = () => {
|
|
| 364 |
<span className="text-xs sm:text-sm">Create Account</span>
|
| 365 |
)}
|
| 366 |
</button>
|
| 367 |
-
|
| 368 |
{/* Confirmation Message */}
|
| 369 |
{!error && !showSuccess && (
|
| 370 |
<div className="text-center text-xs sm:text-sm text-gray-600">
|
|
@@ -372,8 +472,8 @@ const Register = () => {
|
|
| 372 |
</div>
|
| 373 |
)}
|
| 374 |
</form>
|
| 375 |
-
|
| 376 |
-
|
| 377 |
{/* Login Link */}
|
| 378 |
<div className="text-center">
|
| 379 |
<p className="text-xs sm:text-sm text-gray-600">
|
|
@@ -389,7 +489,7 @@ const Register = () => {
|
|
| 389 |
</p>
|
| 390 |
</div>
|
| 391 |
</div>
|
| 392 |
-
|
| 393 |
{/* Footer */}
|
| 394 |
<div className="text-center mt-6 sm:mt-8 text-xs text-gray-500">
|
| 395 |
<p>© 2024 Lin. All rights reserved.</p>
|
|
|
|
| 9 |
const { isAuthenticated, loading, error } = useSelector(state => state.auth);
|
| 10 |
// Convert string loading state to boolean for the button
|
| 11 |
const isLoading = loading === 'pending';
|
| 12 |
+
|
| 13 |
+
const [countryOptions, setCountryOptions] = useState([]);
|
| 14 |
+
const [languageOptions, setLanguageOptions] = useState([]);
|
| 15 |
+
const [loadingOptions, setLoadingOptions] = useState(false);
|
| 16 |
+
|
| 17 |
const [formData, setFormData] = useState({
|
| 18 |
email: '',
|
| 19 |
password: '',
|
| 20 |
+
confirmPassword: '',
|
| 21 |
+
country: '',
|
| 22 |
+
language: ''
|
| 23 |
});
|
| 24 |
+
|
| 25 |
const [showPassword, setShowPassword] = useState(false);
|
| 26 |
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
| 27 |
const [passwordStrength, setPasswordStrength] = useState(0);
|
| 28 |
const [isFocused, setIsFocused] = useState({
|
| 29 |
email: false,
|
| 30 |
password: false,
|
| 31 |
+
confirmPassword: false,
|
| 32 |
+
country: false,
|
| 33 |
+
language: false
|
| 34 |
});
|
| 35 |
+
|
| 36 |
useEffect(() => {
|
| 37 |
if (isAuthenticated) {
|
| 38 |
navigate('/dashboard');
|
| 39 |
}
|
| 40 |
+
|
| 41 |
// Clear any existing errors when component mounts
|
| 42 |
dispatch(clearError());
|
| 43 |
+
|
| 44 |
+
// Load registration options
|
| 45 |
+
const loadOptions = async () => {
|
| 46 |
+
setLoadingOptions(true);
|
| 47 |
+
try {
|
| 48 |
+
const response = await fetch('/api/auth/registration-options');
|
| 49 |
+
if (response.ok) {
|
| 50 |
+
const data = await response.json();
|
| 51 |
+
if (data.success) {
|
| 52 |
+
setCountryOptions(data.countries);
|
| 53 |
+
setLanguageOptions(data.languages);
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
} catch (err) {
|
| 57 |
+
console.error('Failed to load registration options:', err);
|
| 58 |
+
// Use fallback options if API call fails
|
| 59 |
+
setCountryOptions([{ code: 'US', name: 'United States' }, { code: 'FR', name: 'France' }]);
|
| 60 |
+
setLanguageOptions([{ code: 'en', name: 'English' }, { code: 'fr', name: 'French' }]);
|
| 61 |
+
} finally {
|
| 62 |
+
setLoadingOptions(false);
|
| 63 |
+
}
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
loadOptions();
|
| 67 |
}, [isAuthenticated, navigate, dispatch]);
|
| 68 |
+
|
| 69 |
const handleChange = (e) => {
|
| 70 |
const { name, value } = e.target;
|
| 71 |
setFormData({
|
| 72 |
...formData,
|
| 73 |
[name]: value
|
| 74 |
});
|
| 75 |
+
|
| 76 |
// Calculate password strength
|
| 77 |
if (name === 'password') {
|
| 78 |
calculatePasswordStrength(value);
|
| 79 |
}
|
| 80 |
};
|
| 81 |
+
|
| 82 |
const calculatePasswordStrength = (password) => {
|
| 83 |
let strength = 0;
|
| 84 |
+
|
| 85 |
// Length check
|
| 86 |
if (password.length >= 8) strength += 1;
|
| 87 |
if (password.length >= 12) strength += 1;
|
| 88 |
+
|
| 89 |
// Character variety checks
|
| 90 |
if (/[a-z]/.test(password)) strength += 1;
|
| 91 |
if (/[A-Z]/.test(password)) strength += 1;
|
| 92 |
if (/[0-9]/.test(password)) strength += 1;
|
| 93 |
if (/[^A-Za-z0-9]/.test(password)) strength += 1;
|
| 94 |
+
|
| 95 |
setPasswordStrength(Math.min(strength, 6));
|
| 96 |
};
|
| 97 |
+
|
| 98 |
const handleFocus = (field) => {
|
| 99 |
setIsFocused({
|
| 100 |
...isFocused,
|
| 101 |
[field]: true
|
| 102 |
});
|
| 103 |
};
|
| 104 |
+
|
| 105 |
const handleBlur = (field) => {
|
| 106 |
setIsFocused({
|
| 107 |
...isFocused,
|
| 108 |
[field]: false
|
| 109 |
});
|
| 110 |
};
|
| 111 |
+
|
| 112 |
const [showSuccess, setShowSuccess] = useState(false);
|
| 113 |
+
|
| 114 |
const handleSubmit = async (e) => {
|
| 115 |
e.preventDefault();
|
| 116 |
+
|
| 117 |
// Basic validation
|
| 118 |
if (formData.password !== formData.confirmPassword) {
|
| 119 |
alert('Passwords do not match');
|
| 120 |
return;
|
| 121 |
}
|
| 122 |
+
|
| 123 |
if (formData.password.length < 8) {
|
| 124 |
alert('Password must be at least 8 characters long');
|
| 125 |
return;
|
| 126 |
}
|
| 127 |
+
|
| 128 |
try {
|
| 129 |
await dispatch(registerUser(formData)).unwrap();
|
| 130 |
// Show success message
|
|
|
|
| 138 |
console.error('Registration failed:', err);
|
| 139 |
}
|
| 140 |
};
|
| 141 |
+
|
| 142 |
const togglePasswordVisibility = () => {
|
| 143 |
setShowPassword(!showPassword);
|
| 144 |
};
|
| 145 |
+
|
| 146 |
const toggleConfirmPasswordVisibility = () => {
|
| 147 |
setShowConfirmPassword(!showConfirmPassword);
|
| 148 |
};
|
| 149 |
+
|
| 150 |
const toggleForm = () => {
|
| 151 |
dispatch(clearError());
|
| 152 |
navigate('/login');
|
| 153 |
};
|
| 154 |
+
|
| 155 |
if (isAuthenticated) {
|
| 156 |
return null; // Redirect handled by useEffect
|
| 157 |
}
|
| 158 |
+
|
| 159 |
return (
|
| 160 |
<div className="min-h-screen bg-gradient-to-br from-primary-50 via-white to-accent-50 flex items-center justify-center p-3 sm:p-4 animate-fade-in">
|
| 161 |
<div className="w-full max-w-sm sm:max-w-md">
|
|
|
|
| 167 |
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-1 sm:mb-2">Create Account</h1>
|
| 168 |
<p className="text-sm sm:text-base text-gray-600">Sign up to get started with Lin</p>
|
| 169 |
</div>
|
| 170 |
+
|
| 171 |
{/* Auth Card */}
|
| 172 |
<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">
|
| 173 |
{/* Success Message */}
|
|
|
|
| 195 |
</div>
|
| 196 |
</div>
|
| 197 |
)}
|
| 198 |
+
|
| 199 |
{/* Register Form */}
|
| 200 |
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-5">
|
| 201 |
{/* Email Field */}
|
|
|
|
| 230 |
</div>
|
| 231 |
</div>
|
| 232 |
</div>
|
| 233 |
+
|
| 234 |
+
{/* Country Selection */}
|
| 235 |
+
<div className="space-y-2">
|
| 236 |
+
<label htmlFor="country" className="block text-xs sm:text-sm font-semibold text-gray-700">
|
| 237 |
+
Country
|
| 238 |
+
</label>
|
| 239 |
+
<select
|
| 240 |
+
id="country"
|
| 241 |
+
name="country"
|
| 242 |
+
value={formData.country}
|
| 243 |
+
onChange={handleChange}
|
| 244 |
+
onFocus={() => handleFocus('country')}
|
| 245 |
+
onBlur={() => handleBlur('country')}
|
| 246 |
+
className={`w-full px-3 sm:px-4 py-2 sm:py-3 rounded-xl border-2 transition-all duration-200 ${
|
| 247 |
+
isFocused.country
|
| 248 |
+
? 'border-primary-500 shadow-md'
|
| 249 |
+
: 'border-gray-200 hover:border-gray-300'
|
| 250 |
+
} ${formData.country ? 'text-gray-900' : 'text-gray-500'} focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 touch-manipulation`}
|
| 251 |
+
required
|
| 252 |
+
aria-required="true"
|
| 253 |
+
aria-label="Select your country"
|
| 254 |
+
>
|
| 255 |
+
<option value="">Select a country</option>
|
| 256 |
+
{loadingOptions ? (
|
| 257 |
+
<option value="">Loading...</option>
|
| 258 |
+
) : (
|
| 259 |
+
countryOptions.map(country => (
|
| 260 |
+
<option key={country.code} value={country.code}>
|
| 261 |
+
{country.name}
|
| 262 |
+
</option>
|
| 263 |
+
))
|
| 264 |
+
)}
|
| 265 |
+
</select>
|
| 266 |
+
</div>
|
| 267 |
+
|
| 268 |
+
{/* Language Selection */}
|
| 269 |
+
<div className="space-y-2">
|
| 270 |
+
<label htmlFor="language" className="block text-xs sm:text-sm font-semibold text-gray-700">
|
| 271 |
+
Language
|
| 272 |
+
</label>
|
| 273 |
+
<select
|
| 274 |
+
id="language"
|
| 275 |
+
name="language"
|
| 276 |
+
value={formData.language}
|
| 277 |
+
onChange={handleChange}
|
| 278 |
+
onFocus={() => handleFocus('language')}
|
| 279 |
+
onBlur={() => handleBlur('language')}
|
| 280 |
+
className={`w-full px-3 sm:px-4 py-2 sm:py-3 rounded-xl border-2 transition-all duration-200 ${
|
| 281 |
+
isFocused.language
|
| 282 |
+
? 'border-primary-500 shadow-md'
|
| 283 |
+
: 'border-gray-200 hover:border-gray-300'
|
| 284 |
+
} ${formData.language ? 'text-gray-900' : 'text-gray-500'} focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 touch-manipulation`}
|
| 285 |
+
required
|
| 286 |
+
aria-required="true"
|
| 287 |
+
aria-label="Select your language"
|
| 288 |
+
>
|
| 289 |
+
<option value="">Select a language</option>
|
| 290 |
+
{loadingOptions ? (
|
| 291 |
+
<option value="">Loading...</option>
|
| 292 |
+
) : (
|
| 293 |
+
languageOptions.map(lang => (
|
| 294 |
+
<option key={lang.code} value={lang.code}>
|
| 295 |
+
{lang.name}
|
| 296 |
+
</option>
|
| 297 |
+
))
|
| 298 |
+
)}
|
| 299 |
+
</select>
|
| 300 |
+
</div>
|
| 301 |
+
|
| 302 |
{/* Password Field */}
|
| 303 |
<div className="space-y-2">
|
| 304 |
<label htmlFor="password" className="block text-xs sm:text-sm font-semibold text-gray-700">
|
|
|
|
| 342 |
)}
|
| 343 |
</button>
|
| 344 |
</div>
|
| 345 |
+
|
| 346 |
{/* Password Strength Indicator */}
|
| 347 |
{formData.password && (
|
| 348 |
<div className="space-y-1">
|
|
|
|
| 374 |
</div>
|
| 375 |
)}
|
| 376 |
</div>
|
| 377 |
+
|
| 378 |
{/* Confirm Password Field */}
|
| 379 |
<div className="space-y-2">
|
| 380 |
<label htmlFor="confirmPassword" className="block text-xs sm:text-sm font-semibold text-gray-700">
|
|
|
|
| 422 |
<p className="text-red-600 text-xs">Passwords do not match</p>
|
| 423 |
)}
|
| 424 |
</div>
|
| 425 |
+
|
| 426 |
{/* Terms and Conditions */}
|
| 427 |
<div className="flex items-start">
|
| 428 |
<input
|
|
|
|
| 444 |
</a>
|
| 445 |
</label>
|
| 446 |
</div>
|
| 447 |
+
|
| 448 |
{/* Submit Button */}
|
| 449 |
<button
|
| 450 |
type="submit"
|
|
|
|
| 464 |
<span className="text-xs sm:text-sm">Create Account</span>
|
| 465 |
)}
|
| 466 |
</button>
|
| 467 |
+
|
| 468 |
{/* Confirmation Message */}
|
| 469 |
{!error && !showSuccess && (
|
| 470 |
<div className="text-center text-xs sm:text-sm text-gray-600">
|
|
|
|
| 472 |
</div>
|
| 473 |
)}
|
| 474 |
</form>
|
| 475 |
+
|
| 476 |
+
|
| 477 |
{/* Login Link */}
|
| 478 |
<div className="text-center">
|
| 479 |
<p className="text-xs sm:text-sm text-gray-600">
|
|
|
|
| 489 |
</p>
|
| 490 |
</div>
|
| 491 |
</div>
|
| 492 |
+
|
| 493 |
{/* Footer */}
|
| 494 |
<div className="text-center mt-6 sm:mt-8 text-xs text-gray-500">
|
| 495 |
<p>© 2024 Lin. All rights reserved.</p>
|
frontend/src/services/authService.js
CHANGED
|
@@ -10,6 +10,8 @@ class AuthService {
|
|
| 10 |
* @param {Object} userData - User registration data
|
| 11 |
* @param {string} userData.email - User email
|
| 12 |
* @param {string} userData.password - User password
|
|
|
|
|
|
|
| 13 |
* @returns {Promise<Object>} - API response
|
| 14 |
*/
|
| 15 |
async register(userData) {
|
|
@@ -23,7 +25,7 @@ class AuthService {
|
|
| 23 |
fullData: userData
|
| 24 |
});
|
| 25 |
}
|
| 26 |
-
|
| 27 |
const response = await apiClient.post('/auth/register', userData);
|
| 28 |
return response;
|
| 29 |
} catch (error) {
|
|
|
|
| 10 |
* @param {Object} userData - User registration data
|
| 11 |
* @param {string} userData.email - User email
|
| 12 |
* @param {string} userData.password - User password
|
| 13 |
+
* @param {string} [userData.country] - User country (ISO 3166-1 alpha-2 code)
|
| 14 |
+
* @param {string} [userData.language] - User language (ISO 639-1 code)
|
| 15 |
* @returns {Promise<Object>} - API response
|
| 16 |
*/
|
| 17 |
async register(userData) {
|
|
|
|
| 25 |
fullData: userData
|
| 26 |
});
|
| 27 |
}
|
| 28 |
+
|
| 29 |
const response = await apiClient.post('/auth/register', userData);
|
| 30 |
return response;
|
| 31 |
} catch (error) {
|
frontend/src/store/reducers/authSlice.js
CHANGED
|
@@ -28,18 +28,18 @@ export const checkCachedAuth = createAsyncThunk(
|
|
| 28 |
async (_, { rejectWithValue }) => {
|
| 29 |
try {
|
| 30 |
const isDevelopment = import.meta.env.VITE_NODE_ENV === 'development';
|
| 31 |
-
|
| 32 |
if (isDevelopment) {
|
| 33 |
console.log('🔐 [Auth] Starting cached authentication check');
|
| 34 |
}
|
| 35 |
-
|
| 36 |
// First check cache
|
| 37 |
const cachedAuth = await cacheService.getAuthCache();
|
| 38 |
if (cachedAuth) {
|
| 39 |
if (isDevelopment) {
|
| 40 |
console.log('🗃️ [Cache] Found cached authentication data');
|
| 41 |
}
|
| 42 |
-
|
| 43 |
// Validate that the cached token is still valid by checking expiry
|
| 44 |
if (cachedAuth.expiresAt && Date.now() < cachedAuth.expiresAt) {
|
| 45 |
return {
|
|
@@ -58,18 +58,18 @@ export const checkCachedAuth = createAsyncThunk(
|
|
| 58 |
await cacheService.clearAuthCache();
|
| 59 |
}
|
| 60 |
}
|
| 61 |
-
|
| 62 |
// If not in cache or expired, check cookies
|
| 63 |
if (isDevelopment) {
|
| 64 |
console.log('🍪 [Cookie] Checking for authentication cookies');
|
| 65 |
}
|
| 66 |
-
|
| 67 |
const cookieAuth = await cookieService.getAuthTokens();
|
| 68 |
if (cookieAuth?.accessToken) {
|
| 69 |
if (isDevelopment) {
|
| 70 |
console.log('🍪 [Cookie] Found authentication cookies, validating with API');
|
| 71 |
}
|
| 72 |
-
|
| 73 |
// Validate token and get user data
|
| 74 |
try {
|
| 75 |
const response = await authService.getCurrentUser();
|
|
@@ -79,15 +79,15 @@ export const checkCachedAuth = createAsyncThunk(
|
|
| 79 |
token: cookieAuth.accessToken,
|
| 80 |
user: response.data.user
|
| 81 |
}, cookieAuth.rememberMe);
|
| 82 |
-
|
| 83 |
const expiresAt = cookieAuth.rememberMe ?
|
| 84 |
Date.now() + (7 * 24 * 60 * 60 * 1000) :
|
| 85 |
Date.now() + (60 * 60 * 1000);
|
| 86 |
-
|
| 87 |
if (isDevelopment) {
|
| 88 |
console.log('✅ [Auth] Cookie authentication validated successfully');
|
| 89 |
}
|
| 90 |
-
|
| 91 |
return {
|
| 92 |
success: true,
|
| 93 |
user: response.data.user,
|
|
@@ -113,7 +113,7 @@ export const checkCachedAuth = createAsyncThunk(
|
|
| 113 |
console.log('🍪 [Cookie] No authentication cookies found');
|
| 114 |
}
|
| 115 |
}
|
| 116 |
-
|
| 117 |
if (isDevelopment) {
|
| 118 |
console.log('🔐 [Auth] No valid cached or cookie authentication found');
|
| 119 |
}
|
|
@@ -136,16 +136,16 @@ export const autoLogin = createAsyncThunk(
|
|
| 136 |
if (isDevelopment) {
|
| 137 |
console.log('🔐 [Auth] Starting auto login process');
|
| 138 |
}
|
| 139 |
-
|
| 140 |
// Try to get token from cookies first, then fallback to localStorage
|
| 141 |
let token = null;
|
| 142 |
let rememberMe = false;
|
| 143 |
-
|
| 144 |
try {
|
| 145 |
const cookieAuth = await cookieService.getAuthTokens();
|
| 146 |
token = cookieAuth?.accessToken;
|
| 147 |
rememberMe = cookieAuth?.rememberMe || false;
|
| 148 |
-
|
| 149 |
if (isDevelopment) {
|
| 150 |
console.log('🍪 [Cookie] Got tokens from cookie service:', { token: !!token, rememberMe });
|
| 151 |
}
|
|
@@ -154,7 +154,7 @@ export const autoLogin = createAsyncThunk(
|
|
| 154 |
console.warn('🍪 [Cookie] Error getting cookie tokens, trying localStorage:', cookieError.message);
|
| 155 |
}
|
| 156 |
}
|
| 157 |
-
|
| 158 |
// If no cookie token, try localStorage
|
| 159 |
if (!token) {
|
| 160 |
token = localStorage.getItem('token');
|
|
@@ -162,14 +162,14 @@ export const autoLogin = createAsyncThunk(
|
|
| 162 |
console.log('💾 [Storage] Got token from localStorage:', !!token);
|
| 163 |
}
|
| 164 |
}
|
| 165 |
-
|
| 166 |
if (token) {
|
| 167 |
try {
|
| 168 |
// Try to validate token and get user data
|
| 169 |
if (isDevelopment) {
|
| 170 |
console.log('🔑 [Token] Validating token with API');
|
| 171 |
}
|
| 172 |
-
|
| 173 |
const response = await authService.getCurrentUser();
|
| 174 |
if (response.data.success) {
|
| 175 |
// Update cache and cookies
|
|
@@ -177,14 +177,14 @@ export const autoLogin = createAsyncThunk(
|
|
| 177 |
token: token,
|
| 178 |
user: response.data.user
|
| 179 |
}, rememberMe);
|
| 180 |
-
|
| 181 |
// Ensure cookies are set
|
| 182 |
await cookieService.setAuthTokens(token, rememberMe);
|
| 183 |
-
|
| 184 |
if (isDevelopment) {
|
| 185 |
console.log('✅ [Auth] Auto login successful');
|
| 186 |
}
|
| 187 |
-
|
| 188 |
return {
|
| 189 |
success: true,
|
| 190 |
user: response.data.user,
|
|
@@ -202,7 +202,7 @@ export const autoLogin = createAsyncThunk(
|
|
| 202 |
}
|
| 203 |
}
|
| 204 |
}
|
| 205 |
-
|
| 206 |
if (isDevelopment) {
|
| 207 |
console.log('🔐 [Auth] Auto login failed - no valid token found');
|
| 208 |
}
|
|
@@ -211,11 +211,11 @@ export const autoLogin = createAsyncThunk(
|
|
| 211 |
// Clear tokens on error
|
| 212 |
localStorage.removeItem('token');
|
| 213 |
await cookieService.clearAuthTokens();
|
| 214 |
-
|
| 215 |
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 216 |
console.error('🔐 [Auth] Auto login error:', error);
|
| 217 |
}
|
| 218 |
-
|
| 219 |
return rejectWithValue('Auto login failed');
|
| 220 |
}
|
| 221 |
}
|
|
@@ -229,10 +229,12 @@ export const registerUser = createAsyncThunk(
|
|
| 229 |
// Send only the data that Supabase needs
|
| 230 |
const transformedData = {
|
| 231 |
email: userData.email,
|
| 232 |
-
password: userData.password
|
|
|
|
|
|
|
| 233 |
// Note: confirmPassword is not sent to Supabase as it handles validation automatically
|
| 234 |
};
|
| 235 |
-
|
| 236 |
// Debug log to verify transformation
|
| 237 |
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 238 |
console.log('🔄 [Auth] Transforming registration data:', {
|
|
@@ -240,7 +242,7 @@ export const registerUser = createAsyncThunk(
|
|
| 240 |
transformed: transformedData
|
| 241 |
});
|
| 242 |
}
|
| 243 |
-
|
| 244 |
const response = await authService.register(transformedData);
|
| 245 |
return response.data;
|
| 246 |
} catch (error) {
|
|
@@ -255,7 +257,7 @@ export const loginUser = createAsyncThunk(
|
|
| 255 |
try {
|
| 256 |
const response = await authService.login(credentials);
|
| 257 |
const result = response.data;
|
| 258 |
-
|
| 259 |
if (result.success) {
|
| 260 |
// Store auth data in cache
|
| 261 |
const rememberMe = credentials.rememberMe || false;
|
|
@@ -263,17 +265,17 @@ export const loginUser = createAsyncThunk(
|
|
| 263 |
token: result.token,
|
| 264 |
user: result.user
|
| 265 |
}, rememberMe);
|
| 266 |
-
|
| 267 |
// Store tokens in secure cookies
|
| 268 |
await cookieService.setAuthTokens(result.token, rememberMe);
|
| 269 |
-
|
| 270 |
return {
|
| 271 |
...result,
|
| 272 |
rememberMe,
|
| 273 |
expiresAt: rememberMe ? Date.now() + (7 * 24 * 60 * 60 * 1000) : Date.now() + (60 * 60 * 1000)
|
| 274 |
};
|
| 275 |
}
|
| 276 |
-
|
| 277 |
return result;
|
| 278 |
} catch (error) {
|
| 279 |
return rejectWithValue(error.response?.data || { success: false, message: 'Login failed' });
|
|
@@ -311,10 +313,10 @@ export const logoutUser = createAsyncThunk(
|
|
| 311 |
try {
|
| 312 |
// Clear cache first
|
| 313 |
await cacheService.clearAuthCache();
|
| 314 |
-
|
| 315 |
// Clear cookies
|
| 316 |
await cookieService.clearAuthTokens();
|
| 317 |
-
|
| 318 |
// Then call logout API
|
| 319 |
const response = await authService.logout();
|
| 320 |
return response.data;
|
|
@@ -349,14 +351,14 @@ const authSlice = createSlice({
|
|
| 349 |
state.error = null;
|
| 350 |
state.loading = 'idle';
|
| 351 |
},
|
| 352 |
-
|
| 353 |
setUser: (state, action) => {
|
| 354 |
state.user = action.payload.user;
|
| 355 |
state.isAuthenticated = true;
|
| 356 |
state.loading = 'succeeded';
|
| 357 |
state.error = null;
|
| 358 |
},
|
| 359 |
-
|
| 360 |
clearAuth: (state) => {
|
| 361 |
state.user = null;
|
| 362 |
state.isAuthenticated = false;
|
|
@@ -374,15 +376,15 @@ const authSlice = createSlice({
|
|
| 374 |
deviceFingerprint: null
|
| 375 |
};
|
| 376 |
},
|
| 377 |
-
|
| 378 |
updateSecurityStatus: (state, action) => {
|
| 379 |
state.security = { ...state.security, ...action.payload };
|
| 380 |
},
|
| 381 |
-
|
| 382 |
updateCacheInfo: (state, action) => {
|
| 383 |
state.cache = { ...state.cache, ...action.payload };
|
| 384 |
},
|
| 385 |
-
|
| 386 |
setRememberMe: (state, action) => {
|
| 387 |
state.cache.isRemembered = action.payload;
|
| 388 |
}
|
|
@@ -440,7 +442,7 @@ const authSlice = createSlice({
|
|
| 440 |
.addCase(registerUser.fulfilled, (state, action) => {
|
| 441 |
state.loading = 'succeeded';
|
| 442 |
state.user = action.payload.user;
|
| 443 |
-
|
| 444 |
// Check if email confirmation is required
|
| 445 |
if (action.payload.requires_confirmation) {
|
| 446 |
state.isAuthenticated = false;
|
|
@@ -451,11 +453,11 @@ const authSlice = createSlice({
|
|
| 451 |
})
|
| 452 |
.addCase(registerUser.rejected, (state, action) => {
|
| 453 |
state.loading = 'failed';
|
| 454 |
-
|
| 455 |
// Handle different error types with specific messages
|
| 456 |
const errorPayload = action.payload;
|
| 457 |
let errorMessage = 'Registration failed';
|
| 458 |
-
|
| 459 |
if (errorPayload) {
|
| 460 |
if (errorPayload.message) {
|
| 461 |
// Check for specific error types
|
|
@@ -473,7 +475,7 @@ const authSlice = createSlice({
|
|
| 473 |
errorMessage = errorPayload;
|
| 474 |
}
|
| 475 |
}
|
| 476 |
-
|
| 477 |
state.error = errorMessage;
|
| 478 |
})
|
| 479 |
|
|
@@ -488,18 +490,18 @@ const authSlice = createSlice({
|
|
| 488 |
state.isAuthenticated = true;
|
| 489 |
state.cache.isRemembered = action.payload.rememberMe || false;
|
| 490 |
state.cache.expiresAt = action.payload.expiresAt;
|
| 491 |
-
|
| 492 |
// Store token securely
|
| 493 |
localStorage.setItem('token', action.payload.token);
|
| 494 |
})
|
| 495 |
.addCase(loginUser.rejected, (state, action) => {
|
| 496 |
state.loading = 'failed';
|
| 497 |
state.security.failedAttempts += 1;
|
| 498 |
-
|
| 499 |
// Handle different error types with specific messages
|
| 500 |
const errorPayload = action.payload;
|
| 501 |
let errorMessage = 'Login failed';
|
| 502 |
-
|
| 503 |
if (errorPayload) {
|
| 504 |
if (errorPayload.message) {
|
| 505 |
// Check for specific error types
|
|
@@ -517,7 +519,7 @@ const authSlice = createSlice({
|
|
| 517 |
errorMessage = errorPayload;
|
| 518 |
}
|
| 519 |
}
|
| 520 |
-
|
| 521 |
console.log('Setting Redux error:', errorMessage);
|
| 522 |
state.error = errorMessage;
|
| 523 |
})
|
|
@@ -530,7 +532,7 @@ const authSlice = createSlice({
|
|
| 530 |
state.cache.isRemembered = false;
|
| 531 |
state.cache.expiresAt = null;
|
| 532 |
state.cache.deviceFingerprint = null;
|
| 533 |
-
|
| 534 |
// Clear all cached data (already done in the thunk)
|
| 535 |
localStorage.removeItem('token');
|
| 536 |
})
|
|
@@ -541,7 +543,7 @@ const authSlice = createSlice({
|
|
| 541 |
state.cache.isRemembered = false;
|
| 542 |
state.cache.expiresAt = null;
|
| 543 |
state.cache.deviceFingerprint = null;
|
| 544 |
-
|
| 545 |
localStorage.removeItem('token');
|
| 546 |
})
|
| 547 |
|
|
@@ -556,11 +558,11 @@ const authSlice = createSlice({
|
|
| 556 |
})
|
| 557 |
.addCase(forgotPassword.rejected, (state, action) => {
|
| 558 |
state.loading = 'failed';
|
| 559 |
-
|
| 560 |
// Handle different error types with specific messages
|
| 561 |
const errorPayload = action.payload;
|
| 562 |
let errorMessage = 'Password reset request failed';
|
| 563 |
-
|
| 564 |
if (errorPayload) {
|
| 565 |
if (errorPayload.message) {
|
| 566 |
errorMessage = errorPayload.message;
|
|
@@ -568,7 +570,7 @@ const authSlice = createSlice({
|
|
| 568 |
errorMessage = errorPayload;
|
| 569 |
}
|
| 570 |
}
|
| 571 |
-
|
| 572 |
state.error = errorMessage;
|
| 573 |
})
|
| 574 |
|
|
@@ -583,11 +585,11 @@ const authSlice = createSlice({
|
|
| 583 |
})
|
| 584 |
.addCase(resetPassword.rejected, (state, action) => {
|
| 585 |
state.loading = 'failed';
|
| 586 |
-
|
| 587 |
// Handle different error types with specific messages
|
| 588 |
const errorPayload = action.payload;
|
| 589 |
let errorMessage = 'Password reset failed';
|
| 590 |
-
|
| 591 |
if (errorPayload) {
|
| 592 |
if (errorPayload.message) {
|
| 593 |
// Check for specific error types
|
|
@@ -603,7 +605,7 @@ const authSlice = createSlice({
|
|
| 603 |
errorMessage = errorPayload;
|
| 604 |
}
|
| 605 |
}
|
| 606 |
-
|
| 607 |
state.error = errorMessage;
|
| 608 |
})
|
| 609 |
|
|
|
|
| 28 |
async (_, { rejectWithValue }) => {
|
| 29 |
try {
|
| 30 |
const isDevelopment = import.meta.env.VITE_NODE_ENV === 'development';
|
| 31 |
+
|
| 32 |
if (isDevelopment) {
|
| 33 |
console.log('🔐 [Auth] Starting cached authentication check');
|
| 34 |
}
|
| 35 |
+
|
| 36 |
// First check cache
|
| 37 |
const cachedAuth = await cacheService.getAuthCache();
|
| 38 |
if (cachedAuth) {
|
| 39 |
if (isDevelopment) {
|
| 40 |
console.log('🗃️ [Cache] Found cached authentication data');
|
| 41 |
}
|
| 42 |
+
|
| 43 |
// Validate that the cached token is still valid by checking expiry
|
| 44 |
if (cachedAuth.expiresAt && Date.now() < cachedAuth.expiresAt) {
|
| 45 |
return {
|
|
|
|
| 58 |
await cacheService.clearAuthCache();
|
| 59 |
}
|
| 60 |
}
|
| 61 |
+
|
| 62 |
// If not in cache or expired, check cookies
|
| 63 |
if (isDevelopment) {
|
| 64 |
console.log('🍪 [Cookie] Checking for authentication cookies');
|
| 65 |
}
|
| 66 |
+
|
| 67 |
const cookieAuth = await cookieService.getAuthTokens();
|
| 68 |
if (cookieAuth?.accessToken) {
|
| 69 |
if (isDevelopment) {
|
| 70 |
console.log('🍪 [Cookie] Found authentication cookies, validating with API');
|
| 71 |
}
|
| 72 |
+
|
| 73 |
// Validate token and get user data
|
| 74 |
try {
|
| 75 |
const response = await authService.getCurrentUser();
|
|
|
|
| 79 |
token: cookieAuth.accessToken,
|
| 80 |
user: response.data.user
|
| 81 |
}, cookieAuth.rememberMe);
|
| 82 |
+
|
| 83 |
const expiresAt = cookieAuth.rememberMe ?
|
| 84 |
Date.now() + (7 * 24 * 60 * 60 * 1000) :
|
| 85 |
Date.now() + (60 * 60 * 1000);
|
| 86 |
+
|
| 87 |
if (isDevelopment) {
|
| 88 |
console.log('✅ [Auth] Cookie authentication validated successfully');
|
| 89 |
}
|
| 90 |
+
|
| 91 |
return {
|
| 92 |
success: true,
|
| 93 |
user: response.data.user,
|
|
|
|
| 113 |
console.log('🍪 [Cookie] No authentication cookies found');
|
| 114 |
}
|
| 115 |
}
|
| 116 |
+
|
| 117 |
if (isDevelopment) {
|
| 118 |
console.log('🔐 [Auth] No valid cached or cookie authentication found');
|
| 119 |
}
|
|
|
|
| 136 |
if (isDevelopment) {
|
| 137 |
console.log('🔐 [Auth] Starting auto login process');
|
| 138 |
}
|
| 139 |
+
|
| 140 |
// Try to get token from cookies first, then fallback to localStorage
|
| 141 |
let token = null;
|
| 142 |
let rememberMe = false;
|
| 143 |
+
|
| 144 |
try {
|
| 145 |
const cookieAuth = await cookieService.getAuthTokens();
|
| 146 |
token = cookieAuth?.accessToken;
|
| 147 |
rememberMe = cookieAuth?.rememberMe || false;
|
| 148 |
+
|
| 149 |
if (isDevelopment) {
|
| 150 |
console.log('🍪 [Cookie] Got tokens from cookie service:', { token: !!token, rememberMe });
|
| 151 |
}
|
|
|
|
| 154 |
console.warn('🍪 [Cookie] Error getting cookie tokens, trying localStorage:', cookieError.message);
|
| 155 |
}
|
| 156 |
}
|
| 157 |
+
|
| 158 |
// If no cookie token, try localStorage
|
| 159 |
if (!token) {
|
| 160 |
token = localStorage.getItem('token');
|
|
|
|
| 162 |
console.log('💾 [Storage] Got token from localStorage:', !!token);
|
| 163 |
}
|
| 164 |
}
|
| 165 |
+
|
| 166 |
if (token) {
|
| 167 |
try {
|
| 168 |
// Try to validate token and get user data
|
| 169 |
if (isDevelopment) {
|
| 170 |
console.log('🔑 [Token] Validating token with API');
|
| 171 |
}
|
| 172 |
+
|
| 173 |
const response = await authService.getCurrentUser();
|
| 174 |
if (response.data.success) {
|
| 175 |
// Update cache and cookies
|
|
|
|
| 177 |
token: token,
|
| 178 |
user: response.data.user
|
| 179 |
}, rememberMe);
|
| 180 |
+
|
| 181 |
// Ensure cookies are set
|
| 182 |
await cookieService.setAuthTokens(token, rememberMe);
|
| 183 |
+
|
| 184 |
if (isDevelopment) {
|
| 185 |
console.log('✅ [Auth] Auto login successful');
|
| 186 |
}
|
| 187 |
+
|
| 188 |
return {
|
| 189 |
success: true,
|
| 190 |
user: response.data.user,
|
|
|
|
| 202 |
}
|
| 203 |
}
|
| 204 |
}
|
| 205 |
+
|
| 206 |
if (isDevelopment) {
|
| 207 |
console.log('🔐 [Auth] Auto login failed - no valid token found');
|
| 208 |
}
|
|
|
|
| 211 |
// Clear tokens on error
|
| 212 |
localStorage.removeItem('token');
|
| 213 |
await cookieService.clearAuthTokens();
|
| 214 |
+
|
| 215 |
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 216 |
console.error('🔐 [Auth] Auto login error:', error);
|
| 217 |
}
|
| 218 |
+
|
| 219 |
return rejectWithValue('Auto login failed');
|
| 220 |
}
|
| 221 |
}
|
|
|
|
| 229 |
// Send only the data that Supabase needs
|
| 230 |
const transformedData = {
|
| 231 |
email: userData.email,
|
| 232 |
+
password: userData.password,
|
| 233 |
+
country: userData.country, // User's selected country code
|
| 234 |
+
language: userData.language // User's selected language code
|
| 235 |
// Note: confirmPassword is not sent to Supabase as it handles validation automatically
|
| 236 |
};
|
| 237 |
+
|
| 238 |
// Debug log to verify transformation
|
| 239 |
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
| 240 |
console.log('🔄 [Auth] Transforming registration data:', {
|
|
|
|
| 242 |
transformed: transformedData
|
| 243 |
});
|
| 244 |
}
|
| 245 |
+
|
| 246 |
const response = await authService.register(transformedData);
|
| 247 |
return response.data;
|
| 248 |
} catch (error) {
|
|
|
|
| 257 |
try {
|
| 258 |
const response = await authService.login(credentials);
|
| 259 |
const result = response.data;
|
| 260 |
+
|
| 261 |
if (result.success) {
|
| 262 |
// Store auth data in cache
|
| 263 |
const rememberMe = credentials.rememberMe || false;
|
|
|
|
| 265 |
token: result.token,
|
| 266 |
user: result.user
|
| 267 |
}, rememberMe);
|
| 268 |
+
|
| 269 |
// Store tokens in secure cookies
|
| 270 |
await cookieService.setAuthTokens(result.token, rememberMe);
|
| 271 |
+
|
| 272 |
return {
|
| 273 |
...result,
|
| 274 |
rememberMe,
|
| 275 |
expiresAt: rememberMe ? Date.now() + (7 * 24 * 60 * 60 * 1000) : Date.now() + (60 * 60 * 1000)
|
| 276 |
};
|
| 277 |
}
|
| 278 |
+
|
| 279 |
return result;
|
| 280 |
} catch (error) {
|
| 281 |
return rejectWithValue(error.response?.data || { success: false, message: 'Login failed' });
|
|
|
|
| 313 |
try {
|
| 314 |
// Clear cache first
|
| 315 |
await cacheService.clearAuthCache();
|
| 316 |
+
|
| 317 |
// Clear cookies
|
| 318 |
await cookieService.clearAuthTokens();
|
| 319 |
+
|
| 320 |
// Then call logout API
|
| 321 |
const response = await authService.logout();
|
| 322 |
return response.data;
|
|
|
|
| 351 |
state.error = null;
|
| 352 |
state.loading = 'idle';
|
| 353 |
},
|
| 354 |
+
|
| 355 |
setUser: (state, action) => {
|
| 356 |
state.user = action.payload.user;
|
| 357 |
state.isAuthenticated = true;
|
| 358 |
state.loading = 'succeeded';
|
| 359 |
state.error = null;
|
| 360 |
},
|
| 361 |
+
|
| 362 |
clearAuth: (state) => {
|
| 363 |
state.user = null;
|
| 364 |
state.isAuthenticated = false;
|
|
|
|
| 376 |
deviceFingerprint: null
|
| 377 |
};
|
| 378 |
},
|
| 379 |
+
|
| 380 |
updateSecurityStatus: (state, action) => {
|
| 381 |
state.security = { ...state.security, ...action.payload };
|
| 382 |
},
|
| 383 |
+
|
| 384 |
updateCacheInfo: (state, action) => {
|
| 385 |
state.cache = { ...state.cache, ...action.payload };
|
| 386 |
},
|
| 387 |
+
|
| 388 |
setRememberMe: (state, action) => {
|
| 389 |
state.cache.isRemembered = action.payload;
|
| 390 |
}
|
|
|
|
| 442 |
.addCase(registerUser.fulfilled, (state, action) => {
|
| 443 |
state.loading = 'succeeded';
|
| 444 |
state.user = action.payload.user;
|
| 445 |
+
|
| 446 |
// Check if email confirmation is required
|
| 447 |
if (action.payload.requires_confirmation) {
|
| 448 |
state.isAuthenticated = false;
|
|
|
|
| 453 |
})
|
| 454 |
.addCase(registerUser.rejected, (state, action) => {
|
| 455 |
state.loading = 'failed';
|
| 456 |
+
|
| 457 |
// Handle different error types with specific messages
|
| 458 |
const errorPayload = action.payload;
|
| 459 |
let errorMessage = 'Registration failed';
|
| 460 |
+
|
| 461 |
if (errorPayload) {
|
| 462 |
if (errorPayload.message) {
|
| 463 |
// Check for specific error types
|
|
|
|
| 475 |
errorMessage = errorPayload;
|
| 476 |
}
|
| 477 |
}
|
| 478 |
+
|
| 479 |
state.error = errorMessage;
|
| 480 |
})
|
| 481 |
|
|
|
|
| 490 |
state.isAuthenticated = true;
|
| 491 |
state.cache.isRemembered = action.payload.rememberMe || false;
|
| 492 |
state.cache.expiresAt = action.payload.expiresAt;
|
| 493 |
+
|
| 494 |
// Store token securely
|
| 495 |
localStorage.setItem('token', action.payload.token);
|
| 496 |
})
|
| 497 |
.addCase(loginUser.rejected, (state, action) => {
|
| 498 |
state.loading = 'failed';
|
| 499 |
state.security.failedAttempts += 1;
|
| 500 |
+
|
| 501 |
// Handle different error types with specific messages
|
| 502 |
const errorPayload = action.payload;
|
| 503 |
let errorMessage = 'Login failed';
|
| 504 |
+
|
| 505 |
if (errorPayload) {
|
| 506 |
if (errorPayload.message) {
|
| 507 |
// Check for specific error types
|
|
|
|
| 519 |
errorMessage = errorPayload;
|
| 520 |
}
|
| 521 |
}
|
| 522 |
+
|
| 523 |
console.log('Setting Redux error:', errorMessage);
|
| 524 |
state.error = errorMessage;
|
| 525 |
})
|
|
|
|
| 532 |
state.cache.isRemembered = false;
|
| 533 |
state.cache.expiresAt = null;
|
| 534 |
state.cache.deviceFingerprint = null;
|
| 535 |
+
|
| 536 |
// Clear all cached data (already done in the thunk)
|
| 537 |
localStorage.removeItem('token');
|
| 538 |
})
|
|
|
|
| 543 |
state.cache.isRemembered = false;
|
| 544 |
state.cache.expiresAt = null;
|
| 545 |
state.cache.deviceFingerprint = null;
|
| 546 |
+
|
| 547 |
localStorage.removeItem('token');
|
| 548 |
})
|
| 549 |
|
|
|
|
| 558 |
})
|
| 559 |
.addCase(forgotPassword.rejected, (state, action) => {
|
| 560 |
state.loading = 'failed';
|
| 561 |
+
|
| 562 |
// Handle different error types with specific messages
|
| 563 |
const errorPayload = action.payload;
|
| 564 |
let errorMessage = 'Password reset request failed';
|
| 565 |
+
|
| 566 |
if (errorPayload) {
|
| 567 |
if (errorPayload.message) {
|
| 568 |
errorMessage = errorPayload.message;
|
|
|
|
| 570 |
errorMessage = errorPayload;
|
| 571 |
}
|
| 572 |
}
|
| 573 |
+
|
| 574 |
state.error = errorMessage;
|
| 575 |
})
|
| 576 |
|
|
|
|
| 585 |
})
|
| 586 |
.addCase(resetPassword.rejected, (state, action) => {
|
| 587 |
state.loading = 'failed';
|
| 588 |
+
|
| 589 |
// Handle different error types with specific messages
|
| 590 |
const errorPayload = action.payload;
|
| 591 |
let errorMessage = 'Password reset failed';
|
| 592 |
+
|
| 593 |
if (errorPayload) {
|
| 594 |
if (errorPayload.message) {
|
| 595 |
// Check for specific error types
|
|
|
|
| 605 |
errorMessage = errorPayload;
|
| 606 |
}
|
| 607 |
}
|
| 608 |
+
|
| 609 |
state.error = errorMessage;
|
| 610 |
})
|
| 611 |
|