Zelyanoth commited on
Commit
b3c01df
·
1 Parent(s): cda4ebe

feat(auth): add country and language support to user registration

Browse files
Linkedin_poster_dev CHANGED
@@ -1 +1 @@
1
- Subproject commit 556c23cdbb7a7f9f7a2fe03deb092b7efc521653
 
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
- # Register user
53
- result = register_user(email, password)
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
- HUGGING_KEY = os.environ.get('HUGGING_KEY') or ''
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
- # Call the Hugging Face model to generate content
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>&copy; 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>&copy; 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