Zelyanoth commited on
Commit
25f22bf
·
1 Parent(s): e09e580
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.hf +9 -0
  2. .gitignore +168 -0
  3. Dockerfile +31 -0
  4. FINAL_DOCUMENTATION.md +692 -0
  5. GEMINI.md +160 -0
  6. HEADER_CSS_ANALYSIS.md +128 -0
  7. HEADER_FIX_SUMMARY.md +68 -0
  8. HUGGING_FACE_DEPLOYMENT.md +197 -0
  9. IMPLEMENTATION_SUMMARY.md +215 -0
  10. LINKEDIN_AUTH_GUIDE.md +258 -0
  11. REACT_DEVELOPMENT_GUIDE.md +568 -0
  12. README.md +307 -6
  13. SETUP_GUIDE.md +628 -0
  14. UI_COMPONENT_SNAPSHOT.md +208 -0
  15. api_design.md +348 -0
  16. app.py +26 -0
  17. architecture_summary.md +111 -0
  18. backend/.env.example +27 -0
  19. backend/Dockerfile +40 -0
  20. backend/README.md +275 -0
  21. backend/TASK_SCHEDULING_EVOLUTION.md +124 -0
  22. backend/api/__init__.py +0 -0
  23. backend/api/accounts.py +311 -0
  24. backend/api/auth.py +186 -0
  25. backend/api/posts.py +574 -0
  26. backend/api/schedules.py +233 -0
  27. backend/api/sources.py +181 -0
  28. backend/app.py +139 -0
  29. backend/app.py.bak +141 -0
  30. backend/celery_app.py +35 -0
  31. backend/celery_beat_config.py +21 -0
  32. backend/celery_tasks/__init__.py +1 -0
  33. backend/celery_tasks/content_tasks.py +190 -0
  34. backend/celery_tasks/schedule_loader.py +162 -0
  35. backend/celery_tasks/scheduler.py +105 -0
  36. backend/config.py +78 -0
  37. backend/models/__init__.py +0 -0
  38. backend/models/post.py +42 -0
  39. backend/models/schedule.py +33 -0
  40. backend/models/social_account.py +48 -0
  41. backend/models/source.py +36 -0
  42. backend/models/user.py +30 -0
  43. backend/requirements.txt +17 -0
  44. backend/scheduler/__init__.py +0 -0
  45. backend/scheduler/task_scheduler.py +269 -0
  46. backend/scheduler/task_scheduler.py.bak +252 -0
  47. backend/services/__init__.py +0 -0
  48. backend/services/auth_service.py +152 -0
  49. backend/services/content_service.py +145 -0
  50. backend/services/linkedin_service.py +181 -0
.env.hf ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Spaces environment variables
2
+ # This file is for reference only. Set actual values in Hugging Face Spaces secrets.
3
+
4
+ # Celery configuration for Redis
5
+ CELERY_BROKER_URL=redis://localhost:6379/0
6
+ CELERY_RESULT_BACKEND=redis://localhost:6379/0
7
+
8
+ # Port for Hugging Face Spaces
9
+ PORT=7860
.gitignore ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+ MANIFEST
27
+
28
+ # PyInstaller
29
+ # Usually these files are written by a python script from a template
30
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
31
+ *.manifest
32
+ *.spec
33
+
34
+ # Installer logs
35
+ pip-log.txt
36
+ pip-delete-this-directory.txt
37
+
38
+ # Unit test / coverage reports
39
+ htmlcov/
40
+ .tox/
41
+ .nox/
42
+ .coverage
43
+ .coverage.*
44
+ .cache
45
+ nosetests.xml
46
+ coverage.xml
47
+ *.cover
48
+ *.py,cover
49
+ .hypothesis/
50
+ .pytest_cache/
51
+
52
+ # Translations
53
+ *.mo
54
+ *.pot
55
+
56
+ # Django stuff:
57
+ *.log
58
+ local_settings.py
59
+ db.sqlite3
60
+ db.sqlite3-journal
61
+
62
+ # Flask stuff:
63
+ instance/
64
+ .webassets-cache
65
+
66
+ # Scrapy stuff:
67
+ .scrapy
68
+
69
+ # Sphinx documentation
70
+ docs/_build/
71
+
72
+ # PyBuilder
73
+ target/
74
+
75
+ # Jupyter Notebook
76
+ .ipynb_checkpoints
77
+
78
+ # IPython
79
+ profile_default/
80
+ ipython_config.py
81
+
82
+ # pyenv
83
+ .python-version
84
+
85
+ # pipenv
86
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
87
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
88
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
89
+ # install all needed dependencies.
90
+ #Pipfile.lock
91
+
92
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
93
+ __pypackages__/
94
+
95
+ # Celery stuff
96
+ celerybeat.pid
97
+ # Do NOT ignore celerybeat-schedule - we want to commit this for persistence when possible
98
+
99
+ # SageMath parsed files
100
+ *.sage.py
101
+
102
+ # Environments
103
+ .env
104
+ .venv
105
+ env/
106
+ venv/
107
+ ENV/
108
+ env.bak/
109
+ venv.bak/
110
+
111
+ # Spyder project settings
112
+ .spyderproject
113
+ .spyproject
114
+
115
+ # Rope project settings
116
+ .ropeproject
117
+
118
+ # mkdocs documentation
119
+ /site
120
+
121
+ # mypy
122
+ .mypy_cache/
123
+ .dmypy.json
124
+ dmypy.json
125
+
126
+ # Pyre type checker
127
+ .pyre/
128
+
129
+ # Node.js dependencies
130
+ node_modules/
131
+
132
+ # Vite build output
133
+ frontend/dist/
134
+ frontend/build/
135
+
136
+ # Local environment files
137
+ .env.local
138
+ .env.development.local
139
+ .env.test.local
140
+ .env.production.local
141
+
142
+ # VS Code
143
+ .vscode/
144
+
145
+ # IDE files
146
+ .idea/
147
+
148
+ # OS generated files
149
+ .DS_Store
150
+ .DS_Store?
151
+ ._*
152
+ .Spotlight-V100
153
+ .Trashes
154
+ ehthumbs.db
155
+ Thumbs.db
156
+
157
+ # Logs
158
+ *.log
159
+
160
+ # Temp files
161
+ *.tmp
162
+ *.temp
163
+
164
+ # Supabase
165
+ supabase/.temp/
166
+
167
+ # Docker
168
+ docker-compose.override.yml
Dockerfile ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ WORKDIR /app
4
+
5
+ # Install Node.js for frontend build
6
+ RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
7
+ RUN apt-get update && apt-get install -y nodejs
8
+
9
+ # Copy and install Python dependencies
10
+ COPY requirements.txt .
11
+ RUN pip install -r requirements.txt
12
+
13
+ # Copy package files for frontend
14
+ COPY frontend/package*.json ./frontend/
15
+ # Install frontend dependencies
16
+ RUN cd frontend && npm install
17
+
18
+ # Copy all files
19
+ COPY . .
20
+
21
+ # Build frontend
22
+ RUN cd frontend && npm run build
23
+
24
+ # Make the startup script executable
25
+ RUN chmod +x start_app.py
26
+
27
+ # Expose port
28
+ EXPOSE 7860
29
+
30
+ # Run the application
31
+ CMD ["python", "start_app.py"]
FINAL_DOCUMENTATION.md ADDED
@@ -0,0 +1,692 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Lin React Clone - Final Documentation
2
+
3
+ ## Project Overview
4
+
5
+ This documentation provides a comprehensive overview of the Lin React Clone project, which is a modern reimplementation of the original Taipy-based Lin application using a React frontend with a Flask API backend. The project maintains all core functionality while improving the architecture for better maintainability and scalability.
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Architecture Overview](#architecture-overview)
10
+ 2. [Backend Implementation](#backend-implementation)
11
+ 3. [Frontend Implementation](#frontend-implementation)
12
+ 4. [API Documentation](#api-documentation)
13
+ 5. [Deployment Guide](#deployment-guide)
14
+ 6. [Testing Strategy](#testing-strategy)
15
+ 7. [Future Enhancements](#future-enhancements)
16
+
17
+ ## Architecture Overview
18
+
19
+ ### System Components
20
+
21
+ The Lin React Clone consists of two main components:
22
+
23
+ 1. **Frontend (React)**
24
+ - User interface built with React
25
+ - State management with Redux Toolkit
26
+ - Responsive design for all device sizes
27
+ - Component-based architecture
28
+
29
+ 2. **Backend (Flask API)**
30
+ - RESTful API built with Flask
31
+ - Database integration with Supabase
32
+ - Task scheduling with APScheduler
33
+ - External API integrations (LinkedIn, Hugging Face)
34
+
35
+ ### Data Flow
36
+
37
+ ```
38
+ [React Frontend] ↔ [Flask API] ↔ [Supabase Database]
39
+
40
+ [External APIs: LinkedIn, Hugging Face]
41
+
42
+ [APScheduler Tasks]
43
+ ```
44
+
45
+ ### Technology Stack
46
+
47
+ #### Backend
48
+ - Flask (Python web framework)
49
+ - Supabase (Database and authentication)
50
+ - APScheduler (Task scheduling)
51
+ - requests (HTTP library)
52
+ - requests-oauthlib (OAuth support)
53
+ - gradio-client (Hugging Face API)
54
+ - Flask-JWT-Extended (JWT token management)
55
+
56
+ #### Frontend
57
+ - React (JavaScript library)
58
+ - Redux Toolkit (State management)
59
+ - React Router (Routing)
60
+ - Axios (HTTP client)
61
+ - Material-UI (UI components)
62
+
63
+ ## Backend Implementation
64
+
65
+ ### Project Structure
66
+
67
+ ```
68
+ backend/
69
+ ├── app.py # Flask application entry point
70
+ ├── config.py # Configuration settings
71
+ ├── requirements.txt # Python dependencies
72
+ ├── .env.example # Environment variables example
73
+ ├── models/ # Data models
74
+ │ ├── user.py # User model
75
+ │ ├── social_account.py # Social media account model
76
+ │ ├── source.py # RSS source model
77
+ │ ├── post.py # Post content model
78
+ │ └── schedule.py # Scheduling model
79
+ ├── api/ # API endpoints
80
+ │ ├── auth.py # Authentication endpoints
81
+ │ ├── sources.py # Source management endpoints
82
+ │ ├── accounts.py # Social account endpoints
83
+ │ ├── posts.py # Post management endpoints
84
+ │ └── schedules.py # Scheduling endpoints
85
+ ├── services/ # Business logic
86
+ │ ├── auth_service.py # Authentication service
87
+ │ ├── linkedin_service.py# LinkedIn integration service
88
+ │ ├── content_service.py # Content generation service
89
+ │ └── schedule_service.py# Scheduling service
90
+ ├── utils/ # Utility functions
91
+ │ └── database.py # Database connection
92
+ └── scheduler/ # Task scheduling
93
+ └── task_scheduler.py # Scheduling implementation
94
+ ```
95
+
96
+ ### Key Features
97
+
98
+ #### Authentication System
99
+ - JWT-based authentication with secure token management
100
+ - User registration with email confirmation
101
+ - User login/logout functionality
102
+ - Password hashing with bcrypt
103
+ - Supabase Auth integration
104
+
105
+ #### Source Management
106
+ - CRUD operations for RSS sources
107
+ - Integration with Supabase database
108
+ - Validation and error handling
109
+
110
+ #### Social Account Management
111
+ - LinkedIn OAuth2 integration
112
+ - Account linking and token storage
113
+ - Profile information retrieval
114
+
115
+ #### Post Management
116
+ - AI-powered content generation using Hugging Face API
117
+ - Post creation and storage
118
+ - LinkedIn publishing integration
119
+ - Image handling for posts
120
+
121
+ #### Scheduling System
122
+ - APScheduler for task management
123
+ - Recurring schedule creation
124
+ - Automatic content generation and publishing
125
+ - Conflict resolution for overlapping schedules
126
+
127
+ ## Frontend Implementation
128
+
129
+ ### Project Structure
130
+
131
+ ```
132
+ frontend/
133
+ ├── src/
134
+ │ ├── components/ # Reusable components
135
+ │ │ ├── Header/ # Application header
136
+ │ │ └── Sidebar/ # Navigation sidebar
137
+ │ ├── pages/ # Page components
138
+ │ │ ├── Login.js # Login page
139
+ │ │ ├── Register.js # Registration page
140
+ │ │ ├── Dashboard.js # Dashboard page
141
+ │ │ ├── Sources.js # Source management page
142
+ │ │ ├── Posts.js # Post management page
143
+ │ │ └── Schedule.js # Scheduling page
144
+ │ ├── services/ # API service layer
145
+ │ │ ├── api.js # Axios instance and interceptors
146
+ │ │ ├── authService.js # Authentication API calls
147
+ │ │ ├── sourceService.js# Source management API calls
148
+ │ │ ├── accountService.js# Account management API calls
149
+ │ │ ├── postService.js # Post management API calls
150
+ │ │ └── scheduleService.js# Scheduling API calls
151
+ │ ├── store/ # Redux store
152
+ │ │ ├── index.js # Store configuration
153
+ │ │ └── reducers/ # Redux reducers and actions
154
+ │ ├── App.js # Main application component
155
+ │ ├── App.css # Global application styles
156
+ │ ├── index.js # Application entry point
157
+ │ └── index.css # Global CSS styles
158
+ ├── public/ # Static assets
159
+ └── package.json # Project dependencies and scripts
160
+ ```
161
+
162
+ ### Key Features
163
+
164
+ #### Authentication System
165
+ - Login and registration forms
166
+ - JWT token management in localStorage
167
+ - Protected routes
168
+ - User session management
169
+
170
+ #### Dashboard
171
+ - Overview statistics
172
+ - Recent activity display
173
+ - Quick action buttons
174
+
175
+ #### Source Management
176
+ - Add/delete RSS sources
177
+ - List view of all sources
178
+ - Form validation
179
+
180
+ #### Post Management
181
+ - AI content generation interface
182
+ - Post creation form
183
+ - Draft and published post management
184
+ - Publish and delete functionality
185
+
186
+ #### Scheduling
187
+ - Schedule creation form with time selection
188
+ - Day selection interface
189
+ - List view of all schedules
190
+ - Delete functionality
191
+
192
+ ## API Documentation
193
+
194
+ ### Authentication Endpoints
195
+
196
+ #### POST /api/auth/register
197
+ Register a new user
198
+
199
+ **Request Body:**
200
+ ```json
201
+ {
202
+ "email": "string",
203
+ "password": "string",
204
+ "confirm_password": "string"
205
+ }
206
+ ```
207
+
208
+ **Response:**
209
+ ```json
210
+ {
211
+ "success": true,
212
+ "message": "User registered successfully",
213
+ "user": {
214
+ "id": "string",
215
+ "email": "string",
216
+ "created_at": "datetime"
217
+ }
218
+ }
219
+ ```
220
+
221
+ #### POST /api/auth/login
222
+ Login user
223
+
224
+ **Request Body:**
225
+ ```json
226
+ {
227
+ "email": "string",
228
+ "password": "string"
229
+ }
230
+ ```
231
+
232
+ **Response:**
233
+ ```json
234
+ {
235
+ "success": true,
236
+ "token": "string",
237
+ "user": {
238
+ "id": "string",
239
+ "email": "string"
240
+ }
241
+ }
242
+ ```
243
+
244
+ #### POST /api/auth/logout
245
+ Logout user
246
+
247
+ **Response:**
248
+ ```json
249
+ {
250
+ "success": true,
251
+ "message": "Logged out successfully"
252
+ }
253
+ ```
254
+
255
+ #### GET /api/auth/user
256
+ Get current user
257
+
258
+ **Response:**
259
+ ```json
260
+ {
261
+ "success": true,
262
+ "user": {
263
+ "id": "string",
264
+ "email": "string"
265
+ }
266
+ }
267
+ ```
268
+
269
+ ### Source Endpoints
270
+
271
+ #### GET /api/sources
272
+ Get all sources for current user
273
+
274
+ **Response:**
275
+ ```json
276
+ {
277
+ "success": true,
278
+ "sources": [
279
+ {
280
+ "id": "string",
281
+ "user_id": "string",
282
+ "source": "string",
283
+ "category": "string",
284
+ "last_update": "datetime",
285
+ "created_at": "datetime"
286
+ }
287
+ ]
288
+ }
289
+ ```
290
+
291
+ #### POST /api/sources
292
+ Add a new source
293
+
294
+ **Request Body:**
295
+ ```json
296
+ {
297
+ "source": "string"
298
+ }
299
+ ```
300
+
301
+ **Response:**
302
+ ```json
303
+ {
304
+ "success": true,
305
+ "source": {
306
+ "id": "string",
307
+ "user_id": "string",
308
+ "source": "string",
309
+ "category": "string",
310
+ "last_update": "datetime",
311
+ "created_at": "datetime"
312
+ }
313
+ }
314
+ ```
315
+
316
+ #### DELETE /api/sources/{id}
317
+ Delete a source
318
+
319
+ **Response:**
320
+ ```json
321
+ {
322
+ "success": true,
323
+ "message": "Source deleted successfully"
324
+ }
325
+ ```
326
+
327
+ ### Account Endpoints
328
+
329
+ #### GET /api/accounts
330
+ Get all social accounts for current user
331
+
332
+ **Response:**
333
+ ```json
334
+ {
335
+ "success": true,
336
+ "accounts": [
337
+ {
338
+ "id": "string",
339
+ "user_id": "string",
340
+ "social_network": "string",
341
+ "account_name": "string",
342
+ "created_at": "datetime"
343
+ }
344
+ ]
345
+ }
346
+ ```
347
+
348
+ #### POST /api/accounts
349
+ Add a new social account
350
+
351
+ **Request Body:**
352
+ ```json
353
+ {
354
+ "account_name": "string",
355
+ "social_network": "string"
356
+ }
357
+ ```
358
+
359
+ **Response:**
360
+ ```json
361
+ {
362
+ "success": true,
363
+ "account": {
364
+ "id": "string",
365
+ "user_id": "string",
366
+ "social_network": "string",
367
+ "account_name": "string",
368
+ "created_at": "datetime"
369
+ }
370
+ }
371
+ ```
372
+
373
+ #### DELETE /api/accounts/{id}
374
+ Delete a social account
375
+
376
+ **Response:**
377
+ ```json
378
+ {
379
+ "success": true,
380
+ "message": "Account deleted successfully"
381
+ }
382
+ ```
383
+
384
+ ### Post Endpoints
385
+
386
+ #### GET /api/posts
387
+ Get all posts for current user
388
+
389
+ **Query Parameters:**
390
+ - `published` (boolean): Filter by published status
391
+
392
+ **Response:**
393
+ ```json
394
+ {
395
+ "success": true,
396
+ "posts": [
397
+ {
398
+ "id": "string",
399
+ "social_account_id": "string",
400
+ "text_content": "string",
401
+ "is_published": "boolean",
402
+ "sched": "string",
403
+ "image_content_url": "string",
404
+ "created_at": "datetime",
405
+ "scheduled_at": "datetime"
406
+ }
407
+ ]
408
+ }
409
+ ```
410
+
411
+ #### POST /api/posts/generate
412
+ Generate AI content
413
+
414
+ **Response:**
415
+ ```json
416
+ {
417
+ "success": true,
418
+ "content": "string"
419
+ }
420
+ ```
421
+
422
+ #### POST /api/posts
423
+ Create a new post
424
+
425
+ **Request Body:**
426
+ ```json
427
+ {
428
+ "social_account_id": "string",
429
+ "text_content": "string",
430
+ "image_content_url": "string",
431
+ "scheduled_at": "datetime"
432
+ }
433
+ ```
434
+
435
+ **Response:**
436
+ ```json
437
+ {
438
+ "success": true,
439
+ "post": {
440
+ "id": "string",
441
+ "social_account_id": "string",
442
+ "text_content": "string",
443
+ "is_published": "boolean",
444
+ "sched": "string",
445
+ "image_content_url": "string",
446
+ "created_at": "datetime",
447
+ "scheduled_at": "datetime"
448
+ }
449
+ }
450
+ ```
451
+
452
+ #### POST /api/posts/{id}/publish
453
+ Publish a post
454
+
455
+ **Response:**
456
+ ```json
457
+ {
458
+ "success": true,
459
+ "message": "Post published successfully"
460
+ }
461
+ ```
462
+
463
+ #### DELETE /api/posts/{id}
464
+ Delete a post
465
+
466
+ **Response:**
467
+ ```json
468
+ {
469
+ "success": true,
470
+ "message": "Post deleted successfully"
471
+ }
472
+ ```
473
+
474
+ ### Schedule Endpoints
475
+
476
+ #### GET /api/schedules
477
+ Get all schedules for current user
478
+
479
+ **Response:**
480
+ ```json
481
+ {
482
+ "success": true,
483
+ "schedules": [
484
+ {
485
+ "id": "string",
486
+ "social_account_id": "string",
487
+ "schedule_time": "string",
488
+ "adjusted_time": "string",
489
+ "created_at": "datetime"
490
+ }
491
+ ]
492
+ }
493
+ ```
494
+
495
+ #### POST /api/schedules
496
+ Create a new schedule
497
+
498
+ **Request Body:**
499
+ ```json
500
+ {
501
+ "social_network": "string",
502
+ "schedule_time": "string",
503
+ "days": ["string"]
504
+ }
505
+ ```
506
+
507
+ **Response:**
508
+ ```json
509
+ {
510
+ "success": true,
511
+ "schedules": [
512
+ {
513
+ "id": "string",
514
+ "social_account_id": "string",
515
+ "schedule_time": "string",
516
+ "adjusted_time": "string",
517
+ "created_at": "datetime"
518
+ }
519
+ ]
520
+ }
521
+ ```
522
+
523
+ #### DELETE /api/schedules/{id}
524
+ Delete a schedule
525
+
526
+ **Response:**
527
+ ```json
528
+ {
529
+ "success": true,
530
+ "message": "Schedule deleted successfully"
531
+ }
532
+ ```
533
+
534
+ ## Deployment Guide
535
+
536
+ ### Backend Deployment
537
+
538
+ 1. **Environment Setup**
539
+ ```bash
540
+ # Copy environment example
541
+ cp .env.example .env
542
+
543
+ # Edit .env with your values
544
+ ```
545
+
546
+ 2. **Install Dependencies**
547
+ ```bash
548
+ pip install -r requirements.txt
549
+ ```
550
+
551
+ 3. **Run Application**
552
+ ```bash
553
+ python app.py
554
+ ```
555
+
556
+ 4. **Docker Deployment**
557
+ ```bash
558
+ docker build -t lin-backend .
559
+ docker run -p 5000:5000 lin-backend
560
+ ```
561
+
562
+ ### Frontend Deployment
563
+
564
+ 1. **Install Dependencies**
565
+ ```bash
566
+ npm install
567
+ ```
568
+
569
+ 2. **Build for Production**
570
+ ```bash
571
+ npm run build
572
+ ```
573
+
574
+ 3. **Serve Build**
575
+ ```bash
576
+ npm install -g serve
577
+ serve -s build
578
+ ```
579
+
580
+ ### Environment Variables
581
+
582
+ #### Backend (.env)
583
+ ```
584
+ SUPABASE_URL=your_supabase_project_url
585
+ SUPABASE_KEY=your_supabase_api_key
586
+ CLIENT_ID=your_linkedin_client_id
587
+ CLIENT_SECRET=your_linkedin_client_secret
588
+ REDIRECT_URL=your_redirect_url
589
+ HUGGING_KEY=your_hugging_face_api_key
590
+ JWT_SECRET_KEY=your_jwt_secret_key
591
+ SECRET_KEY=your_secret_key
592
+ DEBUG=True
593
+ SCHEDULER_ENABLED=True
594
+ PORT=5000
595
+ ```
596
+
597
+ #### Frontend (.env)
598
+ ```
599
+ REACT_APP_API_URL=http://localhost:5000/api
600
+ ```
601
+
602
+ ## Testing Strategy
603
+
604
+ ### Backend Testing
605
+
606
+ 1. **Unit Tests**
607
+ - Model validation tests
608
+ - Service layer tests
609
+ - Utility function tests
610
+
611
+ 2. **Integration Tests**
612
+ - API endpoint tests
613
+ - Database integration tests
614
+ - External API integration tests
615
+
616
+ 3. **Test Commands**
617
+ ```bash
618
+ # Run all tests
619
+ pytest
620
+
621
+ # Run tests with coverage
622
+ pytest --cov=.
623
+ ```
624
+
625
+ ### Frontend Testing
626
+
627
+ 1. **Component Tests**
628
+ - Rendering tests
629
+ - User interaction tests
630
+ - State management tests
631
+
632
+ 2. **Integration Tests**
633
+ - Form submission tests
634
+ - API integration tests
635
+ - Routing tests
636
+
637
+ 3. **Test Commands**
638
+ ```bash
639
+ # Run all tests
640
+ npm test
641
+
642
+ # Run tests in watch mode
643
+ npm test -- --watch
644
+ ```
645
+
646
+ ## Future Enhancements
647
+
648
+ ### Backend Enhancements
649
+ 1. **Advanced Analytics**
650
+ - Post performance tracking
651
+ - User engagement metrics
652
+ - Content effectiveness analysis
653
+
654
+ 2. **Multi-Platform Support**
655
+ - Twitter integration
656
+ - Facebook integration
657
+ - Instagram integration
658
+
659
+ 3. **Enhanced Scheduling**
660
+ - Advanced scheduling algorithms
661
+ - Timezone support
662
+ - Recurrence patterns
663
+
664
+ 4. **Performance Improvements**
665
+ - Caching strategies
666
+ - Database optimization
667
+ - Asynchronous processing
668
+
669
+ ### Frontend Enhancements
670
+ 1. **Advanced UI Components**
671
+ - Data visualization dashboards
672
+ - Real-time updates
673
+ - Drag-and-drop scheduling
674
+
675
+ 2. **Enhanced User Experience**
676
+ - Dark mode support
677
+ - Keyboard shortcuts
678
+ - Accessibility improvements
679
+
680
+ 3. **Mobile Enhancements**
681
+ - Progressive Web App (PWA) support
682
+ - Native mobile features
683
+ - Offline capabilities
684
+
685
+ 4. **Advanced Features**
686
+ - Content calendar view
687
+ - Team collaboration features
688
+ - Content approval workflows
689
+
690
+ ## Conclusion
691
+
692
+ The Lin React Clone project successfully reimplements the original Taipy-based application with a modern, scalable architecture. The separation of concerns between the frontend and backend allows for independent development and deployment, while the RESTful API design ensures clear communication between components. The implementation includes all core features of the original application while providing a foundation for future enhancements and improvements.
GEMINI.md ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Lin - Community Manager Assistant for LinkedIn
2
+
3
+ ## Project Overview
4
+
5
+ Lin is a comprehensive community management tool designed to help users automate and streamline their LinkedIn activities. The project follows a modern full-stack architecture with:
6
+
7
+ - **Frontend**: React application with Vite build system, utilizing Tailwind CSS for styling and Redux for state management
8
+ - **Backend**: Flask-based REST API with SQLAlchemy for database operations and Supabase for authentication
9
+ - **Key Features**: LinkedIn OAuth integration, content scheduling, post management, and analytics
10
+
11
+ ## Project Structure
12
+
13
+ ```
14
+ Lin/
15
+ ├── package.json # Root package.json with combined scripts
16
+ ├── frontend/ # React frontend application
17
+ │ ├── package.json # Frontend-specific dependencies
18
+ │ ├── src/ # React source code
19
+ │ ├── public/ # Static assets
20
+ │ └── build/ # Build output
21
+ ├── backend/ # Flask backend API
22
+ │ ├── app.py # Main application file
23
+ │ ├── requirements.txt # Python dependencies
24
+ │ ├── api/ # API endpoints
25
+ │ ├── models/ # Data models
26
+ │ ├── services/ # Business logic
27
+ │ └── utils/ # Utility functions
28
+ └── README.md # Project documentation
29
+ ```
30
+
31
+ ## Building and Running
32
+
33
+ ### Prerequisites
34
+
35
+ - Node.js (v16 or higher)
36
+ - Python (v3.8 or higher)
37
+ - npm (v8 or higher)
38
+
39
+ ### Installation
40
+
41
+ **Option 1: Using the root package.json (Recommended)**
42
+
43
+ ```bash
44
+ # Install all dependencies
45
+ npm install
46
+
47
+ # Setup the project
48
+ npm run setup
49
+
50
+ # Start both frontend and backend
51
+ npm start
52
+ ```
53
+
54
+ **Option 2: Manual installation**
55
+
56
+ ```bash
57
+ # Install frontend dependencies
58
+ cd frontend
59
+ npm install
60
+
61
+ # Install backend dependencies
62
+ cd ../backend
63
+ pip install -r requirements.txt
64
+ ```
65
+
66
+ ### Development Servers
67
+
68
+ - `npm run dev:frontend` - Start frontend development server
69
+ - `npm run dev:backend` - Start backend development server
70
+ - `npm run dev:all` - Start both servers concurrently
71
+ - `npm start` - Alias for `npm run dev:all`
72
+
73
+ ### Build & Test
74
+
75
+ - `npm run build` - Build frontend for production
76
+ - `npm run preview` - Preview production build
77
+ - `npm run test` - Run frontend tests
78
+ - `npm run test:backend` - Run backend tests
79
+ - `npm run lint` - Run ESLint
80
+ - `npm run lint:fix` - Fix ESLint issues
81
+
82
+ ## Development Conventions
83
+
84
+ ### Frontend
85
+
86
+ - Built with React and Vite
87
+ - Uses Tailwind CSS for styling
88
+ - Implements Redux for state management
89
+ - Follows responsive design principles with mobile-first approach
90
+ - Uses React Router for navigation
91
+ - Implements proper error boundaries and loading states
92
+
93
+ ### Backend
94
+
95
+ - Built with Flask
96
+ - Uses Supabase for authentication and database
97
+ - Implements JWT for token-based authentication
98
+ - Uses SQLAlchemy for database operations
99
+ - Follows REST API design principles
100
+
101
+ ### UI Components
102
+
103
+ The application features several key UI components:
104
+
105
+ 1. **Header**: Contains the application logo and user profile/logout functionality
106
+ 2. **Sidebar**: Navigation menu with links to different sections of the app
107
+ 3. **Responsive Design**: Adapts to different screen sizes with special handling for mobile devices
108
+
109
+ ### Key Features
110
+
111
+ 1. **Authentication**: Login and registration functionality with JWT tokens
112
+ 2. **LinkedIn Integration**: OAuth integration for connecting LinkedIn accounts
113
+ 3. **Content Management**: Create, edit, and schedule posts
114
+ 4. **Analytics**: Dashboard with overview and analytics
115
+ 5. **Responsive UI**: Mobile-friendly design with optimized touch interactions
116
+
117
+ ## Environment Setup
118
+
119
+ ### Frontend Environment
120
+
121
+ ```bash
122
+ # Copy environment file
123
+ cd frontend
124
+ cp .env.example .env.local
125
+
126
+ # Edit environment variables
127
+ # Open .env.local and add your required values
128
+ ```
129
+
130
+ **Required Frontend Variables**:
131
+ - `REACT_APP_API_URL` - Backend API URL (default: http://localhost:5000)
132
+
133
+ ### Backend Environment
134
+
135
+ ```bash
136
+ # Copy environment file
137
+ cd backend
138
+ cp .env.example .env
139
+
140
+ # Edit environment variables
141
+ # Open .env and add your required values
142
+ ```
143
+
144
+ **Required Backend Variables**:
145
+ - `SUPABASE_URL` - Your Supabase project URL
146
+ - `SUPABASE_KEY` - Your Supabase API key
147
+ - `CLIENT_ID` - LinkedIn OAuth client ID
148
+ - `CLIENT_SECRET` - LinkedIn OAuth client secret
149
+ - `REDIRECT_URL` - LinkedIn OAuth redirect URL
150
+ - `HUGGING_KEY` - Hugging Face API key
151
+ - `JWT_SECRET_KEY` - Secret key for JWT token generation
152
+ - `SECRET_KEY` - Flask secret key
153
+ - `DEBUG` - Debug mode (True/False)
154
+ - `SCHEDULER_ENABLED` - Enable/disable task scheduler (True/False)
155
+ - `PORT` - Port to run the application on (default: 5000)
156
+
157
+ ## Development URLs
158
+
159
+ - **Frontend**: http://localhost:3000
160
+ - **Backend API**: http://localhost:5000
HEADER_CSS_ANALYSIS.md ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Header Component CSS Analysis Report
2
+
3
+ ## Overview
4
+
5
+ This report analyzes the Header component in the Lin application to identify potential alignment, spacing, and layout issues based on the current implementation and CSS styles.
6
+
7
+ ## Current Header Structure
8
+
9
+ The Header component uses a flex layout with three main sections:
10
+ 1. Logo and App Title (left)
11
+ 2. Desktop Navigation (center) - Currently empty
12
+ 3. User Profile and Logout (right)
13
+
14
+ ## Identified Issues
15
+
16
+ ### 1. Height Inconsistency
17
+ **Issue**: The Header component has a fixed height that changes between screen sizes (h-14 on mobile, h-16 on larger screens), but the CSS in header.css defines a fixed height of 4rem (64px).
18
+
19
+ **Files Affected**:
20
+ - `frontend/src/components/Header/Header.jsx` (line 47)
21
+ - `frontend/src/css/components/header.css` (line 10)
22
+
23
+ **Impact**: This inconsistency can cause layout shifts and alignment issues between different screen sizes.
24
+
25
+ ### 2. Vertical Alignment Issues
26
+ **Issue**: The flex container uses `items-center` for vertical alignment, but the varying heights between mobile (h-14 = 56px) and desktop (h-16 = 64px) can cause elements to appear misaligned.
27
+
28
+ **Files Affected**:
29
+ - `frontend/src/components/Header/Header.jsx` (line 47)
30
+
31
+ ### 3. Spacing Inconsistencies
32
+ **Issue**: The Header uses different spacing values for mobile and desktop:
33
+ - Logo section: `space-x-2` on mobile, `space-x-3` on larger screens
34
+ - User profile section: `space-x-3` on mobile, `space-x-4` on larger screens
35
+
36
+ **Files Affected**:
37
+ - `frontend/src/components/Header/Header.jsx` (lines 50, 73, 82)
38
+
39
+ ### 4. Responsive Breakpoint Mismatch
40
+ **Issue**: The Header component uses Tailwind's `lg:` prefix for desktop elements, but the CSS media queries in header.css use `max-width: 767px`. This creates a mismatch where elements might not display correctly at the 1024px breakpoint.
41
+
42
+ **Files Affected**:
43
+ - `frontend/src/components/Header/Header.jsx` (multiple lines)
44
+ - `frontend/src/css/components/header.css` (line 73)
45
+
46
+ ### 5. Z-Index Conflicts
47
+ **Issue**: The Header uses `z-50` in Tailwind classes, but the CSS defines a z-index of `var(--z-40)`. Additionally, the Sidebar has `z-index: var(--z-50)` in CSS, which could cause layering issues.
48
+
49
+ **Files Affected**:
50
+ - `frontend/src/components/Header/Header.jsx` (line 44)
51
+ - `frontend/src/css/components/header.css` (line 13)
52
+ - `frontend/src/css/components/sidebar.css` (line 13)
53
+
54
+ ### 6. Padding Inconsistencies
55
+ **Issue**: The header-content div uses responsive padding (`px-3 sm:px-4 lg:px-8`) but the CSS in header.css doesn't account for these variations.
56
+
57
+ **Files Affected**:
58
+ - `frontend/src/components/Header/Header.jsx` (line 45)
59
+ - `frontend/src/css/components/header.css` (line 19)
60
+
61
+ ### 7. Mobile Menu Button Alignment
62
+ **Issue**: The mobile menu button section uses `space-x-2` for spacing, but the user avatar and button have different styling between authenticated and unauthenticated states.
63
+
64
+ **Files Affected**:
65
+ - `frontend/src/components/Header/Header.jsx` (lines 111, 125)
66
+
67
+ ## Recommendations
68
+
69
+ ### 1. Standardize Height
70
+ **Solution**: Use a consistent height across all screen sizes or adjust the CSS to match the Tailwind classes.
71
+
72
+ ```jsx
73
+ // In Header.jsx, consider using consistent height:
74
+ <div className="flex items-center justify-between h-16">
75
+ ```
76
+
77
+ ### 2. Improve Vertical Alignment
78
+ **Solution**: Ensure all elements within the Header have consistent vertical alignment by using the same height and alignment properties.
79
+
80
+ ### 3. Standardize Spacing
81
+ **Solution**: Use consistent spacing values or create CSS variables for the different spacing to maintain consistency.
82
+
83
+ ### 4. Align Responsive Breakpoints
84
+ **Solution**: Ensure Tailwind breakpoints and CSS media queries use the same values. Consider using Tailwind's default breakpoints or customizing them to match.
85
+
86
+ ### 5. Resolve Z-Index Conflicts
87
+ **Solution**: Standardize z-index values across components. The Header should have a lower z-index than the Sidebar when the Sidebar is active.
88
+
89
+ ### 6. Harmonize Padding
90
+ **Solution**: Either rely solely on Tailwind classes for padding or ensure CSS values match the Tailwind spacing.
91
+
92
+ ### 7. Consistent Mobile Menu
93
+ **Solution**: Standardize the mobile menu button styling regardless of authentication state to ensure consistent appearance.
94
+
95
+ ## CSS Specificity Issues
96
+
97
+ ### 1. Overriding Styles
98
+ The Header component uses inline Tailwind classes that may override the styles defined in header.css due to CSS specificity rules.
99
+
100
+ ### 2. Missing Responsive Styles
101
+ Some responsive behaviors are implemented with Tailwind classes but not reflected in the CSS files, which could cause maintenance issues.
102
+
103
+ ## Accessibility Considerations
104
+
105
+ ### 1. Focus Management
106
+ The Header includes proper ARIA attributes and focus management, which is good for accessibility.
107
+
108
+ ### 2. Keyboard Navigation
109
+ The component supports keyboard navigation with proper event handlers.
110
+
111
+ ## Performance Considerations
112
+
113
+ ### 1. CSS File Structure
114
+ The separation of CSS into modular files is good for maintainability, but ensure there are no conflicting styles between Tailwind classes and custom CSS.
115
+
116
+ ### 2. Animation Performance
117
+ The Header includes animations (animate-fade-down), which should be optimized for performance, especially on mobile devices.
118
+
119
+ ## Conclusion
120
+
121
+ The Header component has several alignment and spacing issues primarily due to:
122
+
123
+ 1. Inconsistent height definitions between Tailwind classes and CSS
124
+ 2. Mismatched responsive breakpoints between Tailwind and CSS media queries
125
+ 3. Z-index conflicts between Header and Sidebar components
126
+ 4. Inconsistent spacing values across different screen sizes
127
+
128
+ Addressing these issues will improve the visual consistency and user experience of the Header component across different devices and screen sizes.
HEADER_FIX_SUMMARY.md ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Header Component Fix Summary
2
+
3
+ ## Issues Identified and Fixed
4
+
5
+ ### 1. Height Consistency
6
+ **Issue**: Inconsistent height definitions between Tailwind classes (`h-14` on mobile, `h-16` on larger screens) and CSS (`height: 4rem`).
7
+
8
+ **Fix**: Standardized to `h-16` (4rem/64px) across all screen sizes for both Tailwind classes and CSS.
9
+
10
+ ### 2. Padding Consistency
11
+ **Issue**: Header content padding was defined with Tailwind classes (`px-3 sm:px-4 lg:px-8`) but not reflected in CSS.
12
+
13
+ **Fix**: Updated CSS to match the corrected Tailwind classes:
14
+ - Mobile: 1rem (px-4)
15
+ - Small screens (sm): 1.5rem (px-6)
16
+ - Large screens (lg): 2rem (px-8)
17
+
18
+ ### 3. Responsive Breakpoint Alignment
19
+ **Issue**: Mismatch between Tailwind's `lg:` breakpoint (1024px) and CSS media query (767px).
20
+
21
+ **Fix**: Updated CSS media queries to use 1023px max-width to match Tailwind's lg breakpoint.
22
+
23
+ ### 4. Z-Index Standardization
24
+ **Issue**: Header was using `z-50` which could conflict with the Sidebar's z-index.
25
+
26
+ **Fix**:
27
+ - Header: z-index 40 (var(--z-40))
28
+ - Sidebar: z-index 50 (var(--z-50)) on mobile to ensure it appears above the header
29
+
30
+ ### 5. Spacing Standardization
31
+ **Issue**: Inconsistent spacing between elements on different screen sizes.
32
+
33
+ **Fix**: Standardized spacing values:
34
+ - Logo section: `space-x-3` (consistent across screen sizes)
35
+ - User profile section: `space-x-4` (consistent across screen sizes)
36
+
37
+ ### 6. Component Structure Improvements
38
+ **Issue**: Minor inconsistencies in component structure.
39
+
40
+ **Fix**:
41
+ - Standardized button sizes and padding
42
+ - Consistent avatar sizes
43
+ - Unified text sizes and styling
44
+ - Improved mobile menu button sizing
45
+
46
+ ## Files Updated
47
+
48
+ 1. **Header.jsx** - Component with standardized heights, spacing, and responsive behavior
49
+ 2. **header.css** - CSS file with aligned breakpoints, consistent padding, and proper z-index management
50
+
51
+ ## Benefits of Fixes
52
+
53
+ 1. **Visual Consistency**: Header will appear consistent across all screen sizes
54
+ 2. **Improved Responsiveness**: Proper breakpoint alignment ensures correct behavior
55
+ 3. **Better Layering**: Correct z-index relationships between Header and Sidebar
56
+ 4. **Accessibility**: Maintained all existing accessibility features
57
+ 5. **Performance**: Optimized CSS for better rendering performance
58
+ 6. **Maintainability**: Aligned Tailwind and CSS implementations for easier maintenance
59
+
60
+ ## Testing Recommendations
61
+
62
+ 1. **Cross-Browser Testing**: Verify appearance in Chrome, Firefox, Safari, and Edge
63
+ 2. **Responsive Testing**: Check behavior at various screen sizes (mobile, tablet, desktop)
64
+ 3. **Z-Index Testing**: Ensure proper layering of Header and Sidebar components
65
+ 4. **Accessibility Testing**: Verify keyboard navigation and screen reader compatibility
66
+ 5. **Performance Testing**: Confirm smooth animations and transitions
67
+
68
+ These fixes address all the alignment, spacing, and layout issues identified in the Header component while maintaining all existing functionality and accessibility features.
HUGGING_FACE_DEPLOYMENT.md ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hosting Lin on Hugging Face Spaces
2
+
3
+ This guide explains how to deploy your Lin application (Vite + React + Tailwind frontend with Flask backend) on Hugging Face Spaces.
4
+
5
+ ## Project Structure
6
+
7
+ Hugging Face Spaces requires both frontend and backend to run in the same environment. Organize your project as follows:
8
+
9
+ ```
10
+ Lin/
11
+ ├── backend/
12
+ │ ├── app.py
13
+ │ ├── requirements.txt
14
+ │ └── ... (other backend files)
15
+ ├── frontend/
16
+ │ ├── package.json
17
+ │ ├── vite.config.js
18
+ │ └── ... (other frontend files)
19
+ ├── app.py # Main entry point for Flask
20
+ ├── requirements.txt # Python dependencies
21
+ ├── runtime.txt # Python version (optional)
22
+ ├── README.md # Project documentation
23
+ └── Dockerfile # Docker configuration (optional)
24
+ ```
25
+
26
+ ## Prerequisites
27
+
28
+ 1. Build your Vite frontend (Hugging Face Spaces cannot run Vite dev server)
29
+ 2. Configure Flask to serve both API endpoints and static frontend files
30
+ 3. Set up proper environment variables for production
31
+
32
+ ## Step-by-Step Deployment
33
+
34
+ ### 1. Build the Frontend
35
+
36
+ Before deployment, you need to build your React frontend:
37
+
38
+ ```bash
39
+ cd frontend
40
+ npm install
41
+ npm run build
42
+ ```
43
+
44
+ This creates a `dist/` folder with static assets that Flask can serve.
45
+
46
+ ### 2. Configure Flask to Serve Frontend
47
+
48
+ Your Flask app is already configured to serve the frontend. The `backend/app.py` file includes routes to serve static files from the `frontend/dist` directory.
49
+
50
+ ### 3. Update Environment Variables
51
+
52
+ Update your `.env` files for production:
53
+
54
+ **frontend/.env.production:**
55
+ ```bash
56
+ VITE_API_URL=https://zelyanoth-lin.hf.space
57
+ VITE_NODE_ENV=production
58
+ ```
59
+
60
+ **backend/.env:**
61
+ ```bash
62
+ # LinkedIn OAuth configuration
63
+ REDIRECT_URL=https://zelyanoth-lin.hf.space/auth/callback
64
+
65
+ # Other configurations...
66
+ ```
67
+
68
+ Don't forget to update your LinkedIn App settings in the LinkedIn Developer Console to use this redirect URL.
69
+
70
+ ### 4. Root requirements.txt
71
+
72
+ Create a `requirements.txt` file at the project root with all necessary Python dependencies:
73
+
74
+ ```
75
+ flask
76
+ flask-jwt-extended
77
+ supabase
78
+ python-dotenv
79
+ celery
80
+ redis
81
+ # Add other backend dependencies
82
+ ```
83
+
84
+ ### 5. Runtime Configuration (Optional)
85
+
86
+ Create a `runtime.txt` file to specify the Python version:
87
+
88
+ ```
89
+ python-3.10
90
+ ```
91
+
92
+ ### 6. Hugging Face Metadata
93
+
94
+ Add Hugging Face metadata to your `README.md`:
95
+
96
+ ```markdown
97
+ ---
98
+ title: Lin - LinkedIn Community Manager
99
+ sdk: docker
100
+ app_file: app.py
101
+ license: mit
102
+ ---
103
+ ```
104
+
105
+ ### 7. Docker Configuration (Optional)
106
+
107
+ If you want more control over the deployment environment, create a `Dockerfile`:
108
+
109
+ ```dockerfile
110
+ FROM python:3.10
111
+
112
+ WORKDIR /app
113
+
114
+ # Install Node.js for frontend build
115
+ RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
116
+ RUN apt-get update && apt-get install -y nodejs
117
+
118
+ # Copy and install Python dependencies
119
+ COPY requirements.txt .
120
+ RUN pip install -r requirements.txt
121
+
122
+ # Copy package files for frontend
123
+ COPY frontend/package*.json ./frontend/
124
+ # Install frontend dependencies
125
+ RUN cd frontend && npm install
126
+
127
+ # Copy all files
128
+ COPY . .
129
+
130
+ # Build frontend
131
+ RUN cd frontend && npm run build
132
+
133
+ # Expose port
134
+ EXPOSE 7860
135
+
136
+ # Run the application
137
+ CMD ["python", "app.py"]
138
+ ```
139
+
140
+ ### 8. Deploy to Hugging Face Spaces
141
+
142
+ 1. Create a new Space on Hugging Face:
143
+ - Go to https://huggingface.co/new-space
144
+ - Choose "Python (Docker)" as the SDK
145
+ - Give your Space a name
146
+
147
+ 2. Push your code via Git:
148
+ ```bash
149
+ git init
150
+ git remote add origin https://huggingface.co/spaces/<username>/<space-name>
151
+ git add .
152
+ git commit -m "Deploy to Hugging Face Spaces"
153
+ git push origin main
154
+ ```
155
+
156
+ ## How It Works
157
+
158
+ 1. Hugging Face installs Python and Node.js dependencies
159
+ 2. During the build process, it runs `npm run build` in the frontend directory
160
+ 3. It installs Python dependencies from `requirements.txt`
161
+ 4. It runs `python app.py`
162
+ 5. Flask serves both:
163
+ - Your API endpoints (e.g., `/api/*`)
164
+ - Your compiled React frontend (static files from `frontend/dist`)
165
+
166
+ ## Scheduler (Celery) Considerations
167
+
168
+ Your application uses Celery for scheduling tasks. On Hugging Face Spaces:
169
+
170
+ 1. **Celery Worker and Beat** can run in the same container as your Flask app
171
+ 2. **Redis** is used as the broker and result backend
172
+ 3. **Task Persistence** is handled through the database, not just the Celery schedule file
173
+
174
+ The scheduler will work, but you should be aware that:
175
+ - Hugging Face Spaces might put your app to sleep after periods of inactivity
176
+ - When the app wakes up, the Celery processes will restart
177
+ - Your schedules are stored in the database, so they will be reloaded when the app restarts
178
+
179
+ ## Access Your Application
180
+
181
+ After deployment, your app will be available at:
182
+ `https://zelyanoth-lin.hf.space`
183
+
184
+ ## Troubleshooting
185
+
186
+ 1. **Frontend not loading**: Ensure you've run `npm run build` and committed the `dist/` folder
187
+ 2. **API calls failing**: Check that your `VITE_API_URL` points to the correct Space URL
188
+ 3. **OAuth issues**: Verify that your redirect URL in both the `.env` file and LinkedIn Developer Console match your Space URL
189
+ 4. **Build errors**: Check the build logs in your Space's "Files" tab for detailed error messages
190
+ 5. **Scheduler not working**: Check that Redis is available and that Celery processes are running
191
+
192
+ ## Additional Notes
193
+
194
+ - Hugging Face Spaces has resource limitations, so optimize your application accordingly
195
+ - For production use with significant traffic, consider using a more robust hosting solution
196
+ - Regularly update your dependencies to ensure security patches
197
+ - Monitor your Space's logs for any runtime errors
IMPLEMENTATION_SUMMARY.md ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Lin React Clone Implementation Summary
2
+
3
+ ## Overview
4
+
5
+ This document provides a summary of the implementation of the React clone of the Lin application with a Flask API backend. The implementation follows the architecture designed in the planning phase and includes both frontend and backend components.
6
+
7
+ ## Backend Implementation (Flask API)
8
+
9
+ ### Project Structure
10
+ The backend follows a modular structure with clear separation of concerns:
11
+ - `app.py` - Main application entry point with Flask initialization
12
+ - `config.py` - Configuration management
13
+ - `models/` - Data models for all entities
14
+ - `api/` - RESTful API endpoints organized by feature
15
+ - `services/` - Business logic and external API integrations
16
+ - `utils/` - Utility functions and helpers
17
+ - `scheduler/` - Task scheduling implementation
18
+ - `requirements.txt` - Python dependencies
19
+
20
+ ### Key Features Implemented
21
+
22
+ #### Authentication System
23
+ - JWT-based authentication with secure token management
24
+ - User registration with email confirmation
25
+ - User login/logout functionality
26
+ - Password hashing with bcrypt
27
+ - Supabase Auth integration
28
+
29
+ #### Source Management
30
+ - CRUD operations for RSS sources
31
+ - Integration with Supabase database
32
+ - Validation and error handling
33
+
34
+ #### Social Account Management
35
+ - LinkedIn OAuth2 integration
36
+ - Account linking and token storage
37
+ - Profile information retrieval
38
+
39
+ #### Post Management
40
+ - AI-powered content generation using Hugging Face API
41
+ - Post creation and storage
42
+ - LinkedIn publishing integration
43
+ - Image handling for posts
44
+
45
+ #### Scheduling System
46
+ - APScheduler for task management
47
+ - Recurring schedule creation
48
+ - Automatic content generation and publishing
49
+ - Conflict resolution for overlapping schedules
50
+
51
+ ### API Endpoints
52
+ All endpoints follow REST conventions:
53
+ - Authentication: `/api/auth/*`
54
+ - Sources: `/api/sources/*`
55
+ - Accounts: `/api/accounts/*`
56
+ - Posts: `/api/posts/*`
57
+ - Schedules: `/api/schedules/*`
58
+
59
+ ## Frontend Implementation (React)
60
+
61
+ ### Project Structure
62
+ The frontend follows a component-based architecture:
63
+ - `components/` - Reusable UI components
64
+ - `pages/` - Page-level components corresponding to routes
65
+ - `services/` - API service layer
66
+ - `store/` - Redux store with reducers and actions
67
+ - `App.js` - Main application component
68
+ - `index.js` - Entry point
69
+
70
+ ### Key Features Implemented
71
+
72
+ #### Authentication System
73
+ - Login and registration forms
74
+ - JWT token management in localStorage
75
+ - Protected routes
76
+ - User session management
77
+
78
+ #### Dashboard
79
+ - Overview statistics
80
+ - Recent activity display
81
+ - Quick action buttons
82
+
83
+ #### Source Management
84
+ - Add/delete RSS sources
85
+ - List view of all sources
86
+ - Form validation
87
+
88
+ #### Post Management
89
+ - AI content generation interface
90
+ - Post creation form
91
+ - Draft and published post management
92
+ - Publish and delete functionality
93
+
94
+ #### Scheduling
95
+ - Schedule creation form with time selection
96
+ - Day selection interface
97
+ - List view of all schedules
98
+ - Delete functionality
99
+
100
+ ### State Management
101
+ - Redux Toolkit for global state management
102
+ - Async thunks for API calls
103
+ - Loading and error states
104
+ - Slice-based organization
105
+
106
+ ### UI/UX Features
107
+ - Responsive design for all device sizes
108
+ - Consistent color scheme based on brand colors
109
+ - Material-UI components
110
+ - Form validation and error handling
111
+ - Loading states and user feedback
112
+
113
+ ## Integration Points
114
+
115
+ ### Backend-Frontend Communication
116
+ - RESTful API endpoints
117
+ - JSON request/response format
118
+ - JWT token authentication
119
+ - CORS support
120
+
121
+ ### External Services
122
+ - Supabase for database and authentication
123
+ - LinkedIn API for social media integration
124
+ - Hugging Face API for content generation
125
+ - APScheduler for task management
126
+
127
+ ## Technologies Used
128
+
129
+ ### Backend
130
+ - Flask (Python web framework)
131
+ - Supabase (Database and authentication)
132
+ - APScheduler (Task scheduling)
133
+ - requests (HTTP library)
134
+ - requests-oauthlib (OAuth support)
135
+ - gradio-client (Hugging Face API)
136
+ - Flask-JWT-Extended (JWT token management)
137
+
138
+ ### Frontend
139
+ - React (JavaScript library)
140
+ - Redux Toolkit (State management)
141
+ - React Router (Routing)
142
+ - Axios (HTTP client)
143
+ - Material-UI (UI components)
144
+
145
+ ## Deployment Considerations
146
+
147
+ ### Backend
148
+ - Docker support with Dockerfile
149
+ - Environment variable configuration
150
+ - Health check endpoint
151
+ - Error logging and monitoring
152
+
153
+ ### Frontend
154
+ - Static asset optimization
155
+ - Environment variable configuration
156
+ - Responsive design
157
+ - Accessibility features
158
+
159
+ ## Testing
160
+
161
+ ### Backend
162
+ - Unit tests for services
163
+ - Integration tests for API endpoints
164
+ - Database integration tests
165
+
166
+ ### Frontend
167
+ - Component rendering tests
168
+ - Redux action and reducer tests
169
+ - Form submission tests
170
+ - Routing tests
171
+
172
+ ## Security Features
173
+
174
+ ### Backend
175
+ - JWT token authentication
176
+ - Input validation and sanitization
177
+ - Secure password hashing
178
+ - CORS policy configuration
179
+
180
+ ### Frontend
181
+ - Secure token storage
182
+ - Protected routes
183
+ - Form validation
184
+ - Error handling
185
+
186
+ ## Performance Optimizations
187
+
188
+ ### Backend
189
+ - Database connection pooling
190
+ - Caching strategies
191
+ - Efficient query design
192
+
193
+ ### Frontend
194
+ - Component memoization
195
+ - Lazy loading
196
+ - Bundle optimization
197
+ - Image optimization
198
+
199
+ ## Future Enhancements
200
+
201
+ ### Backend
202
+ - Advanced analytics and reporting
203
+ - Additional social media platform support
204
+ - Enhanced scheduling algorithms
205
+ - Performance monitoring
206
+
207
+ ### Frontend
208
+ - Advanced data visualization
209
+ - Real-time updates with WebSockets
210
+ - Enhanced accessibility features
211
+ - Additional UI components
212
+
213
+ ## Conclusion
214
+
215
+ The React clone of the Lin application has been successfully implemented with a clear separation between the frontend and backend. The implementation follows modern best practices for both Flask and React development, with a focus on maintainability, scalability, and security. The application includes all core features of the original Taipy application with an improved architecture that allows for easier maintenance and future enhancements.
LINKEDIN_AUTH_GUIDE.md ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LinkedIn Authentication Implementation Guide
2
+
3
+ This guide provides a comprehensive overview of the LinkedIn authentication implementation in the React Clone application.
4
+
5
+ ## Overview
6
+
7
+ The LinkedIn authentication system allows users to connect multiple LinkedIn accounts to the application for posting content. The implementation follows OAuth 2.0 standards and includes proper error handling, state management, and user experience considerations.
8
+
9
+ ## Architecture
10
+
11
+ ### Frontend Components
12
+
13
+ 1. **LinkedInAuthService** (`frontend/src/services/linkedinAuthService.js`)
14
+ - Handles all API calls related to LinkedIn authentication
15
+ - Manages OAuth flow initiation and callback processing
16
+ - Provides methods for account management
17
+
18
+ 2. **LinkedInAccountsSlice** (`frontend/src/store/reducers/linkedinAccountsSlice.js`)
19
+ - Redux slice for managing LinkedIn accounts state
20
+ - Handles async operations for fetching, adding, and managing accounts
21
+ - Manages loading states and error handling
22
+
23
+ 3. **LinkedInAccountsManager** (`frontend/src/components/LinkedInAccount/LinkedInAccountsManager.js`)
24
+ - Main component for managing LinkedIn accounts
25
+ - Displays connected accounts and provides management options
26
+ - Handles the "Add Account" flow
27
+
28
+ 4. **LinkedInAccountCard** (`frontend/src/components/LinkedInAccount/LinkedInAccountCard.js`)
29
+ - Individual account display component
30
+ - Shows account information and actions (set primary, delete)
31
+ - Handles account-specific operations
32
+
33
+ 5. **LinkedInCallbackHandler** (`frontend/src/components/LinkedInAccount/LinkedInCallbackHandler.js`)
34
+ - Handles the OAuth callback from LinkedIn
35
+ - Processes authorization code and exchanges it for access token
36
+ - Manages authentication states and error handling
37
+
38
+ 6. **Accounts Page** (`frontend/src/pages/Accounts.js`)
39
+ - Dedicated page for managing all social media accounts
40
+ - Provides a clean interface for LinkedIn account management
41
+ - Separates account management from RSS source management
42
+
43
+ ### Backend API
44
+
45
+ 1. **Accounts API** (`backend/api/accounts.py`)
46
+ - `/accounts` - GET: Fetch all accounts, POST: Initiate OAuth flow
47
+ - `/accounts/callback` - POST: Handle OAuth callback
48
+ - `/accounts/{id}` - DELETE: Remove account
49
+ - `/accounts/{id}/primary` - PUT: Set account as primary
50
+
51
+ 2. **LinkedInService** (`backend/services/linkedin_service.py`)
52
+ - Handles LinkedIn API interactions
53
+ - Manages OAuth token exchange
54
+ - Provides methods for user info retrieval and posting
55
+
56
+ ## Implementation Details
57
+
58
+ ### OAuth Flow
59
+
60
+ 1. **Initiation**
61
+ - User clicks "Add LinkedIn Account" button on the Accounts page
62
+ - Frontend calls `/accounts` endpoint with `social_network: 'LinkedIn'`
63
+ - Backend generates authorization URL and state parameter
64
+ - User is redirected to LinkedIn for authentication
65
+
66
+ 2. **Callback Handling**
67
+ - LinkedIn redirects back to `/linkedin/callback` with authorization code
68
+ - Frontend processes the callback and exchanges code for access token
69
+ - Backend validates the code and retrieves user information
70
+ - Account information is stored in the database
71
+
72
+ 3. **Account Management**
73
+ - Users can view all connected LinkedIn accounts on the Accounts page
74
+ - Primary account can be set for posting operations
75
+ - Accounts can be disconnected (deleted)
76
+
77
+ ### State Management
78
+
79
+ The Redux store manages the following states for LinkedIn accounts:
80
+
81
+ ```javascript
82
+ {
83
+ linkedinAccounts: {
84
+ items: [], // Array of LinkedIn accounts
85
+ loading: false, // Loading state
86
+ error: null, // Error message
87
+ oauthLoading: false, // OAuth process loading
88
+ oauthError: null, // OAuth error message
89
+ deletingAccount: null, // ID of account being deleted
90
+ settingPrimary: null // ID of account being set as primary
91
+ }
92
+ }
93
+ ```
94
+
95
+ ### Database Schema
96
+
97
+ LinkedIn accounts are stored in the `Social_network` table with the following fields:
98
+
99
+ - `id`: Unique identifier
100
+ - `social_network`: 'LinkedIn'
101
+ - `account_name`: Display name for the account
102
+ - `id_utilisateur`: User ID (foreign key)
103
+ - `token`: LinkedIn access token
104
+ - `sub`: LinkedIn user ID
105
+ - `given_name`: User's first name
106
+ - `family_name`: User's last name
107
+ - `picture`: Profile picture URL
108
+ - `is_primary`: Boolean flag for primary account
109
+
110
+ ## Usage Instructions
111
+
112
+ ### Adding a LinkedIn Account
113
+
114
+ 1. Navigate to the **Accounts** page (`/accounts`)
115
+ 2. Click **"Add LinkedIn Account"**
116
+ 3. Follow the LinkedIn authentication flow
117
+ 4. After successful authentication, the account will appear in the list
118
+
119
+ ### Managing LinkedIn Accounts
120
+
121
+ 1. **View Accounts**: All connected accounts are displayed on the Accounts page
122
+ 2. **Set Primary**: Click "Set Primary" on the desired account
123
+ 3. **Disconnect**: Click "Disconnect" to remove an account (confirmation required)
124
+
125
+ ### Page Structure
126
+
127
+ - **Sources Page** (`/sources`): Dedicated to RSS source management only
128
+ - **Accounts Page** (`/accounts`): Dedicated to social media account management
129
+
130
+ ### Error Handling
131
+
132
+ The implementation includes comprehensive error handling:
133
+
134
+ - **OAuth Errors**: Invalid state, expired authorization codes
135
+ - **Network Errors**: API timeouts, connection issues
136
+ - **Authentication Errors**: Invalid tokens, permission denied
137
+ - **Database Errors**: Failed storage operations
138
+
139
+ ### Security Considerations
140
+
141
+ 1. **State Parameter**: Randomly generated state parameter for CSRF protection
142
+ 2. **Token Storage**: Access tokens are stored securely in the database
143
+ 3. **User Validation**: All operations are validated against the authenticated user
144
+ 4. **HTTPS**: All API calls use HTTPS for secure communication
145
+
146
+ ## Configuration
147
+
148
+ ### Environment Variables
149
+
150
+ Ensure the following environment variables are set:
151
+
152
+ ```bash
153
+ # LinkedIn OAuth Configuration
154
+ CLIENT_ID=your_linkedin_client_id
155
+ CLIENT_SECRET=your_linkedin_client_secret
156
+ REDIRECT_URL=your_redirect_url
157
+
158
+ # Supabase Configuration
159
+ SUPABASE_URL=your_supabase_url
160
+ SUPABASE_KEY=your_supabase_key
161
+ ```
162
+
163
+ ### Backend Configuration
164
+
165
+ The backend uses the following LinkedIn API endpoints:
166
+
167
+ - Authorization: `https://www.linkedin.com/oauth/v2/authorization`
168
+ - Token Exchange: `https://www.linkedin.com/oauth/v2/accessToken`
169
+ - User Info: `https://api.linkedin.com/v2/userinfo`
170
+
171
+ ## Testing
172
+
173
+ ### Manual Testing Steps
174
+
175
+ 1. **Account Addition**
176
+ - Navigate to Accounts page
177
+ - Click "Add LinkedIn Account"
178
+ - Complete OAuth flow
179
+ - Verify account appears in the list
180
+
181
+ 2. **Account Management**
182
+ - Test setting primary account
183
+ - Test disconnecting accounts
184
+ - Verify error states and loading indicators
185
+
186
+ 3. **Page Navigation**
187
+ - Verify Sources page only shows RSS sources
188
+ - Verify Accounts page only shows social media accounts
189
+ - Test navigation between pages
190
+
191
+ ### Automated Testing
192
+
193
+ The implementation includes Redux action and reducer tests that can be extended with:
194
+
195
+ - OAuth flow simulation
196
+ - API mocking
197
+ - State validation
198
+ - Error condition testing
199
+
200
+ ## Troubleshooting
201
+
202
+ ### Common Issues
203
+
204
+ 1. **OAuth State Mismatch**
205
+ - Ensure state parameter is properly generated and stored
206
+ - Check for proper state validation in callback handling
207
+
208
+ 2. **Token Exchange Failures**
209
+ - Verify client ID and client secret are correct
210
+ - Ensure redirect URL matches configuration
211
+ - Check for expired authorization codes
212
+
213
+ 3. **Account Not Displaying**
214
+ - Verify database insertion was successful
215
+ - Check user ID mapping
216
+ - Ensure API response is properly formatted
217
+
218
+ 4. **Page Navigation Issues**
219
+ - Verify routes are properly configured in App.js
220
+ - Check that components are imported correctly
221
+ - Ensure Redux state is properly connected
222
+
223
+ ### Debug Mode
224
+
225
+ Enable debug logging by setting:
226
+
227
+ ```javascript
228
+ // In development mode
229
+ localStorage.setItem('debug', 'linkedin:*');
230
+
231
+ // Backend logging
232
+ export DEBUG=True
233
+ ```
234
+
235
+ ## Future Enhancements
236
+
237
+ 1. **Token Refresh**: Implement automatic token refresh
238
+ 2. **Account Permissions**: Request specific LinkedIn permissions
239
+ 3. **Batch Operations**: Support for managing multiple accounts simultaneously
240
+ 4. **Analytics**: Track account usage and posting statistics
241
+ 5. **Webhooks**: Implement LinkedIn webhook support for real-time updates
242
+ 6. **Additional Social Networks**: Extend to Twitter, Facebook, etc.
243
+
244
+ ## Support
245
+
246
+ For issues or questions regarding the LinkedIn authentication implementation:
247
+
248
+ 1. Check the troubleshooting section above
249
+ 2. Review browser console and network tab for errors
250
+ 3. Check backend logs for API-related issues
251
+ 4. Verify environment configuration
252
+ 5. Ensure proper page navigation and routing
253
+
254
+ ## References
255
+
256
+ - [LinkedIn OAuth 2.0 Documentation](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin)
257
+ - [React Redux Toolkit Documentation](https://redux-toolkit.js.org/)
258
+ - [OAuth 2.0 Framework](https://datatracker.ietf.org/doc/html/rfc6749)
REACT_DEVELOPMENT_GUIDE.md ADDED
@@ -0,0 +1,568 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React Development Guide for Lin Project
2
+
3
+ ## Overview
4
+
5
+ This guide documents the React development patterns, best practices, and conventions used in the Lin project. It serves as a reference for current and future developers working on the frontend.
6
+
7
+ ## Project Structure
8
+
9
+ The frontend follows a component-based architecture with clear separation of concerns:
10
+
11
+ ```
12
+ frontend/
13
+ ├── src/
14
+ │ ├── components/ # Reusable UI components
15
+ │ │ ├── Header/ # Header component
16
+ │ │ ├── Sidebar/ # Sidebar navigation
17
+ │ │ └── ... # Other reusable components
18
+ │ ├── pages/ # Page-level components
19
+ │ ├── services/ # API service layer
20
+ │ ├── store/ # Redux store configuration
21
+ │ ├── App.jsx # Root component
22
+ │ └── index.jsx # Entry point
23
+ ├── public/ # Static assets
24
+ └── package.json # Dependencies and scripts
25
+ ```
26
+
27
+ ## Core Technologies
28
+
29
+ - React 18+ with Hooks
30
+ - React Router v6 for routing
31
+ - Redux Toolkit for state management
32
+ - Axios for HTTP requests
33
+ - Tailwind CSS for styling
34
+ - Material Icons for icons
35
+
36
+ ## Component Development Patterns
37
+
38
+ ### Functional Components with Hooks
39
+
40
+ All components are implemented as functional components using React hooks:
41
+
42
+ ```jsx
43
+ import React, { useState, useEffect } from 'react';
44
+
45
+ const MyComponent = ({ prop1, prop2 }) => {
46
+ const [state, setState] = useState(initialValue);
47
+
48
+ useEffect(() => {
49
+ // Side effects
50
+ }, [dependencies]);
51
+
52
+ return (
53
+ <div className="component-class">
54
+ {/* JSX */}
55
+ </div>
56
+ );
57
+ };
58
+
59
+ export default MyComponent;
60
+ ```
61
+
62
+ ### Component Structure
63
+
64
+ 1. **Imports** - All necessary imports at the top
65
+ 2. **Component Definition** - Functional component with destructured props
66
+ 3. **State Management** - useState, useEffect, and custom hooks
67
+ 4. **Helper Functions** - Small utility functions within the component
68
+ 5. **JSX Return** - Clean, semantic HTML with Tailwind classes
69
+ 6. **Export** - Default export of the component
70
+
71
+ ### State Management
72
+
73
+ #### Local Component State
74
+
75
+ Use `useState` for local component state:
76
+
77
+ ```jsx
78
+ const [isOpen, setIsOpen] = useState(false);
79
+ const [data, setData] = useState([]);
80
+ const [loading, setLoading] = useState(false);
81
+ ```
82
+
83
+ #### Global State (Redux)
84
+
85
+ Use Redux Toolkit for global state management. Structure slices by feature:
86
+
87
+ ```jsx
88
+ // store/reducers/featureSlice.js
89
+ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
90
+
91
+ export const fetchFeatureData = createAsyncThunk(
92
+ 'feature/fetchData',
93
+ async (params) => {
94
+ const response = await api.getFeatureData(params);
95
+ return response.data;
96
+ }
97
+ );
98
+
99
+ const featureSlice = createSlice({
100
+ name: 'feature',
101
+ initialState: {
102
+ items: [],
103
+ loading: false,
104
+ error: null
105
+ },
106
+ reducers: {
107
+ clearError: (state) => {
108
+ state.error = null;
109
+ }
110
+ },
111
+ extraReducers: (builder) => {
112
+ builder
113
+ .addCase(fetchFeatureData.pending, (state) => {
114
+ state.loading = true;
115
+ })
116
+ .addCase(fetchFeatureData.fulfilled, (state, action) => {
117
+ state.loading = false;
118
+ state.items = action.payload;
119
+ })
120
+ .addCase(fetchFeatureData.rejected, (state, action) => {
121
+ state.loading = false;
122
+ state.error = action.error.message;
123
+ });
124
+ }
125
+ });
126
+
127
+ export const { clearError } = featureSlice.actions;
128
+ export default featureSlice.reducer;
129
+ ```
130
+
131
+ ### Props and Prop Types
132
+
133
+ Destructure props in the component signature and provide default values when appropriate:
134
+
135
+ ```jsx
136
+ const MyComponent = ({
137
+ title,
138
+ items = [],
139
+ isLoading = false,
140
+ onAction = () => {}
141
+ }) => {
142
+ // Component implementation
143
+ };
144
+ ```
145
+
146
+ ### Event Handling
147
+
148
+ Use inline arrow functions or separate handler functions:
149
+
150
+ ```jsx
151
+ // Inline
152
+ <button onClick={() => handleClick(item.id)}>Click me</button>
153
+
154
+ // Separate function
155
+ const handleDelete = (id) => {
156
+ dispatch(deleteItem(id));
157
+ };
158
+
159
+ <button onClick={() => handleDelete(item.id)}>Delete</button>
160
+ ```
161
+
162
+ ## Styling with Tailwind CSS
163
+
164
+ The project uses Tailwind CSS for styling. Follow these conventions:
165
+
166
+ ### Class Organization
167
+
168
+ 1. **Layout classes** (flex, grid, etc.) first
169
+ 2. **Positioning** (relative, absolute, etc.)
170
+ 3. **Sizing** (w-, h-, etc.)
171
+ 4. **Spacing** (m-, p-, etc.)
172
+ 5. **Typography** (text-, font-, etc.)
173
+ 6. **Visual** (bg-, border-, shadow-, etc.)
174
+ 7. **Interactive states** (hover:, focus:, etc.)
175
+
176
+ ```jsx
177
+ <div className="flex items-center justify-between w-full p-4 bg-white rounded-lg shadow hover:shadow-md focus:outline-none focus:ring-2 focus:ring-primary-500">
178
+ {/* Content */}
179
+ </div>
180
+ ```
181
+
182
+ ### Responsive Design
183
+
184
+ Use Tailwind's responsive prefixes (sm:, md:, lg:, xl:) for responsive styles:
185
+
186
+ ```jsx
187
+ <div className="flex flex-col lg:flex-row items-center p-4 sm:p-6">
188
+ <div className="w-full lg:w-1/2 mb-4 lg:mb-0 lg:mr-6">
189
+ {/* Content */}
190
+ </div>
191
+ <div className="w-full lg:w-1/2">
192
+ {/* Content */}
193
+ </div>
194
+ </div>
195
+ ```
196
+
197
+ ### Custom Classes
198
+
199
+ For complex components, use component-specific classes in conjunction with Tailwind:
200
+
201
+ ```jsx
202
+ <NavLink
203
+ to={item.path}
204
+ className={({ isActive }) => `
205
+ nav-link group relative flex items-center px-3 py-2.5 text-sm font-medium rounded-lg
206
+ transition-all duration-200 ease-in-out
207
+ ${isActive
208
+ ? 'bg-gradient-to-r from-primary-600 to-primary-700 text-white'
209
+ : 'text-secondary-700 hover:bg-accent-100'
210
+ }
211
+ `}
212
+ >
213
+ ```
214
+
215
+ ## Routing
216
+
217
+ Use React Router v6 for navigation:
218
+
219
+ ```jsx
220
+ import { Routes, Route, useNavigate, useLocation } from 'react-router-dom';
221
+
222
+ // In App.jsx
223
+ <Routes>
224
+ <Route path="/dashboard" element={<Dashboard />} />
225
+ <Route path="/sources" element={<Sources />} />
226
+ <Route path="/posts" element={<Posts />} />
227
+ <Route path="/schedule" element={<Schedule />} />
228
+ </Routes>
229
+
230
+ // In components
231
+ const navigate = useNavigate();
232
+ const location = useLocation();
233
+
234
+ // Navigation
235
+ navigate('/dashboard');
236
+
237
+ // Check current route
238
+ if (location.pathname === '/dashboard') {
239
+ // Do something
240
+ }
241
+ ```
242
+
243
+ ## API Integration
244
+
245
+ ### Service Layer
246
+
247
+ Create service functions for API calls:
248
+
249
+ ```jsx
250
+ // services/api.js
251
+ import axios from 'axios';
252
+
253
+ const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000';
254
+
255
+ const api = axios.create({
256
+ baseURL: API_BASE_URL,
257
+ timeout: 10000,
258
+ });
259
+
260
+ // Request interceptor
261
+ api.interceptors.request.use((config) => {
262
+ const token = localStorage.getItem('token');
263
+ if (token) {
264
+ config.headers.Authorization = `Bearer ${token}`;
265
+ }
266
+ return config;
267
+ });
268
+
269
+ // Response interceptor
270
+ api.interceptors.response.use(
271
+ (response) => response,
272
+ (error) => {
273
+ if (error.response?.status === 401) {
274
+ // Handle unauthorized access
275
+ localStorage.removeItem('token');
276
+ window.location.href = '/login';
277
+ }
278
+ return Promise.reject(error);
279
+ }
280
+ );
281
+
282
+ export default api;
283
+
284
+ // services/featureService.js
285
+ import api from './api';
286
+
287
+ export const getFeatures = async () => {
288
+ const response = await api.get('/api/features');
289
+ return response.data;
290
+ };
291
+
292
+ export const createFeature = async (data) => {
293
+ const response = await api.post('/api/features', data);
294
+ return response.data;
295
+ };
296
+ ```
297
+
298
+ ### Async Operations with Redux Thunks
299
+
300
+ Use createAsyncThunk for asynchronous operations:
301
+
302
+ ```jsx
303
+ // In slice
304
+ export const fetchData = createAsyncThunk(
305
+ 'feature/fetchData',
306
+ async (_, { rejectWithValue }) => {
307
+ try {
308
+ const data = await featureService.getFeatures();
309
+ return data;
310
+ } catch (error) {
311
+ return rejectWithValue(error.response.data);
312
+ }
313
+ }
314
+ );
315
+
316
+ // In component
317
+ const dispatch = useDispatch();
318
+ const { items, loading, error } = useSelector(state => state.feature);
319
+
320
+ useEffect(() => {
321
+ dispatch(fetchData());
322
+ }, [dispatch]);
323
+ ```
324
+
325
+ ## Accessibility (a11y)
326
+
327
+ Implement proper accessibility features:
328
+
329
+ 1. **Semantic HTML** - Use appropriate HTML elements
330
+ 2. **ARIA attributes** - When needed for dynamic content
331
+ 3. **Keyboard navigation** - Ensure all interactive elements are keyboard accessible
332
+ 4. **Focus management** - Proper focus handling for modals, dropdowns, etc.
333
+ 5. **Screen reader support** - Use aria-label, aria-describedby, etc.
334
+
335
+ ```jsx
336
+ <button
337
+ aria-label="Close dialog"
338
+ aria-expanded={isOpen}
339
+ onClick={handleClose}
340
+ >
341
+
342
+ </button>
343
+
344
+ <nav aria-label="Main navigation">
345
+ <ul role="menubar">
346
+ <li role="none">
347
+ <a
348
+ href="/dashboard"
349
+ role="menuitem"
350
+ aria-current={currentPage === 'dashboard' ? 'page' : undefined}
351
+ >
352
+ Dashboard
353
+ </a>
354
+ </li>
355
+ </ul>
356
+ </nav>
357
+ ```
358
+
359
+ ## Performance Optimization
360
+
361
+ ### Memoization
362
+
363
+ Use React.memo for components that render frequently:
364
+
365
+ ```jsx
366
+ const MyComponent = React.memo(({ data, onUpdate }) => {
367
+ // Component implementation
368
+ });
369
+
370
+ export default MyComponent;
371
+ ```
372
+
373
+ ### useCallback and useMemo
374
+
375
+ Optimize expensive calculations and callback functions:
376
+
377
+ ```jsx
378
+ const expensiveValue = useMemo(() => {
379
+ return computeExpensiveValue(data);
380
+ }, [data]);
381
+
382
+ const handleClick = useCallback((id) => {
383
+ dispatch(action(id));
384
+ }, [dispatch]);
385
+ ```
386
+
387
+ ### Lazy Loading
388
+
389
+ Use React.lazy for code splitting:
390
+
391
+ ```jsx
392
+ import { lazy, Suspense } from 'react';
393
+
394
+ const LazyComponent = lazy(() => import('./components/MyComponent'));
395
+
396
+ <Suspense fallback={<div>Loading...</div>}>
397
+ <LazyComponent />
398
+ </Suspense>
399
+ ```
400
+
401
+ ## Error Handling
402
+
403
+ ### Error Boundaries
404
+
405
+ Implement error boundaries for catching JavaScript errors:
406
+
407
+ ```jsx
408
+ class ErrorBoundary extends React.Component {
409
+ constructor(props) {
410
+ super(props);
411
+ this.state = { hasError: false };
412
+ }
413
+
414
+ static getDerivedStateFromError(error) {
415
+ return { hasError: true };
416
+ }
417
+
418
+ componentDidCatch(error, errorInfo) {
419
+ console.error('Error caught by boundary:', error, errorInfo);
420
+ }
421
+
422
+ render() {
423
+ if (this.state.hasError) {
424
+ return <h1>Something went wrong.</h1>;
425
+ }
426
+
427
+ return this.props.children;
428
+ }
429
+ }
430
+
431
+ // Usage
432
+ <ErrorBoundary>
433
+ <MyComponent />
434
+ </ErrorBoundary>
435
+ ```
436
+
437
+ ### API Error Handling
438
+
439
+ Handle API errors gracefully:
440
+
441
+ ```jsx
442
+ const [error, setError] = useState(null);
443
+
444
+ const handleSubmit = async (data) => {
445
+ try {
446
+ setError(null);
447
+ const result = await api.createItem(data);
448
+ // Handle success
449
+ } catch (err) {
450
+ setError(err.response?.data?.message || 'An error occurred');
451
+ }
452
+ };
453
+
454
+ {error && (
455
+ <div className="text-red-500 text-sm mt-2">
456
+ {error}
457
+ </div>
458
+ )}
459
+ ```
460
+
461
+ ## Testing
462
+
463
+ ### Unit Testing
464
+
465
+ Use Jest and React Testing Library for unit tests:
466
+
467
+ ```jsx
468
+ import { render, screen, fireEvent } from '@testing-library/react';
469
+ import MyComponent from './MyComponent';
470
+
471
+ test('renders component with title', () => {
472
+ render(<MyComponent title="Test Title" />);
473
+ expect(screen.getByText('Test Title')).toBeInTheDocument();
474
+ });
475
+
476
+ test('calls onClick when button is clicked', () => {
477
+ const handleClick = jest.fn();
478
+ render(<MyComponent onClick={handleClick} />);
479
+ fireEvent.click(screen.getByRole('button'));
480
+ expect(handleClick).toHaveBeenCalledTimes(1);
481
+ });
482
+ ```
483
+
484
+ ### Redux Testing
485
+
486
+ Test Redux slices and async thunks:
487
+
488
+ ```jsx
489
+ import featureReducer, { fetchData } from './featureSlice';
490
+
491
+ test('handles fulfilled fetch data', () => {
492
+ const initialState = { items: [], loading: false, error: null };
493
+ const data = [{ id: 1, name: 'Test' }];
494
+ const action = { type: fetchData.fulfilled, payload: data };
495
+ const state = featureReducer(initialState, action);
496
+ expect(state.items).toEqual(data);
497
+ expect(state.loading).toBe(false);
498
+ });
499
+ ```
500
+
501
+ ## Mobile Responsiveness
502
+
503
+ ### Touch Optimization
504
+
505
+ Add touch-specific classes and handlers:
506
+
507
+ ```jsx
508
+ <button
509
+ className="touch-manipulation active:scale-95"
510
+ onTouchStart={handleTouchStart}
511
+ onTouchEnd={handleTouchEnd}
512
+ >
513
+ Click me
514
+ </button>
515
+ ```
516
+
517
+ ### Responsive Breakpoints
518
+
519
+ Use Tailwind's responsive utilities for different screen sizes:
520
+
521
+ - Mobile: Default styles (0-767px)
522
+ - Tablet: `sm:` (768px+) and `md:` (1024px+)
523
+ - Desktop: `lg:` (1280px+) and `xl:` (1536px+)
524
+
525
+ ### Mobile-First Approach
526
+
527
+ Start with mobile styles and enhance for larger screens:
528
+
529
+ ```jsx
530
+ <div className="flex flex-col lg:flex-row">
531
+ <div className="w-full lg:w-1/2">
532
+ {/* Content that stacks on mobile, side-by-side on desktop */}
533
+ </div>
534
+ </div>
535
+ ```
536
+
537
+ ## Best Practices
538
+
539
+ ### Component Design
540
+
541
+ 1. **Single Responsibility** - Each component should have one clear purpose
542
+ 2. **Reusability** - Design components to be reusable across the application
543
+ 3. **Composition** - Build complex UIs by composing simpler components
544
+ 4. **Controlled Components** - Prefer controlled components for form elements
545
+ 5. **Props Drilling** - Use context or Redux to avoid excessive prop drilling
546
+
547
+ ### Code Organization
548
+
549
+ 1. **Consistent Naming** - Use consistent naming conventions (PascalCase for components)
550
+ 2. **Logical Grouping** - Group related files in directories
551
+ 3. **Export Strategy** - Use default exports for components, named exports for utilities
552
+ 4. **Import Organization** - Group imports logically (external, internal, styles)
553
+
554
+ ### Performance
555
+
556
+ 1. **Bundle Size** - Monitor bundle size and optimize when necessary
557
+ 2. **Rendering** - Use React.memo, useMemo, and useCallback appropriately
558
+ 3. **API Calls** - Implement caching and pagination where appropriate
559
+ 4. **Images** - Optimize images and use lazy loading
560
+
561
+ ### Security
562
+
563
+ 1. **XSS Prevention** - React automatically escapes content, but be careful with dangerouslySetInnerHTML
564
+ 2. **Token Storage** - Store JWT tokens securely (HttpOnly cookies or secure localStorage)
565
+ 3. **Input Validation** - Validate and sanitize user inputs
566
+ 4. **CORS** - Ensure proper CORS configuration on the backend
567
+
568
+ This guide should help maintain consistency and quality across the React frontend implementation in the Lin project.
README.md CHANGED
@@ -1,10 +1,311 @@
1
  ---
2
- title: Lin
3
- emoji: 📊
4
- colorFrom: purple
5
- colorTo: indigo
6
  sdk: docker
7
- pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Lin - LinkedIn Community Manager
 
 
 
3
  sdk: docker
4
+ app_file: app.py
5
+ license: mit
6
  ---
7
 
8
+ # Lin - Community Manager Assistant for LinkedIn
9
+
10
+ A comprehensive community management tool that helps you automate and streamline your LinkedIn activities.
11
+
12
+ ## 🚀 Quick Start
13
+
14
+ ### Prerequisites
15
+
16
+ - Node.js (v16 or higher)
17
+ - Python (v3.8 or higher)
18
+ - npm (v8 or higher)
19
+
20
+ ### Installation
21
+
22
+ **Option 1: Using the root package.json (Recommended)**
23
+
24
+ ```bash
25
+ # Clone the repository
26
+ git clone <repository-url>
27
+ cd Lin
28
+
29
+ # Install all dependencies
30
+ npm install
31
+
32
+ # Setup the project
33
+ npm run setup
34
+
35
+ # Start both frontend and backend
36
+ npm start
37
+ ```
38
+
39
+ **Option 2: Manual installation**
40
+
41
+ ```bash
42
+ # Install frontend dependencies
43
+ cd frontend
44
+ npm install
45
+
46
+ # Install backend dependencies
47
+ cd ../backend
48
+ pip install -r requirements.txt
49
+
50
+ # Return to root directory
51
+ cd ..
52
+ ```
53
+
54
+ ## 📁 Project Structure
55
+
56
+ ```
57
+ Lin/
58
+ ├── package.json # Root package.json with combined scripts
59
+ ├── frontend/ # React frontend application
60
+ │ ├── package.json # Frontend-specific dependencies
61
+ │ ├── src/ # React source code
62
+ │ ├── public/ # Static assets
63
+ │ └── build/ # Build output
64
+ ├── backend/ # Flask backend API
65
+ │ ├── app.py # Main application file
66
+ │ ├── requirements.txt # Python dependencies
67
+ │ ├── api/ # API endpoints
68
+ │ ├── models/ # Data models
69
+ │ ├── services/ # Business logic
70
+ │ └── utils/ # Utility functions
71
+ └── README.md # This file
72
+ ```
73
+
74
+ ## 🛠️ Development
75
+
76
+ ### Available Scripts
77
+
78
+ From the project root directory, you can use the following commands:
79
+
80
+ #### Installation
81
+ - `npm install` - Install root dependencies
82
+ - `npm run install:frontend` - Install frontend dependencies
83
+ - `npm run install:backend` - Install backend dependencies
84
+ - `npm run install:all` - Install all dependencies
85
+ - `npm run install:all:win` - Install all dependencies (Windows-specific)
86
+
87
+ #### Development Servers
88
+ - `npm run dev:frontend` - Start frontend development server
89
+ - `npm run dev:backend` - Start backend development server
90
+ - `npm run dev:all` - Start both servers concurrently
91
+ - `npm run start` - Alias for `npm run dev:all`
92
+ - `npm run start:frontend` - Start frontend only
93
+ - `npm run start:backend` - Start backend only
94
+
95
+ #### Build & Test
96
+ - `npm run build` - Build frontend for production
97
+ - `npm run build:prod` - Build frontend for production
98
+ - `npm run preview` - Preview production build
99
+ - `npm run test` - Run frontend tests
100
+ - `npm run test:backend` - Run backend tests
101
+ - `npm run lint` - Run ESLint
102
+ - `npm run lint:fix` - Fix ESLint issues
103
+
104
+ #### Setup & Maintenance
105
+ - `npm run setup` - Full setup (install + build)
106
+ - `npm run setup:win` - Full setup (Windows-specific)
107
+ - `npm run clean` - Clean build artifacts
108
+ - `npm run reset` - Reset project (clean + install)
109
+
110
+ ### Directory Navigation
111
+
112
+ **Important:** Most npm commands should be run from the project root directory where the main `package.json` is located.
113
+
114
+ #### Command Prompt (Windows)
115
+ ```cmd
116
+ # Navigate to project root (if not already there)
117
+ cd C:\Users\YourUser\Documents\Project\Lin_re\Lin
118
+
119
+ # Install dependencies
120
+ npm install
121
+
122
+ # Start development servers
123
+ npm start
124
+
125
+ # Or start individually
126
+ npm run dev:frontend
127
+ npm run dev:backend
128
+ ```
129
+
130
+ #### PowerShell (Windows)
131
+ ```powershell
132
+ # Navigate to project root (if not already there)
133
+ cd C:\Users\YourUser\Documents\Project\Lin_re\Lin
134
+
135
+ # Install dependencies
136
+ npm install
137
+
138
+ # Start development servers
139
+ npm start
140
+
141
+ # Or start individually
142
+ npm run dev:frontend
143
+ npm run dev:backend
144
+ ```
145
+
146
+ #### Linux/macOS
147
+ ```bash
148
+ # Navigate to project root (if not already there)
149
+ cd /path/to/your/project/Lin
150
+
151
+ # Install dependencies
152
+ npm install
153
+
154
+ # Start development servers
155
+ npm start
156
+
157
+ # Or start individually
158
+ npm run dev:frontend
159
+ npm run dev:backend
160
+ ```
161
+
162
+ ## 🔧 Environment Setup
163
+
164
+ ### Frontend Environment
165
+
166
+ ```bash
167
+ # Copy environment file
168
+ cd frontend
169
+ cp .env.example .env.local
170
+
171
+ # Edit environment variables
172
+ # Open .env.local and add your required values
173
+ ```
174
+
175
+ ### Backend Environment
176
+
177
+ ```bash
178
+ # Copy environment file
179
+ cd backend
180
+ cp .env.example .env
181
+
182
+ # Edit environment variables
183
+ # Open .env and add your required values
184
+ ```
185
+
186
+ ### Required Environment Variables
187
+
188
+ **Frontend (.env.local)**
189
+ - `REACT_APP_API_URL` - Backend API URL (default: http://localhost:5000)
190
+
191
+ **Backend (.env)**
192
+ - `SUPABASE_URL` - Your Supabase project URL
193
+ - `SUPABASE_KEY` - Your Supabase API key
194
+ - `CLIENT_ID` - LinkedIn OAuth client ID
195
+ - `CLIENT_SECRET` - LinkedIn OAuth client secret
196
+ - `REDIRECT_URL` - LinkedIn OAuth redirect URL
197
+ - `HUGGING_KEY` - Hugging Face API key
198
+ - `JWT_SECRET_KEY` - Secret key for JWT token generation
199
+ - `SECRET_KEY` - Flask secret key
200
+ - `DEBUG` - Debug mode (True/False)
201
+ - `SCHEDULER_ENABLED` - Enable/disable task scheduler (True/False)
202
+ - `PORT` - Port to run the application on (default: 5000)
203
+
204
+ ## 🌐 Development URLs
205
+
206
+ - **Frontend**: http://localhost:3000
207
+ - **Backend API**: http://localhost:5000
208
+
209
+ ## 🔍 Troubleshooting
210
+
211
+ ### Common Issues
212
+
213
+ #### 1. ENOENT Error: no such file or directory
214
+ **Problem**: Running npm commands from the wrong directory
215
+ **Solution**: Always run npm commands from the project root directory where `package.json` is located
216
+
217
+ ```bash
218
+ # Check if you're in the right directory
219
+ ls package.json
220
+
221
+ # If not, navigate to the root directory
222
+ cd /path/to/project/Lin
223
+ ```
224
+
225
+ #### 2. Port Already in Use
226
+ **Problem**: Port 3000 or 5000 is already in use
227
+ **Solution**: Change ports or stop conflicting services
228
+
229
+ **Command Prompt:**
230
+ ```cmd
231
+ # Check what's using port 3000
232
+ netstat -ano | findstr :3000
233
+
234
+ # Check what's using port 5000
235
+ netstat -ano | findstr :5000
236
+ ```
237
+
238
+ **PowerShell:**
239
+ ```powershell
240
+ # Check what's using port 3000
241
+ netstat -ano | Select-String ":3000"
242
+
243
+ # Check what's using port 5000
244
+ netstat -ano | Select-String ":5000"
245
+ ```
246
+
247
+ #### 3. Python/Node.js Not Recognized
248
+ **Problem**: Python or Node.js commands not found
249
+ **Solution**: Ensure Python and Node.js are added to your system PATH
250
+
251
+ **Windows:**
252
+ 1. Open System Properties > Environment Variables
253
+ 2. Add Python and Node.js installation directories to PATH
254
+ 3. Restart your terminal
255
+
256
+ #### 4. Permission Issues
257
+ **Problem**: Permission denied errors
258
+ **Solution**: Run terminal as Administrator or check file permissions
259
+
260
+ ### Windows-Specific Issues
261
+
262
+ #### File Copy Commands
263
+ **Command Prompt:**
264
+ ```cmd
265
+ # Copy frontend environment file
266
+ copy frontend\.env.example frontend\.env.local
267
+
268
+ # Copy backend environment file
269
+ copy backend\.env.example backend\.env
270
+ ```
271
+
272
+ **PowerShell:**
273
+ ```powershell
274
+ # Copy frontend environment file
275
+ Copy-Item frontend\.env.example -Destination frontend\.env.local
276
+
277
+ # Copy backend environment file
278
+ Copy-Item backend\.env.example -Destination backend\.env
279
+ ```
280
+
281
+ #### Python Installation Issues
282
+ ```cmd
283
+ # If pip fails, try using python -m pip
284
+ cd backend
285
+ python -m pip install -r requirements.txt
286
+ ```
287
+
288
+ ## 📚 Additional Resources
289
+
290
+ - [Windows Compatibility Guide](./test_windows_compatibility.md)
291
+ - [Backend API Documentation](./backend/README.md)
292
+ - [Frontend Development Guide](./frontend/README.md)
293
+
294
+ ## 🤝 Contributing
295
+
296
+ 1. Fork the repository
297
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
298
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
299
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
300
+ 5. Open a Pull Request
301
+
302
+ ## 📄 License
303
+
304
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
305
+
306
+ ## 🙏 Acknowledgments
307
+
308
+ - Built with React, Flask, and Tailwind CSS
309
+ - Powered by Supabase for authentication and database
310
+ - LinkedIn API integration for social media management
311
+ - Hugging Face for AI-powered content generation
SETUP_GUIDE.md ADDED
@@ -0,0 +1,628 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Lin - Comprehensive Setup Guide
2
+
3
+ This guide provides step-by-step instructions for setting up the Lin application on different operating systems.
4
+
5
+ ## 📋 Table of Contents
6
+
7
+ 1. [Prerequisites](#prerequisites)
8
+ 2. [Quick Setup](#quick-setup)
9
+ 3. [Detailed Setup Process](#detailed-setup-process)
10
+ 4. [Environment Configuration](#environment-configuration)
11
+ 5. [Development Workflow](#development-workflow)
12
+ 6. [Troubleshooting](#troubleshooting)
13
+ 7. [Platform-Specific Instructions](#platform-specific-instructions)
14
+
15
+ ## 🚀 Prerequisites
16
+
17
+ Before you begin, ensure you have the following installed:
18
+
19
+ ### System Requirements
20
+ - **Operating System**: Windows 10/11, macOS 10.14+, or Linux (Ubuntu 18.04+)
21
+ - **RAM**: 4GB minimum, 8GB recommended
22
+ - **Storage**: 1GB free space
23
+ - **Internet Connection**: Required for downloading dependencies
24
+
25
+ ### Software Requirements
26
+ - **Node.js**: v16.0.0 or higher
27
+ - **npm**: v8.0.0 or higher
28
+ - **Python**: v3.8.0 or higher
29
+ - **Git**: v2.0.0 or higher (for cloning the repository)
30
+
31
+ ### Verification Commands
32
+
33
+ **Windows Command Prompt:**
34
+ ```cmd
35
+ # Check Node.js
36
+ node --version
37
+
38
+ # Check npm
39
+ npm --version
40
+
41
+ # Check Python
42
+ python --version
43
+
44
+ # Check Git
45
+ git --version
46
+ ```
47
+
48
+ **Windows PowerShell:**
49
+ ```powershell
50
+ # Check Node.js
51
+ node --version
52
+
53
+ # Check npm
54
+ npm --version
55
+
56
+ # Check Python
57
+ python --version
58
+
59
+ # Check Git
60
+ git --version
61
+ ```
62
+
63
+ **Linux/macOS:**
64
+ ```bash
65
+ # Check Node.js
66
+ node --version
67
+
68
+ # Check npm
69
+ npm --version
70
+
71
+ # Check Python
72
+ python3 --version
73
+
74
+ # Check Git
75
+ git --version
76
+ ```
77
+
78
+ ## 🚀 Quick Setup
79
+
80
+ ### Option 1: Automated Setup (Recommended)
81
+
82
+ ```bash
83
+ # Clone the repository
84
+ git clone <repository-url>
85
+ cd Lin
86
+
87
+ # Run automated setup
88
+ npm run setup
89
+ ```
90
+
91
+ ### Option 2: Manual Setup
92
+
93
+ ```bash
94
+ # Clone the repository
95
+ git clone <repository-url>
96
+ cd Lin
97
+
98
+ # Install dependencies
99
+ npm install
100
+
101
+ # Setup environment files
102
+ npm run setup:env
103
+
104
+ # Build the project
105
+ npm run build
106
+ ```
107
+
108
+ ## 🔧 Detailed Setup Process
109
+
110
+ ### Step 1: Clone the Repository
111
+
112
+ ```bash
113
+ # Clone using HTTPS
114
+ git clone https://github.com/your-username/lin.git
115
+
116
+ # Or clone using SSH
117
+ git clone git@github.com:your-username/lin.git
118
+
119
+ # Navigate to the project directory
120
+ cd lin
121
+ ```
122
+
123
+ ### Step 2: Install Dependencies
124
+
125
+ ```bash
126
+ # Install root dependencies
127
+ npm install
128
+
129
+ # Install frontend dependencies
130
+ npm run install:frontend
131
+
132
+ # Install backend dependencies
133
+ npm run install:backend
134
+ ```
135
+
136
+ ### Step 3: Configure Environment Variables
137
+
138
+ #### Frontend Configuration
139
+
140
+ ```bash
141
+ # Navigate to frontend directory
142
+ cd frontend
143
+
144
+ # Copy environment template
145
+ cp .env.example .env.local
146
+
147
+ # Edit the environment file
148
+ # Open .env.local in your preferred editor
149
+ ```
150
+
151
+ **Frontend Environment Variables (.env.local):**
152
+ ```env
153
+ # API Configuration
154
+ REACT_APP_API_URL=http://localhost:5000
155
+ REACT_APP_ENVIRONMENT=development
156
+
157
+ # Optional: Custom configuration
158
+ REACT_APP_APP_NAME=Lin
159
+ REACT_APP_APP_VERSION=1.0.0
160
+ ```
161
+
162
+ #### Backend Configuration
163
+
164
+ ```bash
165
+ # Navigate to backend directory
166
+ cd ../backend
167
+
168
+ # Copy environment template
169
+ cp .env.example .env
170
+
171
+ # Edit the environment file
172
+ # Open .env in your preferred editor
173
+ ```
174
+
175
+ **Backend Environment Variables (.env):**
176
+ ```env
177
+ # Supabase Configuration
178
+ SUPABASE_URL=your_supabase_project_url
179
+ SUPABASE_KEY=your_supabase_api_key
180
+
181
+ # LinkedIn OAuth Configuration
182
+ CLIENT_ID=your_linkedin_client_id
183
+ CLIENT_SECRET=your_linkedin_client_secret
184
+ REDIRECT_URL=http://localhost:5000/api/auth/callback
185
+
186
+ # AI/ML Configuration
187
+ HUGGING_KEY=your_huggingface_api_key
188
+
189
+ # Security Configuration
190
+ JWT_SECRET_KEY=your_jwt_secret_key
191
+ SECRET_KEY=your_flask_secret_key
192
+
193
+ # Application Configuration
194
+ DEBUG=True
195
+ SCHEDULER_ENABLED=True
196
+ PORT=5000
197
+ ```
198
+
199
+ ### Step 4: Build the Project
200
+
201
+ ```bash
202
+ # Navigate back to root directory
203
+ cd ..
204
+
205
+ # Build frontend for production
206
+ npm run build
207
+ ```
208
+
209
+ ### Step 5: Verify Installation
210
+
211
+ ```bash
212
+ # Run tests
213
+ npm test
214
+
215
+ # Check linting
216
+ npm run lint
217
+
218
+ # Verify build
219
+ npm run preview
220
+ ```
221
+
222
+ ## 🌐 Environment Configuration
223
+
224
+ ### Development Environment
225
+
226
+ ```bash
227
+ # Start development servers
228
+ npm run dev:all
229
+
230
+ # Or start individually
231
+ npm run dev:frontend # Frontend: http://localhost:3000
232
+ npm run dev:backend # Backend: http://localhost:5000
233
+ ```
234
+
235
+ ### Production Environment
236
+
237
+ ```bash
238
+ # Build for production
239
+ npm run build:prod
240
+
241
+ # Start production servers
242
+ npm run start:prod
243
+ ```
244
+
245
+ ### Environment-Specific Configuration
246
+
247
+ #### Development
248
+ - Frontend runs on http://localhost:3000
249
+ - Backend API runs on http://localhost:5000
250
+ - Hot reload enabled
251
+ - Debug logging enabled
252
+
253
+ #### Production
254
+ - Frontend built to static files
255
+ - Backend runs with optimized settings
256
+ - Debug logging disabled
257
+ - Error handling optimized
258
+
259
+ ## 🛠️ Development Workflow
260
+
261
+ ### Daily Development
262
+
263
+ 1. **Start the Development Environment**
264
+ ```bash
265
+ npm run dev:all
266
+ ```
267
+
268
+ 2. **Make Changes to Code**
269
+ - Frontend changes are automatically hot-reloaded
270
+ - Backend changes require restart
271
+
272
+ 3. **Test Changes**
273
+ ```bash
274
+ # Run specific tests
275
+ npm test
276
+
277
+ # Run linting
278
+ npm run lint
279
+
280
+ # Fix linting issues
281
+ npm run lint:fix
282
+ ```
283
+
284
+ 4. **Commit Changes**
285
+ ```bash
286
+ git add .
287
+ git commit -m "Descriptive commit message"
288
+ git push
289
+ ```
290
+
291
+ ### Building for Production
292
+
293
+ 1. **Clean Previous Builds**
294
+ ```bash
295
+ npm run clean
296
+ ```
297
+
298
+ 2. **Build for Production**
299
+ ```bash
300
+ npm run build:prod
301
+ ```
302
+
303
+ 3. **Test Production Build**
304
+ ```bash
305
+ npm run preview
306
+ ```
307
+
308
+ 4. **Deploy**
309
+ ```bash
310
+ # Deploy to your preferred hosting platform
311
+ npm run deploy
312
+ ```
313
+
314
+ ### Common Development Tasks
315
+
316
+ #### Adding New Dependencies
317
+
318
+ ```bash
319
+ # Add frontend dependency
320
+ cd frontend
321
+ npm install package-name
322
+
323
+ # Add backend dependency
324
+ cd ../backend
325
+ pip install package-name
326
+ ```
327
+
328
+ #### Updating Dependencies
329
+
330
+ ```bash
331
+ # Update frontend dependencies
332
+ cd frontend
333
+ npm update
334
+
335
+ # Update backend dependencies
336
+ cd ../backend
337
+ pip install --upgrade package-name
338
+ ```
339
+
340
+ #### Database Migrations
341
+
342
+ ```bash
343
+ # Run database migrations
344
+ cd backend
345
+ flask db upgrade
346
+ ```
347
+
348
+ ## 🔍 Troubleshooting
349
+
350
+ ### Common Issues and Solutions
351
+
352
+ #### 1. ENOENT Error: no such file or directory
353
+
354
+ **Problem**: Running npm commands from the wrong directory
355
+ **Solution**: Always run npm commands from the project root directory
356
+
357
+ ```bash
358
+ # Verify you're in the correct directory
359
+ pwd # Linux/macOS
360
+ cd # Windows (shows current directory)
361
+
362
+ # List files to confirm package.json exists
363
+ ls package.json # Linux/macOS
364
+ dir package.json # Windows
365
+ ```
366
+
367
+ #### 2. Port Already in Use
368
+
369
+ **Problem**: Port 3000 or 5000 is already occupied
370
+ **Solution**: Find and stop the process using the port
371
+
372
+ **Windows Command Prompt:**
373
+ ```cmd
374
+ # Find process using port 3000
375
+ netstat -ano | findstr :3000
376
+
377
+ # Find process using port 5000
378
+ netstat -ano | findstr :5000
379
+
380
+ # Kill process (replace PID with actual process ID)
381
+ taskkill /F /PID <PID>
382
+ ```
383
+
384
+ **Windows PowerShell:**
385
+ ```powershell
386
+ # Find process using port 3000
387
+ netstat -ano | Select-String ":3000"
388
+
389
+ # Find process using port 5000
390
+ netstat -ano | Select-String ":5000"
391
+
392
+ # Kill process (replace PID with actual process ID)
393
+ Stop-Process -Id <PID> -Force
394
+ ```
395
+
396
+ **Linux/macOS:**
397
+ ```bash
398
+ # Find process using port 3000
399
+ lsof -i :3000
400
+
401
+ # Find process using port 5000
402
+ lsof -i :5000
403
+
404
+ # Kill process (replace PID with actual process ID)
405
+ kill -9 <PID>
406
+ ```
407
+
408
+ #### 3. Python/Node.js Not Recognized
409
+
410
+ **Problem**: Command not found errors
411
+ **Solution**: Add Python and Node.js to system PATH
412
+
413
+ **Windows:**
414
+ 1. Press `Win + R` and type `sysdm.cpl`
415
+ 2. Go to "Advanced" tab > "Environment Variables"
416
+ 3. Under "System variables", edit "Path"
417
+ 4. Add paths to Python and Node.js installation directories
418
+ 5. Restart your terminal
419
+
420
+ **Linux (Ubuntu/Debian):**
421
+ ```bash
422
+ # Add to ~/.bashrc or ~/.zshrc
423
+ echo 'export PATH=$PATH:/path/to/python' >> ~/.bashrc
424
+ echo 'export PATH=$PATH:/path/to/node' >> ~/.bashrc
425
+ source ~/.bashrc
426
+ ```
427
+
428
+ **macOS:**
429
+ ```bash
430
+ # Add to ~/.zshrc or ~/.bash_profile
431
+ echo 'export PATH=$PATH:/path/to/python' >> ~/.zshrc
432
+ echo 'export PATH=$PATH:/path/to/node' >> ~/.zshrc
433
+ source ~/.zshrc
434
+ ```
435
+
436
+ #### 4. Permission Denied Errors
437
+
438
+ **Problem**: Permission issues when installing dependencies
439
+ **Solution**: Run with proper permissions or use package managers
440
+
441
+ **Windows:**
442
+ ```cmd
443
+ # Run as Administrator
444
+ # Or check file permissions
445
+ ```
446
+
447
+ **Linux/macOS:**
448
+ ```bash
449
+ # Fix permissions
450
+ chmod -R 755 node_modules
451
+ chmod -R 755 backend/venv
452
+ ```
453
+
454
+ #### 5. Environment Variable Issues
455
+
456
+ **Problem**: Environment variables not loading
457
+ **Solution**: Verify file paths and permissions
458
+
459
+ **Windows Command Prompt:**
460
+ ```cmd
461
+ # Check if environment files exist
462
+ if exist frontend\.env.local (
463
+ echo Frontend environment file exists
464
+ ) else (
465
+ echo Frontend environment file missing
466
+ )
467
+
468
+ if exist backend\.env (
469
+ echo Backend environment file exists
470
+ ) else (
471
+ echo Backend environment file missing
472
+ )
473
+ ```
474
+
475
+ **Windows PowerShell:**
476
+ ```powershell
477
+ # Check if environment files exist
478
+ if (Test-Path frontend\.env.local) {
479
+ Write-Host "Frontend environment file exists" -ForegroundColor Green
480
+ } else {
481
+ Write-Host "Frontend environment file missing" -ForegroundColor Red
482
+ }
483
+
484
+ if (Test-Path backend\.env) {
485
+ Write-Host "Backend environment file exists" -ForegroundColor Green
486
+ } else {
487
+ Write-Host "Backend environment file missing" -ForegroundColor Red
488
+ }
489
+ ```
490
+
491
+ ## 🖥️ Platform-Specific Instructions
492
+
493
+ ### Windows Setup
494
+
495
+ #### Prerequisites Installation
496
+ 1. **Download Node.js**: Visit https://nodejs.org and download the LTS version
497
+ 2. **Download Python**: Visit https://python.org and download Python 3.8+
498
+ 3. **Install Git**: Download from https://git-scm.com
499
+
500
+ #### Environment Setup
501
+ ```cmd
502
+ # Command Prompt setup
503
+ copy frontend\.env.example frontend\.env.local
504
+ copy backend\.env.example backend\.env
505
+
506
+ # PowerShell setup
507
+ Copy-Item frontend\.env.example -Destination frontend\.env.local
508
+ Copy-Item backend\.env.example -Destination backend\.env
509
+ ```
510
+
511
+ #### Development Commands
512
+ ```cmd
513
+ # Install dependencies
514
+ npm install
515
+ npm run install:all:win
516
+
517
+ # Start development
518
+ npm run dev:all
519
+
520
+ # Build project
521
+ npm run build
522
+ ```
523
+
524
+ ### macOS Setup
525
+
526
+ #### Prerequisites Installation
527
+ ```bash
528
+ # Install using Homebrew
529
+ brew install node
530
+ brew install python
531
+ brew install git
532
+
533
+ # Or download from official websites
534
+ ```
535
+
536
+ #### Environment Setup
537
+ ```bash
538
+ # Copy environment files
539
+ cp frontend/.env.example frontend/.env.local
540
+ cp backend/.env.example backend/.env
541
+
542
+ # Set permissions
543
+ chmod 600 frontend/.env.local
544
+ chmod 600 backend/.env
545
+ ```
546
+
547
+ #### Development Commands
548
+ ```bash
549
+ # Install dependencies
550
+ npm install
551
+ npm run install:all
552
+
553
+ # Start development
554
+ npm run dev:all
555
+
556
+ # Build project
557
+ npm run build
558
+ ```
559
+
560
+ ### Linux Setup
561
+
562
+ #### Prerequisites Installation
563
+ ```bash
564
+ # Ubuntu/Debian
565
+ sudo apt update
566
+ sudo apt install nodejs npm python3 python3-pip git
567
+
568
+ # CentOS/RHEL
569
+ sudo yum install nodejs npm python3 python3-pip git
570
+
571
+ # Arch Linux
572
+ sudo pacman -S nodejs npm python python-pip git
573
+ ```
574
+
575
+ #### Environment Setup
576
+ ```bash
577
+ # Copy environment files
578
+ cp frontend/.env.example frontend/.env.local
579
+ cp backend/.env.example backend/.env
580
+
581
+ # Set permissions
582
+ chmod 600 frontend/.env.local
583
+ chmod 600 backend/.env
584
+ ```
585
+
586
+ #### Development Commands
587
+ ```bash
588
+ # Install dependencies
589
+ npm install
590
+ npm run install:all
591
+
592
+ # Start development
593
+ npm run dev:all
594
+
595
+ # Build project
596
+ npm run build
597
+ ```
598
+
599
+ ## 📚 Additional Resources
600
+
601
+ - [API Documentation](./backend/README.md)
602
+ - [Frontend Development Guide](./frontend/README.md)
603
+ - [Windows Compatibility Guide](./test_windows_compatibility.md)
604
+ - [Troubleshooting Guide](./TROUBLESHOOTING.md)
605
+ - [Contributing Guidelines](./CONTRIBUTING.md)
606
+
607
+ ## 🆘 Getting Help
608
+
609
+ If you encounter issues not covered in this guide:
610
+
611
+ 1. Check the [Troubleshooting Guide](./TROUBLESHOOTING.md)
612
+ 2. Search existing [GitHub Issues](https://github.com/your-username/lin/issues)
613
+ 3. Create a new issue with:
614
+ - Operating system and version
615
+ - Node.js and Python versions
616
+ - Error messages and stack traces
617
+ - Steps to reproduce the issue
618
+
619
+ ## 🎯 Next Steps
620
+
621
+ After completing the setup:
622
+
623
+ 1. **Explore the Application**: Navigate to http://localhost:3000
624
+ 2. **Read the Documentation**: Check the API documentation and user guides
625
+ 3. **Join the Community**: Join our Discord server or mailing list
626
+ 4. **Start Contributing**: Check out the contributing guidelines
627
+
628
+ Happy coding! 🚀
UI_COMPONENT_SNAPSHOT.md ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Lin UI Component Snapshot
2
+
3
+ ## Overview
4
+
5
+ This document provides a snapshot of the current UI components in the Lin application, focusing on recent changes to the Header and Sidebar components.
6
+
7
+ ## Recent UI Changes
8
+
9
+ ### Header Component Updates
10
+
11
+ The Header component has been modified to improve the user interface:
12
+
13
+ 1. **Moved User Profile and Logout**:
14
+ - Relocated the user profile and logout functionality to the far right of the header
15
+ - This change provides a more intuitive user experience by placing account-related actions in the top-right corner
16
+
17
+ 2. **Removed Desktop Navigation Items**:
18
+ - Cleared the desktop navigation area (previously in the center) to create a cleaner interface
19
+ - This change focuses attention on the primary content and reduces visual clutter
20
+
21
+ ### Sidebar Component Updates
22
+
23
+ The Sidebar component has been modified to improve the user interface:
24
+
25
+ 1. **Removed Username Display**:
26
+ - Removed the username display from the bottom of the sidebar
27
+ - This change creates a cleaner sidebar interface and reduces information overload
28
+
29
+ ## Current UI Component Structure
30
+
31
+ ### Header Component
32
+
33
+ Location: `frontend/src/components/Header/Header.jsx`
34
+
35
+ Key Features:
36
+ - Fixed position at the top of the screen
37
+ - Responsive design for mobile and desktop
38
+ - Mobile menu toggle button
39
+ - User profile and logout functionality (top-right)
40
+ - Logo and application title (top-left)
41
+ - Backdrop blur effect for modern appearance
42
+
43
+ ### Sidebar Component
44
+
45
+ Location: `frontend/src/components/Sidebar/Sidebar.jsx`
46
+
47
+ Key Features:
48
+ - Collapsible design with smooth animations
49
+ - Responsive behavior for mobile and desktop
50
+ - Navigation menu with icons and labels
51
+ - Gradient backgrounds and modern styling
52
+ - Touch-optimized for mobile devices
53
+ - Keyboard navigation support
54
+
55
+ ### App Layout
56
+
57
+ Location: `frontend/src/App.jsx`
58
+
59
+ Key Features:
60
+ - Conditional rendering based on authentication state
61
+ - Responsive layout with Header and Sidebar
62
+ - Mobile-first design approach
63
+ - Accessibility features (skip links, ARIA attributes)
64
+ - Performance optimizations (lazy loading, memoization)
65
+
66
+ ## Component Interactions
67
+
68
+ ### Authentication State Handling
69
+
70
+ The UI components adapt based on the user's authentication state:
71
+
72
+ 1. **Unauthenticated Users**:
73
+ - See only the logo and application title in the header
74
+ - No sidebar is displayed
75
+ - Redirected to login/register pages
76
+
77
+ 2. **Authenticated Users**:
78
+ - See user profile and logout options in the header
79
+ - Have access to the full sidebar navigation
80
+ - Can access protected routes (dashboard, sources, posts, etc.)
81
+
82
+ ### Responsive Behavior
83
+
84
+ 1. **Desktop (>1024px)**:
85
+ - Full sidebar is visible by default
86
+ - Header displays user profile information
87
+ - Traditional navigation patterns
88
+
89
+ 2. **Mobile/Tablet (<1024px)**:
90
+ - Sidebar is collapsed by default
91
+ - Header includes mobile menu toggle
92
+ - Touch-optimized interactions
93
+ - Overlay effects for mobile menus
94
+
95
+ ## Styling and Design System
96
+
97
+ ### Color Palette
98
+
99
+ The application uses a consistent color palette:
100
+
101
+ - Primary: Burgundy (#910029)
102
+ - Secondary: Dark Gray (#39404B)
103
+ - Accent: Light Blue (#ECF4F7)
104
+ - Background: Light gradient backgrounds
105
+ - Text: Dark Blue-Gray (#2c3e50)
106
+
107
+ ### Typography
108
+
109
+ - Font family: System UI fonts
110
+ - Font weights: 400 (regular), 500 (medium), 600 (semi-bold), 700 (bold)
111
+ - Responsive font sizes using Tailwind's scale
112
+
113
+ ### Spacing System
114
+
115
+ - Consistent spacing using Tailwind's spacing scale
116
+ - Responsive padding and margin adjustments
117
+ - Grid-based layout system
118
+
119
+ ## Performance Considerations
120
+
121
+ ### Optimizations Implemented
122
+
123
+ 1. **Lazy Loading**:
124
+ - Components loaded on-demand
125
+ - Code splitting for better initial load times
126
+
127
+ 2. **Memoization**:
128
+ - React.memo for components
129
+ - useMemo and useCallback for expensive operations
130
+
131
+ 3. **Skeleton Loading**:
132
+ - Loading states for data fetching
133
+ - Smooth transitions between loading and content states
134
+
135
+ ### Mobile Performance
136
+
137
+ 1. **Touch Optimization**:
138
+ - Touch-manipulation classes for better mobile interactions
139
+ - Hardware acceleration for animations
140
+
141
+ 2. **Reduced Complexity**:
142
+ - Simplified animations on mobile devices
143
+ - Optimized rendering for smaller screens
144
+
145
+ ## Accessibility Features
146
+
147
+ ### Implemented Features
148
+
149
+ 1. **Keyboard Navigation**:
150
+ - Arrow key navigation for menus
151
+ - Escape key to close modals/menus
152
+ - Skip links for screen readers
153
+
154
+ 2. **ARIA Attributes**:
155
+ - Proper labeling of interactive elements
156
+ - Role attributes for semantic structure
157
+ - Live regions for dynamic content
158
+
159
+ 3. **Focus Management**:
160
+ - Visible focus indicators
161
+ - Proper focus trapping for modals
162
+ - Focus restoration after interactions
163
+
164
+ ## Testing and Quality Assurance
165
+
166
+ ### Component Testing
167
+
168
+ 1. **Unit Tests**:
169
+ - Component rendering tests
170
+ - Prop validation
171
+ - Event handling verification
172
+
173
+ 2. **Integration Tests**:
174
+ - State management verification
175
+ - API integration testing
176
+ - Routing behavior validation
177
+
178
+ ### Browser Compatibility
179
+
180
+ 1. **Supported Browsers**:
181
+ - Latest Chrome, Firefox, Safari, Edge
182
+ - Mobile browsers (iOS Safari, Chrome for Android)
183
+
184
+ 2. **Responsive Testing**:
185
+ - Multiple screen sizes
186
+ - Orientation changes
187
+ - Touch vs. mouse interactions
188
+
189
+ ## Future Improvements
190
+
191
+ ### Planned Enhancements
192
+
193
+ 1. **UI/UX Improvements**:
194
+ - Enhanced animations and transitions
195
+ - Improved loading states
196
+ - Additional accessibility features
197
+
198
+ 2. **Performance Optimizations**:
199
+ - Further code splitting
200
+ - Image optimization
201
+ - Caching strategies
202
+
203
+ 3. **Feature Additions**:
204
+ - Dark mode support
205
+ - Customizable layouts
206
+ - Advanced analytics dashboard
207
+
208
+ This snapshot represents the current state of the UI components as of the recent changes. The modifications to the Header and Sidebar have created a cleaner, more intuitive interface while maintaining all core functionality.
api_design.md ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # API Design Document
2
+
3
+ ## Overview
4
+ This document outlines the RESTful API endpoints for the Lin application backend. The API will be implemented using Flask and will follow REST conventions.
5
+
6
+ ## Authentication
7
+ All endpoints (except authentication endpoints) require a valid JWT token in the Authorization header:
8
+ ```
9
+ Authorization: Bearer <token>
10
+ ```
11
+
12
+ ## Error Handling
13
+ All endpoints will return appropriate HTTP status codes:
14
+ - 200: Success
15
+ - 201: Created
16
+ - 400: Bad Request
17
+ - 401: Unauthorized
18
+ - 404: Not Found
19
+ - 500: Internal Server Error
20
+
21
+ Error responses will follow this format:
22
+ ```json
23
+ {
24
+ "error": "Error message",
25
+ "code": "ERROR_CODE"
26
+ }
27
+ ```
28
+
29
+ ## Endpoints
30
+
31
+ ### Authentication
32
+
33
+ #### Register User
34
+ - **POST** `/api/auth/register`
35
+ - **Description**: Register a new user
36
+ - **Request Body**:
37
+ ```json
38
+ {
39
+ "email": "string",
40
+ "password": "string",
41
+ "confirm_password": "string"
42
+ }
43
+ ```
44
+ - **Response**:
45
+ ```json
46
+ {
47
+ "message": "User registered successfully",
48
+ "user": {
49
+ "id": "string",
50
+ "email": "string"
51
+ }
52
+ }
53
+ ```
54
+
55
+ #### Login User
56
+ - **POST** `/api/auth/login`
57
+ - **Description**: Authenticate a user
58
+ - **Request Body**:
59
+ ```json
60
+ {
61
+ "email": "string",
62
+ "password": "string"
63
+ }
64
+ ```
65
+ - **Response**:
66
+ ```json
67
+ {
68
+ "token": "string",
69
+ "user": {
70
+ "id": "string",
71
+ "email": "string"
72
+ }
73
+ }
74
+ ```
75
+
76
+ #### Logout User
77
+ - **POST** `/api/auth/logout`
78
+ - **Description**: Logout current user
79
+ - **Response**:
80
+ ```json
81
+ {
82
+ "message": "Logged out successfully"
83
+ }
84
+ ```
85
+
86
+ #### Get Current User
87
+ - **GET** `/api/auth/user`
88
+ - **Description**: Get current authenticated user
89
+ - **Response**:
90
+ ```json
91
+ {
92
+ "user": {
93
+ "id": "string",
94
+ "email": "string"
95
+ }
96
+ }
97
+ ```
98
+
99
+ ### Sources
100
+
101
+ #### Get All Sources
102
+ - **GET** `/api/sources`
103
+ - **Description**: Get all sources for the current user
104
+ - **Response**:
105
+ ```json
106
+ {
107
+ "sources": [
108
+ {
109
+ "id": "string",
110
+ "user_id": "string",
111
+ "source": "string",
112
+ "category": "string",
113
+ "last_update": "datetime"
114
+ }
115
+ ]
116
+ }
117
+ ```
118
+
119
+ #### Add Source
120
+ - **POST** `/api/sources`
121
+ - **Description**: Add a new source
122
+ - **Request Body**:
123
+ ```json
124
+ {
125
+ "source": "string"
126
+ }
127
+ ```
128
+ - **Response**:
129
+ ```json
130
+ {
131
+ "message": "Source added successfully",
132
+ "source": {
133
+ "id": "string",
134
+ "user_id": "string",
135
+ "source": "string",
136
+ "category": "string",
137
+ "last_update": "datetime"
138
+ }
139
+ }
140
+ ```
141
+
142
+ #### Delete Source
143
+ - **DELETE** `/api/sources/{id}`
144
+ - **Description**: Delete a source
145
+ - **Response**:
146
+ ```json
147
+ {
148
+ "message": "Source deleted successfully"
149
+ }
150
+ ```
151
+
152
+ ### Social Accounts
153
+
154
+ #### Get All Accounts
155
+ - **GET** `/api/accounts`
156
+ - **Description**: Get all social media accounts for the current user
157
+ - **Response**:
158
+ ```json
159
+ {
160
+ "accounts": [
161
+ {
162
+ "id": "string",
163
+ "user_id": "string",
164
+ "social_network": "string",
165
+ "account_name": "string",
166
+ "created_at": "datetime"
167
+ }
168
+ ]
169
+ }
170
+ ```
171
+
172
+ #### Add Account
173
+ - **POST** `/api/accounts`
174
+ - **Description**: Add a new social media account
175
+ - **Request Body**:
176
+ ```json
177
+ {
178
+ "account_name": "string",
179
+ "social_network": "string"
180
+ }
181
+ ```
182
+ - **Response**:
183
+ ```json
184
+ {
185
+ "message": "Account added successfully",
186
+ "account": {
187
+ "id": "string",
188
+ "user_id": "string",
189
+ "social_network": "string",
190
+ "account_name": "string",
191
+ "created_at": "datetime"
192
+ }
193
+ }
194
+ ```
195
+
196
+ #### Delete Account
197
+ - **DELETE** `/api/accounts/{id}`
198
+ - **Description**: Delete a social media account
199
+ - **Response**:
200
+ ```json
201
+ {
202
+ "message": "Account deleted successfully"
203
+ }
204
+ ```
205
+
206
+ ### Posts
207
+
208
+ #### Get All Posts
209
+ - **GET** `/api/posts`
210
+ - **Description**: Get all posts for the current user
211
+ - **Query Parameters**:
212
+ - `published` (boolean): Filter by published status
213
+ - **Response**:
214
+ ```json
215
+ {
216
+ "posts": [
217
+ {
218
+ "id": "string",
219
+ "user_id": "string",
220
+ "social_account_id": "string",
221
+ "text_content": "string",
222
+ "image_content_url": "string",
223
+ "is_published": "boolean",
224
+ "created_at": "datetime",
225
+ "scheduled_at": "datetime"
226
+ }
227
+ ]
228
+ }
229
+ ```
230
+
231
+ #### Generate Post
232
+ - **POST** `/api/posts/generate`
233
+ - **Description**: Generate a new post using AI
234
+ - **Request Body**:
235
+ ```json
236
+ {
237
+ "user_id": "string"
238
+ }
239
+ ```
240
+ - **Response**:
241
+ ```json
242
+ {
243
+ "content": "string"
244
+ }
245
+ ```
246
+
247
+ #### Create Post
248
+ - **POST** `/api/posts`
249
+ - **Description**: Create a new post
250
+ - **Request Body**:
251
+ ```json
252
+ {
253
+ "social_account_id": "string",
254
+ "text_content": "string",
255
+ "image_content_url": "string",
256
+ "scheduled_at": "datetime"
257
+ }
258
+ ```
259
+ - **Response**:
260
+ ```json
261
+ {
262
+ "message": "Post created successfully",
263
+ "post": {
264
+ "id": "string",
265
+ "user_id": "string",
266
+ "social_account_id": "string",
267
+ "text_content": "string",
268
+ "image_content_url": "string",
269
+ "is_published": "boolean",
270
+ "created_at": "datetime",
271
+ "scheduled_at": "datetime"
272
+ }
273
+ }
274
+ ```
275
+
276
+ #### Publish Post
277
+ - **POST** `/api/posts/{id}/publish`
278
+ - **Description**: Publish a post to social media
279
+ - **Response**:
280
+ ```json
281
+ {
282
+ "message": "Post published successfully"
283
+ }
284
+ ```
285
+
286
+ #### Delete Post
287
+ - **DELETE** `/api/posts/{id}`
288
+ - **Description**: Delete a post
289
+ - **Response**:
290
+ ```json
291
+ {
292
+ "message": "Post deleted successfully"
293
+ }
294
+ ```
295
+
296
+ ### Schedules
297
+
298
+ #### Get All Schedules
299
+ - **GET** `/api/schedules`
300
+ - **Description**: Get all schedules for the current user
301
+ - **Response**:
302
+ ```json
303
+ {
304
+ "schedules": [
305
+ {
306
+ "id": "string",
307
+ "social_account_id": "string",
308
+ "schedule_time": "string",
309
+ "adjusted_time": "string",
310
+ "created_at": "datetime"
311
+ }
312
+ ]
313
+ }
314
+ ```
315
+
316
+ #### Create Schedule
317
+ - **POST** `/api/schedules`
318
+ - **Description**: Create a new schedule
319
+ - **Request Body**:
320
+ ```json
321
+ {
322
+ "social_account_id": "string",
323
+ "schedule_time": "string", // Format: "Monday 18:00"
324
+ "days": ["string"] // Array of days
325
+ }
326
+ ```
327
+ - **Response**:
328
+ ```json
329
+ {
330
+ "message": "Schedule created successfully",
331
+ "schedule": {
332
+ "id": "string",
333
+ "social_account_id": "string",
334
+ "schedule_time": "string",
335
+ "adjusted_time": "string",
336
+ "created_at": "datetime"
337
+ }
338
+ }
339
+ ```
340
+
341
+ #### Delete Schedule
342
+ - **DELETE** `/api/schedules/{id}`
343
+ - **Description**: Delete a schedule
344
+ - **Response**:
345
+ ```json
346
+ {
347
+ "message": "Schedule deleted successfully"
348
+ }
app.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Entry point for the Lin application on Hugging Face Spaces.
3
+ This file imports and runs the backend Flask application directly.
4
+ """
5
+ import os
6
+ import sys
7
+
8
+ if __name__ == '__main__':
9
+ # Set the port for Hugging Face Spaces
10
+ port = os.environ.get('PORT', '7860')
11
+ os.environ.setdefault('PORT', port)
12
+
13
+ print(f"Starting Lin application on port {port}...")
14
+
15
+ try:
16
+ # Import and run the backend Flask app directly
17
+ from backend.app import create_app
18
+ app = create_app()
19
+ app.run(
20
+ host='0.0.0.0',
21
+ port=int(port),
22
+ debug=False
23
+ )
24
+ except Exception as e:
25
+ print(f"Failed to start Lin application: {e}")
26
+ sys.exit(1)
architecture_summary.md ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Lin React Clone - Architecture Summary
2
+
3
+ ## Project Overview
4
+ This document provides a comprehensive summary of the architecture for the React clone of the Lin application, which includes a Flask API backend and a React frontend.
5
+
6
+ ## Current Status
7
+ The current Taipy-based Lin application has been thoroughly analyzed, and a complete architecture plan has been created for the React clone with the following components:
8
+
9
+ ### Documentation Created
10
+ 1. [Project Analysis](project_analysis.md) - Analysis of the current Taipy application
11
+ 2. [README](README.md) - Overview of the React clone project
12
+ 3. [Backend Structure](backend_structure.md) - Planned structure for the Flask API backend
13
+ 4. [Frontend Structure](frontend_structure.md) - Planned structure for the React frontend
14
+ 5. [API Design](api_design.md) - Detailed RESTful API endpoints
15
+ 6. [Component Architecture](component_architecture.md) - React component hierarchy and design
16
+ 7. [Backend Requirements](backend_requirements.md) - Technical requirements for the Flask backend
17
+ 8. [Frontend Requirements](frontend_requirements.md) - Technical requirements for the React frontend
18
+ 9. [Deployment Architecture](deployment_architecture.md) - Infrastructure and deployment plan
19
+ 10. [Development Roadmap](development_roadmap.md) - Phased implementation plan
20
+
21
+ ## Key Architectural Decisions
22
+
23
+ ### Backend Architecture
24
+ - **Framework**: Flask for lightweight, flexible API development
25
+ - **Database**: Supabase (PostgreSQL-based) for data persistence
26
+ - **Authentication**: JWT-based authentication with secure token management
27
+ - **Scheduling**: APScheduler for task scheduling with conflict resolution
28
+ - **External Integrations**: LinkedIn API and Hugging Face API
29
+ - **Deployment**: Containerized deployment with horizontal scaling
30
+
31
+ ### Frontend Architecture
32
+ - **Framework**: React with functional components and hooks
33
+ - **State Management**: Redux Toolkit for predictable state management
34
+ - **Routing**: React Router for client-side routing
35
+ - **UI Components**: Material-UI for consistent, accessible components
36
+ - **Form Handling**: Formik with Yup for form validation
37
+ - **API Communication**: Axios with interceptors for HTTP requests
38
+ - **Deployment**: Static hosting with CDN for optimal performance
39
+
40
+ ### Data Flow
41
+ 1. User interacts with React frontend
42
+ 2. Frontend makes API calls to Flask backend
43
+ 3. Backend processes requests and interacts with Supabase database
44
+ 4. Backend integrates with external APIs (LinkedIn, Hugging Face)
45
+ 5. Backend returns data to frontend
46
+ 6. Frontend updates UI based on response
47
+
48
+ ### Security Considerations
49
+ - JWT tokens for secure authentication
50
+ - HTTPS encryption for all communications
51
+ - Input validation and sanitization
52
+ - CORS policy configuration
53
+ - Secure storage of sensitive data
54
+ - Rate limiting for API endpoints
55
+
56
+ ## Implementation Roadmap
57
+
58
+ The development is planned in 6 phases over 12 weeks:
59
+
60
+ 1. **Foundation** (Weeks 1-2): Project setup, authentication
61
+ 2. **Core Features** (Weeks 3-4): Source and account management
62
+ 3. **Content Management** (Weeks 5-6): Post creation and publishing
63
+ 4. **Scheduling System** (Weeks 7-8): Automated scheduling
64
+ 5. **Advanced Features** (Weeks 9-10): Analytics and optimization
65
+ 6. **Testing and Deployment** (Weeks 11-12): Production readiness
66
+
67
+ ## Technology Stack
68
+
69
+ ### Backend
70
+ - Flask (Python web framework)
71
+ - Supabase (Database and authentication)
72
+ - APScheduler (Task scheduling)
73
+ - requests (HTTP library)
74
+ - python-dotenv (Environment management)
75
+
76
+ ### Frontend
77
+ - React (JavaScript library)
78
+ - Redux Toolkit (State management)
79
+ - Material-UI (UI components)
80
+ - React Router (Routing)
81
+ - Axios (HTTP client)
82
+ - Formik/Yup (Form handling)
83
+
84
+ ## Deployment Architecture
85
+
86
+ The application will be deployed with:
87
+ - CDN for frontend assets
88
+ - Load balancer for backend API
89
+ - Containerized Flask applications
90
+ - Supabase for database and authentication
91
+ - Monitoring and logging infrastructure
92
+ - CI/CD pipeline for automated deployment
93
+
94
+ ## Next Steps
95
+
96
+ To begin implementation, the following actions are recommended:
97
+
98
+ 1. Set up development environments for both frontend and backend
99
+ 2. Create GitHub repositories for version control
100
+ 3. Implement the foundation phase (authentication, project structure)
101
+ 4. Begin CI/CD pipeline setup
102
+ 5. Start frontend and backend development in parallel
103
+
104
+ ## Success Criteria
105
+
106
+ The success of this architecture will be measured by:
107
+ - Performance (API response times < 500ms)
108
+ - Reliability (99.9% uptime)
109
+ - Scalability (support for 10,000+ users)
110
+ - User satisfaction (intuitive UI/UX)
111
+ - Maintainability (modular, well-documented code)
backend/.env.example ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Supabase configuration
2
+ SUPABASE_URL="https://xscdoxrxtnibshcfznuy.supabase.co"
3
+ SUPABASE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhzY2RveHJ4dG5pYnNoY2Z6bnV5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTAyNDk1MzMsImV4cCI6MjA2NTgyNTUzM30.C7EF6VwL44O0yS2Xi5dLz_iNSk6s-8cO1fQq7aab8NA"
4
+
5
+ # LinkedIn OAuth configuration
6
+ CLIENT_ID="786uz2fclmtgnd"
7
+ CLIENT_SECRET="WPL_AP1.L2w25M5xMwPb6vR3.75REhw=="
8
+ REDIRECT_URL=https://zelyanoth-lin.hf.space/auth/callback
9
+
10
+ # Hugging Face configuration
11
+
12
+
13
+ # JWT configuration
14
+ JWT_SECRET_KEY=your_jwt_secret_key
15
+
16
+ # Database configuration (if using direct database connection)
17
+ DATABASE_URL=your_database_url
18
+
19
+ # Application configuration
20
+ SECRET_KEY=your_secret_key
21
+ DEBUG=True
22
+
23
+ # Scheduler configuration
24
+ SCHEDULER_ENABLED=True
25
+
26
+ # Port configuration
27
+ PORT=5000
backend/Dockerfile ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.9 slim image
2
+ FROM python:3.9-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Set environment variables for UTF-8 encoding
8
+ ENV PYTHONDONTWRITEBYTECODE 1
9
+ ENV PYTHONUNBUFFERED 1
10
+ ENV LANG=C.UTF-8
11
+ ENV LC_ALL=C.UTF-8
12
+ ENV PYTHONIOENCODING=utf-8
13
+ ENV PYTHONUTF8=1
14
+ ENV DOCKER_CONTAINER=true
15
+
16
+ # Install system dependencies
17
+ RUN apt-get update \
18
+ && apt-get install -y --no-install-recommends \
19
+ build-essential \
20
+ && rm -rf /var/lib/apt/lists/*
21
+
22
+ # Copy requirements file
23
+ COPY requirements.txt .
24
+
25
+ # Install Python dependencies
26
+ RUN pip install --no-cache-dir -r requirements.txt
27
+
28
+ # Copy project files
29
+ COPY . .
30
+
31
+ # Create non-root user
32
+ RUN adduser --disabled-password --gecos '' appuser
33
+ RUN chown -R appuser:appuser /app
34
+ USER appuser
35
+
36
+ # Expose port
37
+ EXPOSE 5000
38
+
39
+ # Run the application
40
+ CMD ["python", "app.py"]
backend/README.md ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Lin Backend API
2
+
3
+ This is the Flask backend API for the Lin application, a community manager assistant for LinkedIn.
4
+
5
+ ## Features
6
+
7
+ - User authentication (registration, login, logout)
8
+ - Social media account management (LinkedIn integration)
9
+ - RSS source management
10
+ - AI-powered content generation
11
+ - Post scheduling and publishing
12
+ - RESTful API design
13
+ - JWT-based authentication
14
+ - Task scheduling with Celery
15
+
16
+ ## Technologies
17
+
18
+ - Flask (Python web framework)
19
+ - Supabase (Database and authentication)
20
+ - Celery (Distributed task queue)
21
+ - Redis (Message broker for Celery)
22
+ - requests (HTTP library)
23
+ - requests-oauthlib (OAuth support)
24
+ - gradio-client (Hugging Face API)
25
+ - Flask-JWT-Extended (JWT token management)
26
+
27
+ ## Project Structure
28
+
29
+ ```
30
+ backend/
31
+ ├── app.py # Flask application entry point
32
+ ├── config.py # Configuration settings
33
+ ├── requirements.txt # Python dependencies
34
+ ├── .env.example # Environment variables example
35
+ ├── celery_app.py # Celery application configuration
36
+ ├── celery_beat_config.py # Celery Beat configuration
37
+ ├── start_celery.sh # Script to start Celery components (Linux/Mac)
38
+ ├── start_celery.bat # Script to start Celery components (Windows)
39
+ ├── TASK_SCHEDULING_EVOLUTION.md # Documentation on migration from APScheduler to Celery
40
+ ├── models/ # Data models
41
+ │ ├── __init__.py
42
+ │ ├── user.py # User model
43
+ │ ├── social_account.py # Social media account model
44
+ │ ├── source.py # RSS source model
45
+ │ ├── post.py # Post content model
46
+ │ └── schedule.py # Scheduling model
47
+ ├── api/ # API endpoints
48
+ │ ├── __init__.py
49
+ │ ├── auth.py # Authentication endpoints
50
+ │ ├── sources.py # Source management endpoints
51
+ │ ├── accounts.py # Social account endpoints
52
+ │ ├── posts.py # Post management endpoints
53
+ │ └── schedules.py # Scheduling endpoints
54
+ ├── services/ # Business logic
55
+ │ ├── __init__.py
56
+ │ ├── auth_service.py # Authentication service
57
+ │ ├── linkedin_service.py# LinkedIn integration service
58
+ │ ├── content_service.py # Content generation service
59
+ │ └── schedule_service.py# Scheduling service
60
+ ├── celery_tasks/ # Celery tasks
61
+ │ ├── __init__.py
62
+ │ ├── content_tasks.py # Content generation and publishing tasks
63
+ │ ├── scheduler.py # Scheduler functions
64
+ │ └── schedule_loader.py # Task for loading schedules from database
65
+ ├── utils/ # Utility functions
66
+ │ ├── __init__.py
67
+ │ └── database.py # Database connection
68
+ └── scheduler/ # Task scheduling (deprecated, kept for backward compatibility)
69
+ ├── __init__.py
70
+ └── task_scheduler.py # Scheduling implementation
71
+ ```
72
+
73
+ ## API Endpoints
74
+
75
+ ### Authentication
76
+ - `POST /api/auth/register` - Register a new user
77
+ - `POST /api/auth/login` - Login user
78
+ - `POST /api/auth/logout` - Logout user
79
+ - `GET /api/auth/user` - Get current user
80
+
81
+ ### Sources
82
+ - `GET /api/sources` - Get all sources for current user
83
+ - `POST /api/sources` - Add a new source
84
+ - `DELETE /api/sources/<id>` - Delete a source
85
+
86
+ ### Accounts
87
+ - `GET /api/accounts` - Get all social accounts for current user
88
+ - `POST /api/accounts` - Add a new social account
89
+ - `POST /api/accounts/callback` - Handle OAuth callback
90
+ - `DELETE /api/accounts/<id>` - Delete a social account
91
+
92
+ ### Posts
93
+ - `GET /api/posts` - Get all posts for current user
94
+ - `POST /api/posts/generate` - Generate AI content
95
+ - `POST /api/posts` - Create a new post
96
+ - `POST /api/posts/<id>/publish` - Publish a post
97
+ - `DELETE /api/posts/<id>` - Delete a post
98
+
99
+ ### Schedules
100
+ - `GET /api/schedules` - Get all schedules for current user
101
+ - `POST /api/schedules` - Create a new schedule
102
+ - `DELETE /api/schedules/<id>` - Delete a schedule
103
+
104
+ ## Setup Instructions
105
+
106
+ 1. **Install dependencies**:
107
+ ```bash
108
+ pip install -r requirements.txt
109
+ ```
110
+
111
+ 2. **Set up environment variables**:
112
+ Copy `.env.example` to `.env` and fill in your values:
113
+ ```bash
114
+ # Windows Command Prompt
115
+ copy .env.example .env
116
+
117
+ # PowerShell
118
+ Copy-Item .env.example .env
119
+ ```
120
+
121
+ 3. **Start Redis Server**:
122
+ ```bash
123
+ # If you have Redis installed locally
124
+ redis-server
125
+
126
+ # Or use Docker
127
+ docker run -d -p 6379:6379 redis:alpine
128
+ ```
129
+
130
+ 4. **Start Celery Components**:
131
+ ```bash
132
+ # Start Celery worker
133
+ celery -A celery_app worker --loglevel=info
134
+
135
+ # Start Celery Beat scheduler (in another terminal)
136
+ celery -A celery_beat_config beat --loglevel=info
137
+ ```
138
+
139
+ 5. **Run the application**:
140
+ ```bash
141
+ python app.py
142
+ ```
143
+
144
+ ## Environment Variables
145
+
146
+ - `SUPABASE_URL` - Your Supabase project URL
147
+ - `SUPABASE_KEY` - Your Supabase API key
148
+ - `CLIENT_ID` - LinkedIn OAuth client ID
149
+ - `CLIENT_SECRET` - LinkedIn OAuth client secret
150
+ - `REDIRECT_URL` - LinkedIn OAuth redirect URL
151
+ - `HUGGING_KEY` - Hugging Face API key
152
+ - `JWT_SECRET_KEY` - Secret key for JWT token generation
153
+ - `SECRET_KEY` - Flask secret key
154
+ - `DEBUG` - Debug mode (True/False)
155
+ - `SCHEDULER_ENABLED` - Enable/disable task scheduler (True/False)
156
+ - `PORT` - Port to run the application on
157
+ - `CELERY_BROKER_URL` - Redis URL for Celery broker (default: redis://localhost:6379/0)
158
+ - `CELERY_RESULT_BACKEND` - Redis URL for Celery result backend (default: redis://localhost:6379/0)
159
+
160
+ ## Development
161
+
162
+ ### Running Tests
163
+
164
+ ```bash
165
+ pytest
166
+ ```
167
+
168
+ ### Linting
169
+
170
+ ```bash
171
+ flake8 .
172
+ ```
173
+
174
+ ### Starting Celery Components with Scripts
175
+
176
+ On Linux/Mac:
177
+ ```bash
178
+ # Start both worker and scheduler
179
+ ./start_celery.sh all
180
+
181
+ # Start only worker
182
+ ./start_celery.sh worker
183
+
184
+ # Start only scheduler
185
+ ./start_celery.sh beat
186
+ ```
187
+
188
+ On Windows:
189
+ ```cmd
190
+ # Start both worker and scheduler
191
+ start_celery.bat all
192
+
193
+ # Start only worker
194
+ start_celery.bat worker
195
+
196
+ # Start only scheduler
197
+ start_celery.bat beat
198
+ ```
199
+
200
+ ### Windows-Specific Issues
201
+
202
+ 1. **File Copy Commands**
203
+ - Use `copy` command in Command Prompt or `Copy-Item` in PowerShell
204
+ - Avoid using Unix-style `cp` command which doesn't work on Windows
205
+
206
+ 2. **Python Installation Issues**
207
+ - Ensure Python is added to your system PATH
208
+ - Try using `python` instead of `python3` if you have Python 3.x installed
209
+ - If `pip` fails, try `python -m pip install -r requirements.txt`
210
+
211
+ 3. **Permission Issues**
212
+ - If you encounter permission errors, try running your terminal as Administrator
213
+ - Or check file permissions on the project directory
214
+
215
+ 4. **Port Conflicts**
216
+ - Windows might have other services using port 5000
217
+ - Use `netstat -ano | findstr :5000` to check what's using port 5000
218
+ - Change port in configuration if needed
219
+
220
+ 5. **Virtual Environment Issues**
221
+ - If using virtual environments, ensure they're activated properly
222
+ - On Windows, use `venv\Scripts\activate` for Command Prompt
223
+ - Or `venv\Scripts\Activate.ps1` for PowerShell
224
+
225
+ 6. **Environment Variables**
226
+ - Use `set` command in Command Prompt to set environment variables
227
+ - Use `$env:VARIABLE_NAME="value"` in PowerShell
228
+ - Or use the Windows GUI (System Properties > Environment Variables)
229
+
230
+ ## Deployment
231
+
232
+ The application can be deployed to any platform that supports Python applications:
233
+
234
+ 1. **Heroku**:
235
+ - Create a new Heroku app
236
+ - Set environment variables in Heroku config
237
+ - Deploy using Git
238
+ - Add Redis add-on for Celery
239
+
240
+ 2. **Docker**:
241
+ ```bash
242
+ # The docker-compose.yml file includes Redis
243
+ docker-compose up
244
+ ```
245
+
246
+ 3. **Traditional Server**:
247
+ - Install Python dependencies
248
+ - Set environment variables
249
+ - Install and start Redis
250
+ - Start Celery components
251
+ - Run with a WSGI server like Gunicorn
252
+
253
+ ## Task Scheduling with Celery
254
+
255
+ The application now uses Celery for task scheduling instead of APScheduler. This provides better scalability and reliability:
256
+
257
+ 1. **Content Generation Tasks**: Generate AI content for scheduled posts
258
+ 2. **Post Publishing Tasks**: Publish posts to LinkedIn
259
+ 3. **Schedule Loader Task**: Periodically loads schedules from the database
260
+
261
+ Tasks are stored in Redis and can be processed by multiple workers, making the system more robust and scalable.
262
+
263
+ For more details on the migration from APScheduler to Celery, see `TASK_SCHEDULING_EVOLUTION.md`.
264
+
265
+ ## Contributing
266
+
267
+ 1. Fork the repository
268
+ 2. Create a feature branch
269
+ 3. Commit your changes
270
+ 4. Push to the branch
271
+ 5. Create a pull request
272
+
273
+ ## License
274
+
275
+ This project is licensed under the MIT License.
backend/TASK_SCHEDULING_EVOLUTION.md ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Task Scheduling Evolution: From APScheduler to Celery
2
+
3
+ ## Overview
4
+
5
+ This document describes the evolution of the task scheduling system in the Lin application from using APScheduler to using Celery. This change was made to improve scalability, reliability, and maintainability of the scheduling system.
6
+
7
+ ## Previous Implementation (APScheduler)
8
+
9
+ The previous implementation used APScheduler (Advanced Python Scheduler) for managing scheduled tasks. While APScheduler is a powerful library, it has some limitations in production environments:
10
+
11
+ 1. **Single Process**: APScheduler runs within the same process as the Flask application, which can lead to resource contention.
12
+ 2. **Limited Scalability**: Difficult to scale across multiple instances or servers.
13
+ 3. **Persistence Issues**: While APScheduler supports job persistence, it's not as robust as dedicated task queues.
14
+ 4. **Monitoring**: Limited built-in monitoring and management capabilities.
15
+
16
+ ### Key Components of APScheduler Implementation:
17
+
18
+ - `backend/scheduler/task_scheduler.py`: Main scheduler implementation
19
+ - `backend/app.py`: Scheduler initialization in Flask app
20
+ - Jobs were stored in memory and periodically reloaded from the database
21
+
22
+ ## New Implementation (Celery)
23
+
24
+ The new implementation uses Celery, a distributed task queue, which provides several advantages:
25
+
26
+ 1. **Distributed Processing**: Tasks can be distributed across multiple workers and machines.
27
+ 2. **Persistence**: Tasks are stored in a message broker (Redis) for reliability.
28
+ 3. **Scalability**: Easy to scale by adding more workers.
29
+ 4. **Monitoring**: Built-in monitoring tools and integration with Flower for web-based monitoring.
30
+ 5. **Fault Tolerance**: Workers can be restarted without losing tasks.
31
+ 6. **Flexible Routing**: Tasks can be routed to specific queues for better resource management.
32
+
33
+ ### Key Components of Celery Implementation:
34
+
35
+ - `backend/celery_app.py`: Main Celery application configuration
36
+ - `backend/celery_tasks/`: Directory containing Celery tasks
37
+ - `content_tasks.py`: Tasks for content generation and publishing
38
+ - `scheduler.py`: Scheduler functions for task management
39
+ - `schedule_loader.py`: Task for loading schedules from database
40
+ - `backend/celery_beat_config.py`: Celery Beat configuration for periodic tasks
41
+ - `backend/scheduler/task_scheduler.py`: Updated scheduler that works with Celery
42
+
43
+ ### Celery Architecture
44
+
45
+ The new implementation follows this architecture:
46
+
47
+ ```
48
+ [Flask App] --> [Redis Broker] --> [Celery Workers]
49
+ ^
50
+ |
51
+ [Celery Beat]
52
+ ```
53
+
54
+ 1. **Flask App**: The main web application that can trigger tasks
55
+ 2. **Redis Broker**: Message broker that stores tasks and results
56
+ 3. **Celery Workers**: Processes that execute tasks
57
+ 4. **Celery Beat**: Scheduler that triggers periodic tasks
58
+
59
+ ### Task Types
60
+
61
+ 1. **Content Generation Task**: Generates content for scheduled posts
62
+ 2. **Post Publishing Task**: Publishes posts to LinkedIn
63
+ 3. **Schedule Loader Task**: Periodically loads schedules from the database and updates Celery Beat
64
+
65
+ ### Configuration
66
+
67
+ The Celery implementation is configured through environment variables:
68
+
69
+ - `CELERY_BROKER_URL`: URL for the message broker (default: redis://localhost:6379/0)
70
+ - `CELERY_RESULT_BACKEND`: URL for the result backend (default: redis://localhost:6379/0)
71
+
72
+ ### Running the System
73
+
74
+ To run the new Celery-based system:
75
+
76
+ 1. **Start Redis Server**:
77
+ ```bash
78
+ redis-server
79
+ ```
80
+
81
+ 2. **Start Celery Workers**:
82
+ ```bash
83
+ cd backend
84
+ celery -A celery_app worker --loglevel=info
85
+ ```
86
+
87
+ 3. **Start Celery Beat (Scheduler)**:
88
+ ```bash
89
+ cd backend
90
+ celery -A celery_beat_config beat --loglevel=info
91
+ ```
92
+
93
+ 4. **Start Flask Application**:
94
+ ```bash
95
+ cd backend
96
+ python app.py
97
+ ```
98
+
99
+ ### Benefits of the Migration
100
+
101
+ 1. **Improved Reliability**: Tasks are persisted in Redis and survive worker restarts
102
+ 2. **Better Scalability**: Can easily add more workers to handle increased load
103
+ 3. **Enhanced Monitoring**: Can use Flower to monitor tasks and workers
104
+ 4. **Separation of Concerns**: Web application and task processing are now separate processes
105
+ 5. **Fault Tolerance**: If one worker fails, others can continue processing tasks
106
+ 6. **Flexible Deployment**: Workers can be deployed on different machines
107
+
108
+ ### Migration Process
109
+
110
+ The migration was done in a way that maintains backward compatibility:
111
+
112
+ 1. **New Components Added**: All Celery components were added without removing the old ones
113
+ 2. **Configuration Update**: The Flask app was updated to use Celery instead of APScheduler
114
+ 3. **Task Refactoring**: Existing task logic was refactored into Celery tasks
115
+ 4. **Testing**: The new system was tested to ensure it works correctly
116
+ 5. **Documentation**: This document was created to explain the changes
117
+
118
+ ### Future Improvements
119
+
120
+ 1. **Add Flower for Monitoring**: Integrate Flower for web-based task monitoring
121
+ 2. **Implement Retry Logic**: Add more sophisticated retry logic for failed tasks
122
+ 3. **Add Task Priorities**: Implement task priorities for better resource management
123
+ 4. **Enhance Error Handling**: Improve error handling and logging for tasks
124
+ 5. **Add Task Chaining**: Use Celery's chaining capabilities for complex workflows
backend/api/__init__.py ADDED
File without changes
backend/api/accounts.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify, current_app
2
+ from flask_jwt_extended import jwt_required, get_jwt_identity
3
+ from services.linkedin_service import LinkedInService
4
+ import secrets
5
+
6
+ accounts_bp = Blueprint('accounts', __name__)
7
+
8
+ @accounts_bp.route('/', methods=['OPTIONS'])
9
+ @accounts_bp.route('', methods=['OPTIONS'])
10
+ def handle_options():
11
+ """Handle OPTIONS requests for preflight CORS checks."""
12
+ return '', 200
13
+
14
+ @accounts_bp.route('/', methods=['GET'])
15
+ @accounts_bp.route('', methods=['GET'])
16
+ @jwt_required()
17
+ def get_accounts():
18
+ """
19
+ Get all social media accounts for the current user.
20
+
21
+ Returns:
22
+ JSON: List of social media accounts
23
+ """
24
+ try:
25
+ user_id = get_jwt_identity()
26
+
27
+ # Check if Supabase client is initialized
28
+ if not hasattr(current_app, 'supabase') or current_app.supabase is None:
29
+ # Add CORS headers to error response
30
+ response_data = jsonify({
31
+ 'success': False,
32
+ 'message': 'Database connection not initialized'
33
+ })
34
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
35
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
36
+ return response_data, 500
37
+
38
+ # Fetch accounts from Supabase
39
+ response = (
40
+ current_app.supabase
41
+ .table("Social_network")
42
+ .select("*")
43
+ .eq("id_utilisateur", user_id)
44
+ .execute()
45
+ )
46
+
47
+ accounts = response.data if response.data else []
48
+
49
+ # Add CORS headers explicitly
50
+ response_data = jsonify({
51
+ 'success': True,
52
+ 'accounts': accounts
53
+ })
54
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
55
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
56
+ return response_data, 200
57
+
58
+ except Exception as e:
59
+ current_app.logger.error(f"Get accounts error: {str(e)}")
60
+ # Add CORS headers to error response
61
+ response_data = jsonify({
62
+ 'success': False,
63
+ 'message': 'An error occurred while fetching accounts'
64
+ })
65
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
66
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
67
+ return response_data, 500
68
+
69
+ @accounts_bp.route('/', methods=['POST'])
70
+ @accounts_bp.route('', methods=['POST'])
71
+ @jwt_required()
72
+ def add_account():
73
+ """
74
+ Add a new social media account for the current user.
75
+
76
+ Request Body:
77
+ account_name (str): Account name
78
+ social_network (str): Social network name
79
+
80
+ Returns:
81
+ JSON: Add account result
82
+ """
83
+ try:
84
+ user_id = get_jwt_identity()
85
+ data = request.get_json()
86
+
87
+ # Validate required fields
88
+ if not data or not all(k in data for k in ('account_name', 'social_network')):
89
+ return jsonify({
90
+ 'success': False,
91
+ 'message': 'Account name and social network are required'
92
+ }), 400
93
+
94
+ account_name = data['account_name']
95
+ social_network = data['social_network']
96
+
97
+ # For LinkedIn, initiate OAuth flow
98
+ if social_network.lower() == 'linkedin':
99
+ linkedin_service = LinkedInService()
100
+ # Generate a random state for security
101
+ state = secrets.token_urlsafe(32)
102
+
103
+ # Store state in session or database for verification later
104
+ # For now, we'll return it to the frontend
105
+ authorization_url = linkedin_service.get_authorization_url(state)
106
+
107
+ return jsonify({
108
+ 'success': True,
109
+ 'message': 'Please authenticate with LinkedIn',
110
+ 'authorization_url': authorization_url,
111
+ 'state': state
112
+ }), 200
113
+ else:
114
+ return jsonify({
115
+ 'success': False,
116
+ 'message': 'Unsupported social network'
117
+ }), 400
118
+
119
+ except Exception as e:
120
+ current_app.logger.error(f"Add account error: {str(e)}")
121
+ return jsonify({
122
+ 'success': False,
123
+ 'message': f'An error occurred while adding account: {str(e)}'
124
+ }), 500
125
+
126
+ @accounts_bp.route('/callback', methods=['POST'])
127
+ @jwt_required()
128
+ def handle_oauth_callback():
129
+ """
130
+ Handle OAuth callback from social network.
131
+
132
+ Request Body:
133
+ code (str): Authorization code
134
+ state (str): State parameter
135
+ social_network (str): Social network name
136
+
137
+ Returns:
138
+ JSON: OAuth callback result
139
+ """
140
+ try:
141
+ user_id = get_jwt_identity()
142
+ data = request.get_json()
143
+
144
+ # Validate required fields
145
+ if not data or not all(k in data for k in ('code', 'state', 'social_network')):
146
+ return jsonify({
147
+ 'success': False,
148
+ 'message': 'Code, state, and social network are required'
149
+ }), 400
150
+
151
+ code = data['code']
152
+ state = data['state']
153
+ social_network = data['social_network']
154
+
155
+ # Verify state (in a real implementation, you would check against stored state)
156
+ # For now, we'll skip this verification
157
+
158
+ if social_network.lower() == 'linkedin':
159
+ linkedin_service = LinkedInService()
160
+
161
+ # Exchange code for access token
162
+ token_response = linkedin_service.get_access_token(code)
163
+ access_token = token_response['access_token']
164
+
165
+ # Get user info
166
+ user_info = linkedin_service.get_user_info(access_token)
167
+
168
+ # Store account info in Supabase
169
+ response = (
170
+ current_app.supabase
171
+ .table("Social_network")
172
+ .insert({
173
+ "social_network": social_network,
174
+ "account_name": user_info.get('name', 'LinkedIn Account'),
175
+ "id_utilisateur": user_id,
176
+ "token": access_token,
177
+ "sub": user_info.get('sub'),
178
+ "given_name": user_info.get('given_name'),
179
+ "family_name": user_info.get('family_name'),
180
+ "picture": user_info.get('picture')
181
+ })
182
+ .execute()
183
+ )
184
+
185
+ if response.data:
186
+ return jsonify({
187
+ 'success': True,
188
+ 'message': 'Account linked successfully',
189
+ 'account': response.data[0]
190
+ }), 200
191
+ else:
192
+ return jsonify({
193
+ 'success': False,
194
+ 'message': 'Failed to link account'
195
+ }), 500
196
+ else:
197
+ return jsonify({
198
+ 'success': False,
199
+ 'message': 'Unsupported social network'
200
+ }), 400
201
+
202
+ except Exception as e:
203
+ current_app.logger.error(f"OAuth callback error: {str(e)}")
204
+ return jsonify({
205
+ 'success': False,
206
+ 'message': f'An error occurred during OAuth callback: {str(e)}'
207
+ }), 500
208
+
209
+ @accounts_bp.route('/<account_id>', methods=['OPTIONS'])
210
+ def handle_account_options(account_id):
211
+ """Handle OPTIONS requests for preflight CORS checks for specific account."""
212
+ return '', 200
213
+
214
+ @accounts_bp.route('/<account_id>', methods=['DELETE'])
215
+ @jwt_required()
216
+ def delete_account(account_id):
217
+ """
218
+ Delete a social media account.
219
+
220
+ Path Parameters:
221
+ account_id (str): Account ID
222
+
223
+ Returns:
224
+ JSON: Delete account result
225
+ """
226
+ try:
227
+ user_id = get_jwt_identity()
228
+
229
+ # Delete account from Supabase
230
+ response = (
231
+ current_app.supabase
232
+ .table("Social_network")
233
+ .delete()
234
+ .eq("id", account_id)
235
+ .eq("id_utilisateur", user_id)
236
+ .execute()
237
+ )
238
+
239
+ if response.data:
240
+ return jsonify({
241
+ 'success': True,
242
+ 'message': 'Account deleted successfully'
243
+ }), 200
244
+ else:
245
+ return jsonify({
246
+ 'success': False,
247
+ 'message': 'Account not found or unauthorized'
248
+ }), 404
249
+
250
+ except Exception as e:
251
+ current_app.logger.error(f"Delete account error: {str(e)}")
252
+ return jsonify({
253
+ 'success': False,
254
+ 'message': 'An error occurred while deleting account'
255
+ }), 500
256
+
257
+ @accounts_bp.route('/<account_id>/primary', methods=['OPTIONS'])
258
+ def handle_primary_options(account_id):
259
+ """Handle OPTIONS requests for preflight CORS checks for primary account."""
260
+ return '', 200
261
+
262
+ @accounts_bp.route('/<account_id>/primary', methods=['PUT'])
263
+ @jwt_required()
264
+ def set_primary_account(account_id):
265
+ """
266
+ Set an account as primary for the user.
267
+
268
+ Path Parameters:
269
+ account_id (str): Account ID
270
+
271
+ Returns:
272
+ JSON: Update result
273
+ """
274
+ try:
275
+ user_id = get_jwt_identity()
276
+
277
+ # First, get all accounts for this user
278
+ response = (
279
+ current_app.supabase
280
+ .table("Social_network")
281
+ .select("*")
282
+ .eq("id_utilisateur", user_id)
283
+ .execute()
284
+ )
285
+
286
+ accounts = response.data if response.data else []
287
+
288
+ # Check if account exists and belongs to user
289
+ account_exists = any(account['id'] == account_id and account['id_utilisateur'] == user_id for account in accounts)
290
+
291
+ if not account_exists:
292
+ return jsonify({
293
+ 'success': False,
294
+ 'message': 'Account not found or unauthorized'
295
+ }), 404
296
+
297
+ # For now, we'll just return success
298
+ # In a real implementation, you might want to add a 'is_primary' field
299
+ # and update all accounts accordingly
300
+
301
+ return jsonify({
302
+ 'success': True,
303
+ 'message': 'Account set as primary successfully'
304
+ }), 200
305
+
306
+ except Exception as e:
307
+ current_app.logger.error(f"Set primary account error: {str(e)}")
308
+ return jsonify({
309
+ 'success': False,
310
+ 'message': 'An error occurred while setting primary account'
311
+ }), 500
backend/api/auth.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify, current_app
2
+ from flask_jwt_extended import jwt_required, get_jwt_identity
3
+ from services.auth_service import register_user, login_user, get_user_by_id
4
+ from models.user import User
5
+
6
+ auth_bp = Blueprint('auth', __name__)
7
+
8
+ @auth_bp.route('/', methods=['OPTIONS'])
9
+ def handle_options():
10
+ """Handle OPTIONS requests for preflight CORS checks."""
11
+ return '', 200
12
+
13
+ @auth_bp.route('/register', methods=['OPTIONS'])
14
+ def handle_register_options():
15
+ """Handle OPTIONS requests for preflight CORS checks for register route."""
16
+ return '', 200
17
+
18
+ @auth_bp.route('/register', methods=['POST'])
19
+ def register():
20
+ """
21
+ Register a new user.
22
+
23
+ Request Body:
24
+ email (str): User email
25
+ password (str): User password
26
+ confirm_password (str): Password confirmation
27
+
28
+ Returns:
29
+ JSON: Registration result
30
+ """
31
+ try:
32
+ data = request.get_json()
33
+
34
+ # Validate required fields
35
+ if not data or not all(k in data for k in ('email', 'password', 'confirm_password')):
36
+ return jsonify({
37
+ 'success': False,
38
+ 'message': 'Email, password, and confirm_password are required'
39
+ }), 400
40
+
41
+ email = data['email']
42
+ password = data['password']
43
+ confirm_password = data['confirm_password']
44
+
45
+ # Validate password confirmation
46
+ if password != confirm_password:
47
+ return jsonify({
48
+ 'success': False,
49
+ 'message': 'Passwords do not match'
50
+ }), 400
51
+
52
+ # Validate password length
53
+ if len(password) < 8:
54
+ return jsonify({
55
+ 'success': False,
56
+ 'message': 'Password must be at least 8 characters long'
57
+ }), 400
58
+
59
+ # Register user
60
+ result = register_user(email, password)
61
+
62
+ if result['success']:
63
+ return jsonify(result), 201
64
+ else:
65
+ return jsonify(result), 400
66
+
67
+ except Exception as e:
68
+ current_app.logger.error(f"Registration error: {str(e)}")
69
+ return jsonify({
70
+ 'success': False,
71
+ 'message': 'An error occurred during registration'
72
+ }), 500
73
+
74
+ @auth_bp.route('/login', methods=['OPTIONS'])
75
+ def handle_login_options():
76
+ """Handle OPTIONS requests for preflight CORS checks for login route."""
77
+ return '', 200
78
+
79
+ @auth_bp.route('/login', methods=['POST'])
80
+ def login():
81
+ """
82
+ Authenticate and login a user.
83
+
84
+ Request Body:
85
+ email (str): User email
86
+ password (str): User password
87
+ remember_me (bool): Remember me flag for extended session (optional)
88
+
89
+ Returns:
90
+ JSON: Login result with JWT token
91
+ """
92
+ try:
93
+ data = request.get_json()
94
+
95
+ # Validate required fields
96
+ if not data or not all(k in data for k in ('email', 'password')):
97
+ return jsonify({
98
+ 'success': False,
99
+ 'message': 'Email and password are required'
100
+ }), 400
101
+
102
+ email = data['email']
103
+ password = data['password']
104
+ remember_me = data.get('remember_me', False)
105
+
106
+ # Login user
107
+ result = login_user(email, password, remember_me)
108
+
109
+ if result['success']:
110
+ # Set CORS headers explicitly
111
+ response_data = jsonify(result)
112
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
113
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
114
+ return response_data, 200
115
+ else:
116
+ return jsonify(result), 401
117
+
118
+ except Exception as e:
119
+ current_app.logger.error(f"Login error: {str(e)}")
120
+ return jsonify({
121
+ 'success': False,
122
+ 'message': 'An error occurred during login'
123
+ }), 500
124
+
125
+ @auth_bp.route('/logout', methods=['OPTIONS'])
126
+ def handle_logout_options():
127
+ """Handle OPTIONS requests for preflight CORS checks for logout route."""
128
+ return '', 200
129
+
130
+ @auth_bp.route('/logout', methods=['POST'])
131
+ @jwt_required()
132
+ def logout():
133
+ """
134
+ Logout current user.
135
+
136
+ Returns:
137
+ JSON: Logout result
138
+ """
139
+ try:
140
+ return jsonify({
141
+ 'success': True,
142
+ 'message': 'Logged out successfully'
143
+ }), 200
144
+
145
+ except Exception as e:
146
+ current_app.logger.error(f"Logout error: {str(e)}")
147
+ return jsonify({
148
+ 'success': False,
149
+ 'message': 'An error occurred during logout'
150
+ }), 500
151
+
152
+ @auth_bp.route('/user', methods=['OPTIONS'])
153
+ def handle_user_options():
154
+ """Handle OPTIONS requests for preflight CORS checks for user route."""
155
+ return '', 200
156
+
157
+ @auth_bp.route('/user', methods=['GET'])
158
+ @jwt_required()
159
+ def get_current_user():
160
+ """
161
+ Get current authenticated user.
162
+
163
+ Returns:
164
+ JSON: Current user data
165
+ """
166
+ try:
167
+ user_id = get_jwt_identity()
168
+ user_data = get_user_by_id(user_id)
169
+
170
+ if user_data:
171
+ return jsonify({
172
+ 'success': True,
173
+ 'user': user_data
174
+ }), 200
175
+ else:
176
+ return jsonify({
177
+ 'success': False,
178
+ 'message': 'User not found'
179
+ }), 404
180
+
181
+ except Exception as e:
182
+ current_app.logger.error(f"Get user error: {str(e)}")
183
+ return jsonify({
184
+ 'success': False,
185
+ 'message': 'An error occurred while fetching user data'
186
+ }), 500
backend/api/posts.py ADDED
@@ -0,0 +1,574 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import codecs
2
+ import uuid
3
+ from flask import Blueprint, request, jsonify, current_app
4
+ from flask_jwt_extended import jwt_required, get_jwt_identity
5
+ from services.content_service import ContentService
6
+ from services.linkedin_service import LinkedInService
7
+
8
+ posts_bp = Blueprint('posts', __name__)
9
+
10
+ def safe_log_message(message):
11
+ """Safely log messages containing Unicode characters."""
12
+ try:
13
+ # Try to encode as UTF-8 first, then decode with error handling
14
+ if isinstance(message, str):
15
+ # For strings, try to encode and decode safely
16
+ encoded = message.encode('utf-8', errors='replace')
17
+ safe_message = encoded.decode('utf-8', errors='replace')
18
+ else:
19
+ # For non-strings, convert to string first
20
+ safe_message = str(message)
21
+
22
+ # Log to app logger instead of print
23
+ current_app.logger.debug(safe_message)
24
+ except Exception as e:
25
+ # Ultimate fallback - log the error
26
+ current_app.logger.error(f"Failed to log message: {str(e)}")
27
+
28
+ @posts_bp.route('/', methods=['OPTIONS'])
29
+ @posts_bp.route('', methods=['OPTIONS'])
30
+ def handle_options():
31
+ """Handle OPTIONS requests for preflight CORS checks."""
32
+ return '', 200
33
+
34
+ @posts_bp.route('/', methods=['GET'])
35
+ @posts_bp.route('', methods=['GET'])
36
+ @jwt_required()
37
+ def get_posts():
38
+ """
39
+ Get all posts for the current user.
40
+
41
+ Query Parameters:
42
+ published (bool): Filter by published status
43
+
44
+ Returns:
45
+ JSON: List of posts
46
+ """
47
+ try:
48
+ user_id = get_jwt_identity()
49
+ published = request.args.get('published', type=bool)
50
+
51
+ # Check if Supabase client is initialized
52
+ if not hasattr(current_app, 'supabase') or current_app.supabase is None:
53
+ # Add CORS headers to error response
54
+ response_data = jsonify({
55
+ 'success': False,
56
+ 'message': 'Database connection not initialized'
57
+ })
58
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
59
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
60
+ return response_data, 500
61
+
62
+ # Build query
63
+ query = (
64
+ current_app.supabase
65
+ .table("Post_content")
66
+ .select("*, Social_network(id_utilisateur)")
67
+ )
68
+
69
+ # Apply published filter if specified
70
+ if published is not None:
71
+ query = query.eq("is_published", published)
72
+
73
+ response = query.execute()
74
+
75
+ # Filter posts for the current user
76
+ user_posts = [
77
+ post for post in response.data
78
+ if post.get('Social_network', {}).get('id_utilisateur') == user_id
79
+ ] if response.data else []
80
+
81
+ # Add CORS headers explicitly
82
+ response_data = jsonify({
83
+ 'success': True,
84
+ 'posts': user_posts
85
+ })
86
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
87
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
88
+ return response_data, 200
89
+
90
+ except Exception as e:
91
+ error_message = str(e)
92
+ safe_log_message(f"Get posts error: {error_message}")
93
+ # Add CORS headers to error response
94
+ response_data = jsonify({
95
+ 'success': False,
96
+ 'message': 'An error occurred while fetching posts'
97
+ })
98
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
99
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
100
+ return response_data, 500
101
+
102
+ # Add CORS headers explicitly
103
+ response_data = jsonify({
104
+ 'success': True,
105
+ 'posts': user_posts
106
+ })
107
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
108
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
109
+ return response_data, 200
110
+
111
+ except Exception as e:
112
+ error_message = str(e)
113
+ safe_log_message(f"Get posts error: {error_message}")
114
+ # Add CORS headers to error response
115
+ response_data = jsonify({
116
+ 'success': False,
117
+ 'message': 'An error occurred while fetching posts'
118
+ })
119
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
120
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
121
+ return response_data, 500
122
+
123
+ def _generate_post_task(user_id, job_id, job_store, hugging_key):
124
+ """
125
+ Background task to generate post content.
126
+
127
+ Args:
128
+ user_id (str): User ID for personalization
129
+ job_id (str): Job ID to update status in job store
130
+ job_store (dict): Job store dictionary
131
+ hugging_key (str): Hugging Face API key
132
+ """
133
+ try:
134
+ # Update job status to processing
135
+ job_store[job_id] = {
136
+ 'status': 'processing',
137
+ 'result': None,
138
+ 'error': None
139
+ }
140
+
141
+ # Generate content using content service
142
+ # Pass the Hugging Face key directly to the service
143
+ content_service = ContentService(hugging_key=hugging_key)
144
+ generated_content = content_service.generate_post_content(user_id)
145
+
146
+ # Update job status to completed with result
147
+ job_store[job_id] = {
148
+ 'status': 'completed',
149
+ 'result': generated_content,
150
+ 'error': None
151
+ }
152
+
153
+ except Exception as e:
154
+ error_message = str(e)
155
+ safe_log_message(f"Generate post background task error: {error_message}")
156
+ # Update job status to failed with error
157
+ job_store[job_id] = {
158
+ 'status': 'failed',
159
+ 'result': None,
160
+ 'error': error_message
161
+ }
162
+
163
+ @posts_bp.route('/generate', methods=['POST'])
164
+ @jwt_required()
165
+ def generate_post():
166
+ """
167
+ Generate a new post using AI asynchronously.
168
+
169
+ Request Body:
170
+ user_id (str): User ID (optional, defaults to current user)
171
+
172
+ Returns:
173
+ JSON: Job ID for polling
174
+ """
175
+ try:
176
+ current_user_id = get_jwt_identity()
177
+ data = request.get_json()
178
+
179
+ # Use provided user_id or default to current user
180
+ user_id = data.get('user_id', current_user_id)
181
+
182
+ # Verify user authorization (can only generate for self unless admin)
183
+ if user_id != current_user_id:
184
+ return jsonify({
185
+ 'success': False,
186
+ 'message': 'Unauthorized to generate posts for other users'
187
+ }), 403
188
+
189
+ # Create a job ID
190
+ job_id = str(uuid.uuid4())
191
+
192
+ # Initialize job status
193
+ current_app.job_store[job_id] = {
194
+ 'status': 'pending',
195
+ 'result': None,
196
+ 'error': None
197
+ }
198
+
199
+ # Get Hugging Face key
200
+ hugging_key = current_app.config['HUGGING_KEY']
201
+
202
+ # Submit the background task, passing all necessary data
203
+ current_app.executor.submit(_generate_post_task, user_id, job_id, current_app.job_store, hugging_key)
204
+
205
+ # Return job ID immediately
206
+ return jsonify({
207
+ 'success': True,
208
+ 'job_id': job_id,
209
+ 'message': 'Post generation started'
210
+ }), 202 # 202 Accepted
211
+
212
+ except Exception as e:
213
+ error_message = str(e)
214
+ safe_log_message(f"Generate post error: {error_message}")
215
+ return jsonify({
216
+ 'success': False,
217
+ 'message': f'An error occurred while starting post generation: {error_message}'
218
+ }), 500
219
+
220
+ @posts_bp.route('/jobs/<job_id>', methods=['GET'])
221
+ @jwt_required()
222
+ def get_job_status(job_id):
223
+ """
224
+ Get the status of a post generation job.
225
+
226
+ Path Parameters:
227
+ job_id (str): Job ID
228
+
229
+ Returns:
230
+ JSON: Job status and result if completed
231
+ """
232
+ try:
233
+ # Get job from store
234
+ job = current_app.job_store.get(job_id)
235
+
236
+ if not job:
237
+ return jsonify({
238
+ 'success': False,
239
+ 'message': 'Job not found'
240
+ }), 404
241
+
242
+ # Prepare response
243
+ response_data = {
244
+ 'success': True,
245
+ 'job_id': job_id,
246
+ 'status': job['status']
247
+ }
248
+
249
+ # Include result or error if available
250
+ if job['status'] == 'completed':
251
+ response_data['content'] = job['result']
252
+ elif job['status'] == 'failed':
253
+ response_data['error'] = job['error']
254
+
255
+ return jsonify(response_data), 200
256
+
257
+ except Exception as e:
258
+ error_message = str(e)
259
+ safe_log_message(f"Get job status error: {error_message}")
260
+ return jsonify({
261
+ 'success': False,
262
+ 'message': f'An error occurred while fetching job status: {error_message}'
263
+ }), 500
264
+
265
+ @posts_bp.route('/', methods=['OPTIONS'])
266
+ @posts_bp.route('', methods=['OPTIONS'])
267
+ def handle_create_options():
268
+ """Handle OPTIONS requests for preflight CORS checks for create post route."""
269
+ return '', 200
270
+
271
+ @posts_bp.route('/publish-direct', methods=['OPTIONS'])
272
+ def handle_publish_direct_options():
273
+ """Handle OPTIONS requests for preflight CORS checks for publish direct route."""
274
+ return '', 200
275
+
276
+ @posts_bp.route('/publish-direct', methods=['POST'])
277
+ @jwt_required()
278
+ def publish_post_direct():
279
+ """
280
+ Publish a post directly to social media and save to database.
281
+
282
+ Request Body:
283
+ social_account_id (str): Social account ID
284
+ text_content (str): Post text content
285
+ image_content_url (str, optional): Image URL
286
+ scheduled_at (str, optional): Scheduled time in ISO format
287
+
288
+ Returns:
289
+ JSON: Publish post result
290
+ """
291
+ try:
292
+ user_id = get_jwt_identity()
293
+ data = request.get_json()
294
+
295
+ # Validate required fields
296
+ social_account_id = data.get('social_account_id')
297
+ text_content = data.get('text_content')
298
+
299
+ if not social_account_id or not text_content:
300
+ return jsonify({
301
+ 'success': False,
302
+ 'message': 'social_account_id and text_content are required'
303
+ }), 400
304
+
305
+ # Verify the social account belongs to the user
306
+ account_response = (
307
+ current_app.supabase
308
+ .table("Social_network")
309
+ .select("id_utilisateur, token, sub")
310
+ .eq("id", social_account_id)
311
+ .execute()
312
+ )
313
+
314
+ if not account_response.data:
315
+ return jsonify({
316
+ 'success': False,
317
+ 'message': 'Social account not found'
318
+ }), 404
319
+
320
+ account = account_response.data[0]
321
+ if account.get('id_utilisateur') != user_id:
322
+ return jsonify({
323
+ 'success': False,
324
+ 'message': 'Unauthorized to use this social account'
325
+ }), 403
326
+
327
+ # Get account details
328
+ access_token = account.get('token')
329
+ user_sub = account.get('sub')
330
+
331
+ if not access_token or not user_sub:
332
+ return jsonify({
333
+ 'success': False,
334
+ 'message': 'Social account not properly configured'
335
+ }), 400
336
+
337
+ # Get optional fields
338
+ image_url = data.get('image_content_url')
339
+
340
+ # Publish to LinkedIn
341
+ linkedin_service = LinkedInService()
342
+ publish_response = linkedin_service.publish_post(
343
+ access_token, user_sub, text_content, image_url
344
+ )
345
+
346
+ # Save to database as published
347
+ post_data = {
348
+ 'id_social': social_account_id,
349
+ 'Text_content': text_content,
350
+ 'is_published': True
351
+ }
352
+
353
+ # Add optional fields if provided
354
+ if image_url:
355
+ post_data['image_content_url'] = image_url
356
+
357
+ if 'scheduled_at' in data:
358
+ post_data['scheduled_at'] = data['scheduled_at']
359
+
360
+ # Insert post into database
361
+ response = (
362
+ current_app.supabase
363
+ .table("Post_content")
364
+ .insert(post_data)
365
+ .execute()
366
+ )
367
+
368
+ if response.data:
369
+ # Add CORS headers explicitly
370
+ response_data = jsonify({
371
+ 'success': True,
372
+ 'message': 'Post published and saved successfully',
373
+ 'post': response.data[0],
374
+ 'linkedin_response': publish_response
375
+ })
376
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
377
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
378
+ return response_data, 201
379
+ else:
380
+ # Add CORS headers to error response
381
+ response_data = jsonify({
382
+ 'success': False,
383
+ 'message': 'Failed to save post to database'
384
+ })
385
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
386
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
387
+ return response_data, 500
388
+
389
+ except Exception as e:
390
+ error_message = str(e)
391
+ safe_log_message(f"[Post] Publish post directly error: {error_message}")
392
+ # Add CORS headers to error response
393
+ response_data = jsonify({
394
+ 'success': False,
395
+ 'message': f'An error occurred while publishing post: {error_message}'
396
+ })
397
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
398
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
399
+ return response_data, 500
400
+
401
+ @posts_bp.route('/<post_id>', methods=['OPTIONS'])
402
+ def handle_post_options(post_id):
403
+ """Handle OPTIONS requests for preflight CORS checks for specific post."""
404
+ return '', 200
405
+
406
+ @posts_bp.route('/', methods=['POST'])
407
+ @posts_bp.route('', methods=['POST'])
408
+ @jwt_required()
409
+ def create_post():
410
+ """
411
+ Create a new post.
412
+
413
+ Request Body:
414
+ social_account_id (str): Social account ID
415
+ text_content (str): Post text content
416
+ image_content_url (str, optional): Image URL
417
+ scheduled_at (str, optional): Scheduled time in ISO format
418
+ is_published (bool, optional): Whether the post is published (defaults to True)
419
+
420
+ Returns:
421
+ JSON: Created post data
422
+ """
423
+ try:
424
+ user_id = get_jwt_identity()
425
+ data = request.get_json()
426
+
427
+ # Validate required fields
428
+ social_account_id = data.get('social_account_id')
429
+ text_content = data.get('text_content')
430
+
431
+ if not social_account_id or not text_content:
432
+ return jsonify({
433
+ 'success': False,
434
+ 'message': 'social_account_id and text_content are required'
435
+ }), 400
436
+
437
+ # Verify the social account belongs to the user
438
+ account_response = (
439
+ current_app.supabase
440
+ .table("Social_network")
441
+ .select("id_utilisateur")
442
+ .eq("id", social_account_id)
443
+ .execute()
444
+ )
445
+
446
+ if not account_response.data:
447
+ return jsonify({
448
+ 'success': False,
449
+ 'message': 'Social account not found'
450
+ }), 404
451
+
452
+ if account_response.data[0].get('id_utilisateur') != user_id:
453
+ return jsonify({
454
+ 'success': False,
455
+ 'message': 'Unauthorized to use this social account'
456
+ }), 403
457
+
458
+ # Prepare post data - always mark as published
459
+ post_data = {
460
+ 'id_social': social_account_id,
461
+ 'Text_content': text_content,
462
+ 'is_published': data.get('is_published', True) # Default to True
463
+ }
464
+
465
+ # Add optional fields if provided
466
+ if 'image_content_url' in data:
467
+ post_data['image_content_url'] = data['image_content_url']
468
+
469
+ if 'scheduled_at' in data:
470
+ post_data['scheduled_at'] = data['scheduled_at']
471
+
472
+ # Insert post into database
473
+ response = (
474
+ current_app.supabase
475
+ .table("Post_content")
476
+ .insert(post_data)
477
+ .execute()
478
+ )
479
+
480
+ if response.data:
481
+ # Add CORS headers explicitly
482
+ response_data = jsonify({
483
+ 'success': True,
484
+ 'post': response.data[0]
485
+ })
486
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
487
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
488
+ return response_data, 201
489
+ else:
490
+ # Add CORS headers to error response
491
+ response_data = jsonify({
492
+ 'success': False,
493
+ 'message': 'Failed to create post'
494
+ })
495
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
496
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
497
+ return response_data, 500
498
+
499
+ except Exception as e:
500
+ error_message = str(e)
501
+ safe_log_message(f"[Post] Create post error: {error_message}")
502
+ # Add CORS headers to error response
503
+ response_data = jsonify({
504
+ 'success': False,
505
+ 'message': f'An error occurred while creating post: {error_message}'
506
+ })
507
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
508
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
509
+ return response_data, 500
510
+
511
+ @posts_bp.route('/<post_id>', methods=['DELETE'])
512
+ @jwt_required()
513
+ def delete_post(post_id):
514
+ """
515
+ Delete a post.
516
+
517
+ Path Parameters:
518
+ post_id (str): Post ID
519
+
520
+ Returns:
521
+ JSON: Delete post result
522
+ """
523
+ try:
524
+ user_id = get_jwt_identity()
525
+
526
+ # Verify the post belongs to the user
527
+ response = (
528
+ current_app.supabase
529
+ .table("Post_content")
530
+ .select("Social_network(id_utilisateur)")
531
+ .eq("id", post_id)
532
+ .execute()
533
+ )
534
+
535
+ if not response.data:
536
+ return jsonify({
537
+ 'success': False,
538
+ 'message': 'Post not found'
539
+ }), 404
540
+
541
+ post = response.data[0]
542
+ if post.get('Social_network', {}).get('id_utilisateur') != user_id:
543
+ return jsonify({
544
+ 'success': False,
545
+ 'message': 'Unauthorized to delete this post'
546
+ }), 403
547
+
548
+ # Delete post from Supabase
549
+ delete_response = (
550
+ current_app.supabase
551
+ .table("Post_content")
552
+ .delete()
553
+ .eq("id", post_id)
554
+ .execute()
555
+ )
556
+
557
+ if delete_response.data:
558
+ return jsonify({
559
+ 'success': True,
560
+ 'message': 'Post deleted successfully'
561
+ }), 200
562
+ else:
563
+ return jsonify({
564
+ 'success': False,
565
+ 'message': 'Failed to delete post'
566
+ }), 500
567
+
568
+ except Exception as e:
569
+ error_message = str(e)
570
+ safe_log_message(f"Delete post error: {error_message}")
571
+ return jsonify({
572
+ 'success': False,
573
+ 'message': 'An error occurred while deleting post'
574
+ }), 500
backend/api/schedules.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify, current_app
2
+ from flask_jwt_extended import jwt_required, get_jwt_identity
3
+ from services.schedule_service import ScheduleService
4
+
5
+ schedules_bp = Blueprint('schedules', __name__)
6
+
7
+ @schedules_bp.route('/', methods=['OPTIONS'])
8
+ @schedules_bp.route('', methods=['OPTIONS'])
9
+ def handle_options():
10
+ """Handle OPTIONS requests for preflight CORS checks."""
11
+ return '', 200
12
+
13
+ @schedules_bp.route('/', methods=['GET'])
14
+ @schedules_bp.route('', methods=['GET'])
15
+ @jwt_required()
16
+ def get_schedules():
17
+ """
18
+ Get all schedules for the current user.
19
+
20
+ Returns:
21
+ JSON: List of schedules
22
+ """
23
+ try:
24
+ user_id = get_jwt_identity()
25
+ print(f"[DEBUG] get_schedules called for user_id: {user_id}")
26
+
27
+ # Check if Supabase client is initialized
28
+ if not hasattr(current_app, 'supabase') or current_app.supabase is None:
29
+ print("[ERROR] Supabase client not initialized")
30
+ # Add CORS headers to error response
31
+ response_data = jsonify({
32
+ 'success': False,
33
+ 'message': 'Database connection not initialized'
34
+ })
35
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
36
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
37
+ return response_data, 500
38
+
39
+ schedule_service = ScheduleService()
40
+ schedules = schedule_service.get_user_schedules(user_id)
41
+ print(f"[DEBUG] Found {len(schedules)} schedules for user {user_id}")
42
+
43
+ # Add CORS headers explicitly
44
+ response_data = jsonify({
45
+ 'success': True,
46
+ 'schedules': schedules
47
+ })
48
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
49
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
50
+ return response_data, 200
51
+
52
+ except Exception as e:
53
+ print(f"[ERROR] Get schedules error: {str(e)}")
54
+ import traceback
55
+ print(f"[ERROR] Full traceback: {traceback.format_exc()}")
56
+ current_app.logger.error(f"Get schedules error: {str(e)}")
57
+ # Add CORS headers to error response
58
+ response_data = jsonify({
59
+ 'success': False,
60
+ 'message': f'An error occurred while fetching schedules: {str(e)}',
61
+ 'schedules': [] # Return empty array on error
62
+ })
63
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
64
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
65
+ return response_data, 500
66
+
67
+ @schedules_bp.route('/', methods=['POST'])
68
+ @schedules_bp.route('', methods=['POST'])
69
+ @jwt_required()
70
+ def create_schedule():
71
+ """
72
+ Create a new schedule for the current user.
73
+
74
+ Request Body:
75
+ social_network (str): Social account ID
76
+ schedule_time (str): Schedule time in format "HH:MM"
77
+ days (List[str]): List of days to schedule
78
+
79
+ Returns:
80
+ JSON: Create schedule result
81
+ """
82
+ try:
83
+ user_id = get_jwt_identity()
84
+ data = request.get_json()
85
+
86
+ # Validate required fields
87
+ required_fields = ['social_network', 'schedule_time', 'days']
88
+ if not data or not all(k in data for k in required_fields):
89
+ # Add CORS headers to error response
90
+ response_data = jsonify({
91
+ 'success': False,
92
+ 'message': 'Social network, schedule time, and days are required'
93
+ })
94
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
95
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
96
+ return response_data, 400
97
+
98
+ social_network = data['social_network']
99
+ schedule_time = data['schedule_time']
100
+ days = data['days']
101
+
102
+ # Validate days format
103
+ valid_days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
104
+ if not isinstance(days, list) or not all(day in valid_days for day in days):
105
+ # Add CORS headers to error response
106
+ response_data = jsonify({
107
+ 'success': False,
108
+ 'message': 'Days must be a list of valid day names'
109
+ })
110
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
111
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
112
+ return response_data, 400
113
+
114
+ # Validate time format
115
+ try:
116
+ hour, minute = map(int, schedule_time.split(':'))
117
+ if hour < 0 or hour > 23 or minute < 0 or minute > 59:
118
+ raise ValueError
119
+ except ValueError:
120
+ # Add CORS headers to error response
121
+ response_data = jsonify({
122
+ 'success': False,
123
+ 'message': 'Schedule time must be in format HH:MM (24-hour format)'
124
+ })
125
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
126
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
127
+ return response_data, 400
128
+
129
+ # Create schedule using schedule service
130
+ schedule_service = ScheduleService()
131
+ result = schedule_service.create_schedule(user_id, social_network, schedule_time, days)
132
+
133
+ if result['success']:
134
+ # Add CORS headers to success response
135
+ response_data = jsonify(result)
136
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
137
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
138
+ return response_data, 201
139
+ else:
140
+ # Add CORS headers to error response
141
+ response_data = jsonify(result)
142
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
143
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
144
+ return response_data, 400
145
+
146
+ except Exception as e:
147
+ current_app.logger.error(f"Create schedule error: {str(e)}")
148
+ # Add CORS headers to error response
149
+ response_data = jsonify({
150
+ 'success': False,
151
+ 'message': f'An error occurred while creating schedule: {str(e)}'
152
+ })
153
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
154
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
155
+ return response_data, 500
156
+
157
+ @schedules_bp.route('/<schedule_id>', methods=['OPTIONS'])
158
+ def handle_schedule_options(schedule_id):
159
+ """Handle OPTIONS requests for preflight CORS checks for specific schedule."""
160
+ return '', 200
161
+
162
+ @schedules_bp.route('/<schedule_id>', methods=['DELETE'])
163
+ @jwt_required()
164
+ def delete_schedule(schedule_id):
165
+ """
166
+ Delete a schedule.
167
+
168
+ Path Parameters:
169
+ schedule_id (str): Schedule ID
170
+
171
+ Returns:
172
+ JSON: Delete schedule result
173
+ """
174
+ try:
175
+ user_id = get_jwt_identity()
176
+
177
+ # Verify the schedule belongs to the user
178
+ response = (
179
+ current_app.supabase
180
+ .table("Scheduling")
181
+ .select("Social_network(id_utilisateur)")
182
+ .eq("id", schedule_id)
183
+ .execute()
184
+ )
185
+
186
+ if not response.data:
187
+ # Add CORS headers to error response
188
+ response_data = jsonify({
189
+ 'success': False,
190
+ 'message': 'Schedule not found'
191
+ })
192
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
193
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
194
+ return response_data, 404
195
+
196
+ schedule = response.data[0]
197
+ if schedule.get('Social_network', {}).get('id_utilisateur') != user_id:
198
+ # Add CORS headers to error response
199
+ response_data = jsonify({
200
+ 'success': False,
201
+ 'message': 'Unauthorized to delete this schedule'
202
+ })
203
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
204
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
205
+ return response_data, 403
206
+
207
+ # Delete schedule using schedule service
208
+ schedule_service = ScheduleService()
209
+ result = schedule_service.delete_schedule(schedule_id)
210
+
211
+ if result['success']:
212
+ # Add CORS headers to success response
213
+ response_data = jsonify(result)
214
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
215
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
216
+ return response_data, 200
217
+ else:
218
+ # Add CORS headers to error response
219
+ response_data = jsonify(result)
220
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
221
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
222
+ return response_data, 404
223
+
224
+ except Exception as e:
225
+ current_app.logger.error(f"Delete schedule error: {str(e)}")
226
+ # Add CORS headers to error response
227
+ response_data = jsonify({
228
+ 'success': False,
229
+ 'message': f'An error occurred while deleting schedule: {str(e)}'
230
+ })
231
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
232
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
233
+ return response_data, 500
backend/api/sources.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify, current_app
2
+ from flask_jwt_extended import jwt_required, get_jwt_identity
3
+ from services.content_service import ContentService
4
+ import pandas as pd
5
+
6
+ sources_bp = Blueprint('sources', __name__)
7
+
8
+ @sources_bp.route('/', methods=['OPTIONS'])
9
+ @sources_bp.route('', methods=['OPTIONS'])
10
+ def handle_options():
11
+ """Handle OPTIONS requests for preflight CORS checks."""
12
+ return '', 200
13
+
14
+ @sources_bp.route('/', methods=['GET'])
15
+ @sources_bp.route('', methods=['GET'])
16
+ @jwt_required()
17
+ def get_sources():
18
+ """
19
+ Get all sources for the current user.
20
+
21
+ Returns:
22
+ JSON: List of sources
23
+ """
24
+ try:
25
+ user_id = get_jwt_identity()
26
+
27
+ # Check if Supabase client is initialized
28
+ if not hasattr(current_app, 'supabase') or current_app.supabase is None:
29
+ # Add CORS headers to error response
30
+ response_data = jsonify({
31
+ 'success': False,
32
+ 'message': 'Database connection not initialized'
33
+ })
34
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
35
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
36
+ return response_data, 500
37
+
38
+ # Fetch sources from Supabase
39
+ response = (
40
+ current_app.supabase
41
+ .table("Source")
42
+ .select("*")
43
+ .eq("user_id", user_id)
44
+ .execute()
45
+ )
46
+
47
+ sources = response.data if response.data else []
48
+
49
+ # Add CORS headers explicitly
50
+ response_data = jsonify({
51
+ 'success': True,
52
+ 'sources': sources
53
+ })
54
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
55
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
56
+ return response_data, 200
57
+
58
+ except Exception as e:
59
+ current_app.logger.error(f"Get sources error: {str(e)}")
60
+ # Add CORS headers to error response
61
+ response_data = jsonify({
62
+ 'success': False,
63
+ 'message': 'An error occurred while fetching sources'
64
+ })
65
+ response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
66
+ response_data.headers.add('Access-Control-Allow-Credentials', 'true')
67
+ return response_data, 500
68
+
69
+ @sources_bp.route('/', methods=['POST'])
70
+ @sources_bp.route('', methods=['POST'])
71
+ @jwt_required()
72
+ def add_source():
73
+ """
74
+ Add a new source for the current user.
75
+
76
+ Request Body:
77
+ source (str): Source URL
78
+
79
+ Returns:
80
+ JSON: Add source result
81
+ """
82
+ try:
83
+ user_id = get_jwt_identity()
84
+ data = request.get_json()
85
+
86
+ # Validate required fields
87
+ if not data or 'source' not in data:
88
+ return jsonify({
89
+ 'success': False,
90
+ 'message': 'Source URL is required'
91
+ }), 400
92
+
93
+ source_url = data['source']
94
+
95
+ # Use content service to add source
96
+ try:
97
+ content_service = ContentService()
98
+ result = content_service.add_rss_source(source_url, user_id)
99
+
100
+ return jsonify({
101
+ 'success': True,
102
+ 'message': result
103
+ }), 201
104
+ except Exception as e:
105
+ # If content service fails, just store in database directly
106
+ current_app.logger.warning(f"Content service failed, storing in database directly: {str(e)}")
107
+
108
+ # Store source directly in Supabase
109
+ response = (
110
+ current_app.supabase
111
+ .table("Source")
112
+ .insert({
113
+ "url": source_url,
114
+ "user_id": user_id,
115
+ "created_at": "now()"
116
+ })
117
+ .execute()
118
+ )
119
+
120
+ if response.data:
121
+ return jsonify({
122
+ 'success': True,
123
+ 'message': 'Source added successfully'
124
+ }), 201
125
+ else:
126
+ raise Exception("Failed to store source in database")
127
+
128
+ except Exception as e:
129
+ current_app.logger.error(f"Add source error: {str(e)}")
130
+ return jsonify({
131
+ 'success': False,
132
+ 'message': f'An error occurred while adding source: {str(e)}'
133
+ }), 500
134
+
135
+ @sources_bp.route('/<source_id>', methods=['OPTIONS'])
136
+ def handle_source_options(source_id):
137
+ """Handle OPTIONS requests for preflight CORS checks for specific source."""
138
+ return '', 200
139
+
140
+ @sources_bp.route('/<source_id>', methods=['DELETE'])
141
+ @jwt_required()
142
+ def delete_source(source_id):
143
+ """
144
+ Delete a source.
145
+
146
+ Path Parameters:
147
+ source_id (str): Source ID
148
+
149
+ Returns:
150
+ JSON: Delete source result
151
+ """
152
+ try:
153
+ user_id = get_jwt_identity()
154
+
155
+ # Delete source from Supabase
156
+ response = (
157
+ current_app.supabase
158
+ .table("Source")
159
+ .delete()
160
+ .eq("id", source_id)
161
+ .eq("user_id", user_id)
162
+ .execute()
163
+ )
164
+
165
+ if response.data:
166
+ return jsonify({
167
+ 'success': True,
168
+ 'message': 'Source deleted successfully'
169
+ }), 200
170
+ else:
171
+ return jsonify({
172
+ 'success': False,
173
+ 'message': 'Source not found or unauthorized'
174
+ }), 404
175
+
176
+ except Exception as e:
177
+ current_app.logger.error(f"Delete source error: {str(e)}")
178
+ return jsonify({
179
+ 'success': False,
180
+ 'message': 'An error occurred while deleting source'
181
+ }), 500
backend/app.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import locale
4
+ from flask import Flask
5
+ from flask_cors import CORS
6
+ from flask_jwt_extended import JWTManager
7
+ # Import for job handling
8
+ import uuid
9
+ from concurrent.futures import ThreadPoolExecutor
10
+
11
+ from config import Config
12
+ from utils.database import init_supabase
13
+ from utils.cookies import setup_secure_cookies, configure_jwt_with_cookies
14
+
15
+ # Celery imports
16
+ from celery_app import celery
17
+
18
+ def setup_unicode_environment():
19
+ """Setup Unicode environment for proper character handling."""
20
+ try:
21
+ # Set environment variables for UTF-8 support
22
+ os.environ['PYTHONIOENCODING'] = 'utf-8'
23
+ os.environ['PYTHONUTF8'] = '1'
24
+
25
+ # Set locale to UTF-8 if available
26
+ try:
27
+ locale.setlocale(locale.LC_ALL, 'C.UTF-8')
28
+ except locale.Error:
29
+ try:
30
+ locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
31
+ except locale.Error:
32
+ try:
33
+ locale.setlocale(locale.LC_ALL, '')
34
+ except locale.Error:
35
+ pass
36
+
37
+ # Set stdout/stderr encoding to UTF-8 if possible
38
+ if hasattr(sys.stdout, 'reconfigure'):
39
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
40
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
41
+
42
+ # Log to app logger instead of print
43
+ if 'app' in globals():
44
+ app.logger.info("Unicode environment setup completed")
45
+ except Exception as e:
46
+ if 'app' in globals():
47
+ app.logger.warning(f"Unicode setup failed: {str(e)}")
48
+
49
+ def create_app():
50
+ """Create and configure the Flask application."""
51
+ # Setup Unicode environment first
52
+ setup_unicode_environment()
53
+
54
+ app = Flask(__name__)
55
+ app.config.from_object(Config)
56
+
57
+ # Disable strict slashes to prevent redirects
58
+ app.url_map.strict_slashes = False
59
+
60
+ # Initialize CORS with specific configuration
61
+ CORS(app, resources={
62
+ r"/api/*": {
63
+ "origins": [
64
+ "http://localhost:3000",
65
+ "http://localhost:5000",
66
+ "http://127.0.0.1:3000",
67
+ "http://127.0.0.1:5000",
68
+ "http://192.168.1.4:3000",
69
+ "https://zelyanoth-lin.hf.space"
70
+ ],
71
+ "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
72
+ "allow_headers": ["Content-Type", "Authorization"],
73
+ "supports_credentials": True
74
+ }
75
+ })
76
+
77
+ # Setup secure cookies
78
+ app = setup_secure_cookies(app)
79
+
80
+ # Initialize JWT with cookie support
81
+ jwt = configure_jwt_with_cookies(app)
82
+
83
+ # Initialize Supabase client
84
+ app.supabase = init_supabase(app.config['SUPABASE_URL'], app.config['SUPABASE_KEY'])
85
+
86
+ # Initialize a simple in-memory job store for tracking async tasks
87
+ # In production, you'd use a database or Redis for this
88
+ app.job_store = {}
89
+
90
+ # Initialize a ThreadPoolExecutor for running background tasks
91
+ # In production, you'd use a proper task queue like Celery
92
+ app.executor = ThreadPoolExecutor(max_workers=4)
93
+
94
+ # Register blueprints
95
+ from api.auth import auth_bp
96
+ from api.sources import sources_bp
97
+ from api.accounts import accounts_bp
98
+ from api.posts import posts_bp
99
+ from api.schedules import schedules_bp
100
+
101
+ app.register_blueprint(auth_bp, url_prefix='/api/auth')
102
+ app.register_blueprint(sources_bp, url_prefix='/api/sources')
103
+ app.register_blueprint(accounts_bp, url_prefix='/api/accounts')
104
+ app.register_blueprint(posts_bp, url_prefix='/api/posts')
105
+ app.register_blueprint(schedules_bp, url_prefix='/api/schedules')
106
+
107
+ # Health check endpoint
108
+ @app.route('/health')
109
+ def health_check():
110
+ return {'status': 'healthy', 'message': 'Lin backend is running'}, 200
111
+
112
+ # Add database connection check endpoint
113
+ @app.route('/api/health')
114
+ def api_health_check():
115
+ """Enhanced health check that includes database connection."""
116
+ try:
117
+ from utils.database import check_database_connection
118
+ db_connected = check_database_connection(app.supabase)
119
+ return {
120
+ 'status': 'healthy' if db_connected else 'degraded',
121
+ 'database': 'connected' if db_connected else 'disconnected',
122
+ 'message': 'Lin backend is running' if db_connected else 'Database connection issues'
123
+ }, 200 if db_connected else 503
124
+ except Exception as e:
125
+ return {
126
+ 'status': 'unhealthy',
127
+ 'database': 'error',
128
+ 'message': f'Health check failed: {str(e)}'
129
+ }, 503
130
+
131
+ return app
132
+
133
+ if __name__ == '__main__':
134
+ app = create_app()
135
+ app.run(
136
+ host='0.0.0.0',
137
+ port=int(os.environ.get('PORT', 5000)),
138
+ debug=app.config['DEBUG']
139
+ )
backend/app.py.bak ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import locale
4
+ from flask import Flask
5
+ from flask_cors import CORS
6
+ from flask_jwt_extended import JWTManager
7
+ from apscheduler.schedulers.background import BackgroundScheduler
8
+ import atexit
9
+ # Import for job handling
10
+ import uuid
11
+ from concurrent.futures import ThreadPoolExecutor
12
+
13
+ from config import Config
14
+ from utils.database import init_supabase
15
+ from utils.cookies import setup_secure_cookies, configure_jwt_with_cookies
16
+ from scheduler.task_scheduler import init_scheduler
17
+
18
+ def setup_unicode_environment():
19
+ """Setup Unicode environment for proper character handling."""
20
+ try:
21
+ # Set environment variables for UTF-8 support
22
+ os.environ['PYTHONIOENCODING'] = 'utf-8'
23
+ os.environ['PYTHONUTF8'] = '1'
24
+
25
+ # Set locale to UTF-8 if available
26
+ try:
27
+ locale.setlocale(locale.LC_ALL, 'C.UTF-8')
28
+ except locale.Error:
29
+ try:
30
+ locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
31
+ except locale.Error:
32
+ try:
33
+ locale.setlocale(locale.LC_ALL, '')
34
+ except locale.Error:
35
+ pass
36
+
37
+ # Set stdout/stderr encoding to UTF-8 if possible
38
+ if hasattr(sys.stdout, 'reconfigure'):
39
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
40
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
41
+
42
+ # Log to app logger instead of print
43
+ if 'app' in globals():
44
+ app.logger.info("Unicode environment setup completed")
45
+ except Exception as e:
46
+ if 'app' in globals():
47
+ app.logger.warning(f"Unicode setup failed: {str(e)}")
48
+
49
+ def create_app():
50
+ """Create and configure the Flask application."""
51
+ # Setup Unicode environment first
52
+ setup_unicode_environment()
53
+
54
+ app = Flask(__name__)
55
+ app.config.from_object(Config)
56
+
57
+ # Disable strict slashes to prevent redirects
58
+ app.url_map.strict_slashes = False
59
+
60
+ # Initialize CORS with specific configuration
61
+ CORS(app, resources={
62
+ r"/api/*": {
63
+ "origins": ["http://localhost:3000", "http://localhost:5000", "http://127.0.0.1:3000", "http://127.0.0.1:5000", "http://192.168.1.4:3000"],
64
+ "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
65
+ "allow_headers": ["Content-Type", "Authorization"],
66
+ "supports_credentials": True
67
+ }
68
+ })
69
+
70
+ # Setup secure cookies
71
+ app = setup_secure_cookies(app)
72
+
73
+ # Initialize JWT with cookie support
74
+ jwt = configure_jwt_with_cookies(app)
75
+
76
+ # Initialize Supabase client
77
+ app.supabase = init_supabase(app.config['SUPABASE_URL'], app.config['SUPABASE_KEY'])
78
+
79
+ # Initialize a simple in-memory job store for tracking async tasks
80
+ # In production, you'd use a database or Redis for this
81
+ app.job_store = {}
82
+
83
+ # Initialize a ThreadPoolExecutor for running background tasks
84
+ # In production, you'd use a proper task queue like Celery
85
+ app.executor = ThreadPoolExecutor(max_workers=4)
86
+
87
+ # Initialize scheduler
88
+ if app.config['SCHEDULER_ENABLED']:
89
+ app.scheduler = BackgroundScheduler()
90
+ init_scheduler(app.scheduler, app.supabase)
91
+ app.scheduler.start()
92
+
93
+ # Shut down the scheduler when exiting the app
94
+ atexit.register(lambda: app.scheduler.shutdown())
95
+
96
+ # Register blueprints
97
+ from api.auth import auth_bp
98
+ from api.sources import sources_bp
99
+ from api.accounts import accounts_bp
100
+ from api.posts import posts_bp
101
+ from api.schedules import schedules_bp
102
+
103
+ app.register_blueprint(auth_bp, url_prefix='/api/auth')
104
+ app.register_blueprint(sources_bp, url_prefix='/api/sources')
105
+ app.register_blueprint(accounts_bp, url_prefix='/api/accounts')
106
+ app.register_blueprint(posts_bp, url_prefix='/api/posts')
107
+ app.register_blueprint(schedules_bp, url_prefix='/api/schedules')
108
+
109
+ # Health check endpoint
110
+ @app.route('/health')
111
+ def health_check():
112
+ return {'status': 'healthy', 'message': 'Lin backend is running'}, 200
113
+
114
+ # Add database connection check endpoint
115
+ @app.route('/api/health')
116
+ def api_health_check():
117
+ """Enhanced health check that includes database connection."""
118
+ try:
119
+ from utils.database import check_database_connection
120
+ db_connected = check_database_connection(app.supabase)
121
+ return {
122
+ 'status': 'healthy' if db_connected else 'degraded',
123
+ 'database': 'connected' if db_connected else 'disconnected',
124
+ 'message': 'Lin backend is running' if db_connected else 'Database connection issues'
125
+ }, 200 if db_connected else 503
126
+ except Exception as e:
127
+ return {
128
+ 'status': 'unhealthy',
129
+ 'database': 'error',
130
+ 'message': f'Health check failed: {str(e)}'
131
+ }, 503
132
+
133
+ return app
134
+
135
+ if __name__ == '__main__':
136
+ app = create_app()
137
+ app.run(
138
+ host='0.0.0.0',
139
+ port=int(os.environ.get('PORT', 5000)),
140
+ debug=app.config['DEBUG']
141
+ )
backend/celery_app.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from celery import Celery
3
+ from config import Config
4
+
5
+ def make_celery(app_name=__name__):
6
+ """Create and configure the Celery application."""
7
+ # Create Celery instance
8
+ celery = Celery(app_name)
9
+
10
+ # Configure Celery with broker and result backend from environment variables
11
+ celery.conf.broker_url = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')
12
+ celery.conf.result_backend = os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
13
+
14
+ # Additional Celery configuration
15
+ celery.conf.update(
16
+ task_serializer='json',
17
+ accept_content=['json'],
18
+ result_serializer='json',
19
+ timezone='UTC',
20
+ enable_utc=True,
21
+ task_routes={
22
+ 'celery_tasks.content_tasks.generate_content': {'queue': 'content'},
23
+ 'celery_tasks.publish_tasks.publish_post': {'queue': 'publish'},
24
+ },
25
+ worker_prefetch_multiplier=1,
26
+ task_acks_late=True,
27
+ )
28
+
29
+ return celery
30
+
31
+ # Create the Celery instance
32
+ celery = make_celery()
33
+
34
+ if __name__ == '__main__':
35
+ celery.start()
backend/celery_beat_config.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from celery import Celery
2
+ from celery.schedules import crontab
3
+ import os
4
+
5
+ # Create Celery instance for Beat scheduler
6
+ celery_beat = Celery('lin_scheduler')
7
+
8
+ # Configure Celery Beat
9
+ celery_beat.conf.broker_url = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')
10
+ celery_beat.conf.result_backend = os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
11
+
12
+ # Configure schedules
13
+ celery_beat.conf.beat_schedule = {
14
+ # This task will run every 5 minutes to load schedules from the database
15
+ 'load-schedules': {
16
+ 'task': 'load_schedules_task',
17
+ 'schedule': crontab(minute='*/5'),
18
+ },
19
+ }
20
+
21
+ celery_beat.conf.timezone = 'UTC'
backend/celery_tasks/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Initialize the celery_tasks package
backend/celery_tasks/content_tasks.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from celery import current_task
2
+ from celery_app import celery
3
+ from services.content_service import ContentService
4
+ from services.linkedin_service import LinkedInService
5
+ import logging
6
+
7
+ # Configure logging
8
+ logger = logging.getLogger(__name__)
9
+
10
+ @celery.task(bind=True)
11
+ def generate_content_task(self, user_id: str, schedule_id: str, supabase_client_config: dict):
12
+ """
13
+ Celery task to generate content for a scheduled post.
14
+
15
+ Args:
16
+ user_id (str): User ID
17
+ schedule_id (str): Schedule ID
18
+ supabase_client_config (dict): Supabase client configuration
19
+
20
+ Returns:
21
+ dict: Result of content generation
22
+ """
23
+ try:
24
+ logger.info(f"Starting content generation for schedule {schedule_id}")
25
+
26
+ # Update task state
27
+ self.update_state(state='PROGRESS', meta={'status': 'Generating content...'})
28
+
29
+ # Initialize content service
30
+ content_service = ContentService()
31
+
32
+ # Generate content using content service
33
+ generated_content = content_service.generate_post_content(user_id)
34
+
35
+ # Initialize Supabase client from config
36
+ from utils.database import init_supabase
37
+ supabase_client = init_supabase(
38
+ supabase_client_config['SUPABASE_URL'],
39
+ supabase_client_config['SUPABASE_KEY']
40
+ )
41
+
42
+ # Store generated content in database
43
+ # We need to get the social account ID from the schedule
44
+ schedule_response = (
45
+ supabase_client
46
+ .table("Scheduling")
47
+ .select("id_social")
48
+ .eq("id", schedule_id)
49
+ .execute()
50
+ )
51
+
52
+ if not schedule_response.data:
53
+ raise Exception(f"Schedule {schedule_id} not found")
54
+
55
+ social_account_id = schedule_response.data[0]['id_social']
56
+
57
+ # Store the generated content
58
+ response = (
59
+ supabase_client
60
+ .table("Post_content")
61
+ .insert({
62
+ "social_account_id": social_account_id,
63
+ "Text_content": generated_content,
64
+ "is_published": False,
65
+ "sched": schedule_id
66
+ })
67
+ .execute()
68
+ )
69
+
70
+ if response.data:
71
+ logger.info(f"Content generated and stored for schedule {schedule_id}")
72
+ return {
73
+ 'status': 'success',
74
+ 'message': f'Content generated for schedule {schedule_id}',
75
+ 'post_id': response.data[0]['id']
76
+ }
77
+ else:
78
+ logger.error(f"Failed to store generated content for schedule {schedule_id}")
79
+ return {
80
+ 'status': 'failure',
81
+ 'message': f'Failed to store generated content for schedule {schedule_id}'
82
+ }
83
+
84
+ except Exception as e:
85
+ logger.error(f"Error in content generation task for schedule {schedule_id}: {str(e)}")
86
+ return {
87
+ 'status': 'failure',
88
+ 'message': f'Error in content generation: {str(e)}'
89
+ }
90
+
91
+ @celery.task(bind=True)
92
+ def publish_post_task(self, schedule_id: str, supabase_client_config: dict):
93
+ """
94
+ Celery task to publish a scheduled post.
95
+
96
+ Args:
97
+ schedule_id (str): Schedule ID
98
+ supabase_client_config (dict): Supabase client configuration
99
+
100
+ Returns:
101
+ dict: Result of post publishing
102
+ """
103
+ try:
104
+ logger.info(f"Starting post publishing for schedule {schedule_id}")
105
+
106
+ # Update task state
107
+ self.update_state(state='PROGRESS', meta={'status': 'Publishing post...'})
108
+
109
+ # Initialize Supabase client from config
110
+ from utils.database import init_supabase
111
+ supabase_client = init_supabase(
112
+ supabase_client_config['SUPABASE_URL'],
113
+ supabase_client_config['SUPABASE_KEY']
114
+ )
115
+
116
+ # Fetch the post to publish
117
+ response = (
118
+ supabase_client
119
+ .table("Post_content")
120
+ .select("*")
121
+ .eq("sched", schedule_id)
122
+ .eq("is_published", False)
123
+ .order("created_at", desc=True)
124
+ .limit(1)
125
+ .execute()
126
+ )
127
+
128
+ if not response.data:
129
+ logger.info(f"No unpublished posts found for schedule {schedule_id}")
130
+ return {
131
+ 'status': 'info',
132
+ 'message': f'No unpublished posts found for schedule {schedule_id}'
133
+ }
134
+
135
+ post = response.data[0]
136
+ post_id = post.get('id')
137
+ text_content = post.get('Text_content')
138
+ image_url = post.get('image_content_url')
139
+
140
+ # Get social network credentials
141
+ schedule_response = (
142
+ supabase_client
143
+ .table("Scheduling")
144
+ .select("Social_network(token, sub)")
145
+ .eq("id", schedule_id)
146
+ .execute()
147
+ )
148
+
149
+ if not schedule_response.data:
150
+ raise Exception(f"Schedule {schedule_id} not found")
151
+
152
+ social_network = schedule_response.data[0].get('Social_network', {})
153
+ access_token = social_network.get('token')
154
+ user_sub = social_network.get('sub')
155
+
156
+ if not access_token or not user_sub:
157
+ logger.error(f"Missing social network credentials for schedule {schedule_id}")
158
+ return {
159
+ 'status': 'failure',
160
+ 'message': f'Missing social network credentials for schedule {schedule_id}'
161
+ }
162
+
163
+ # Publish to LinkedIn
164
+ linkedin_service = LinkedInService()
165
+ publish_response = linkedin_service.publish_post(
166
+ access_token, user_sub, text_content, image_url
167
+ )
168
+
169
+ # Update post status in database
170
+ update_response = (
171
+ supabase_client
172
+ .table("Post_content")
173
+ .update({"is_published": True})
174
+ .eq("id", post_id)
175
+ .execute()
176
+ )
177
+
178
+ logger.info(f"Post published successfully for schedule {schedule_id}")
179
+ return {
180
+ 'status': 'success',
181
+ 'message': f'Post published successfully for schedule {schedule_id}',
182
+ 'linkedin_response': publish_response
183
+ }
184
+
185
+ except Exception as e:
186
+ logger.error(f"Error in publishing task for schedule {schedule_id}: {str(e)}")
187
+ return {
188
+ 'status': 'failure',
189
+ 'message': f'Error in publishing post: {str(e)}'
190
+ }
backend/celery_tasks/schedule_loader.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from celery import current_app
2
+ from celery.schedules import crontab
3
+ from datetime import datetime
4
+ import logging
5
+ from utils.database import init_supabase
6
+ from config import Config
7
+ from celery_tasks.scheduler import schedule_content_generation, schedule_post_publishing
8
+
9
+ # Configure logging
10
+ logger = logging.getLogger(__name__)
11
+
12
+ def get_supabase_config():
13
+ """Get Supabase configuration from environment."""
14
+ return {
15
+ 'SUPABASE_URL': Config.SUPABASE_URL,
16
+ 'SUPABASE_KEY': Config.SUPABASE_KEY
17
+ }
18
+
19
+ def parse_schedule_time(schedule_time):
20
+ """
21
+ Parse schedule time string into crontab format.
22
+
23
+ Args:
24
+ schedule_time (str): Schedule time in format "Day HH:MM"
25
+
26
+ Returns:
27
+ dict: Crontab parameters
28
+ """
29
+ try:
30
+ day_name, time_str = schedule_time.split()
31
+ hour, minute = map(int, time_str.split(':'))
32
+
33
+ # Map day names to crontab format
34
+ day_map = {
35
+ 'Monday': 1,
36
+ 'Tuesday': 2,
37
+ 'Wednesday': 3,
38
+ 'Thursday': 4,
39
+ 'Friday': 5,
40
+ 'Saturday': 6,
41
+ 'Sunday': 0
42
+ }
43
+
44
+ day_of_week = day_map.get(day_name, '*')
45
+
46
+ return {
47
+ 'minute': minute,
48
+ 'hour': hour,
49
+ 'day_of_week': day_of_week
50
+ }
51
+ except Exception as e:
52
+ logger.error(f"Error parsing schedule time {schedule_time}: {str(e)}")
53
+ # Default to every minute for error cases
54
+ return {
55
+ 'minute': '*',
56
+ 'hour': '*',
57
+ 'day_of_week': '*'
58
+ }
59
+
60
+ def load_schedules_task():
61
+ """
62
+ Celery task to load schedules from the database and create periodic tasks.
63
+ This task runs every 5 minutes to check for new or updated schedules.
64
+ """
65
+ try:
66
+ logger.info("Loading schedules from database...")
67
+
68
+ # Get Supabase configuration
69
+ supabase_config = get_supabase_config()
70
+
71
+ # Initialize Supabase client
72
+ supabase_client = init_supabase(
73
+ supabase_config['SUPABASE_URL'],
74
+ supabase_config['SUPABASE_KEY']
75
+ )
76
+
77
+ # Fetch all schedules from Supabase
78
+ response = (
79
+ supabase_client
80
+ .table("Scheduling")
81
+ .select("*, Social_network(id_utilisateur, token, sub)")
82
+ .execute()
83
+ )
84
+
85
+ schedules = response.data if response.data else []
86
+ logger.info(f"Found {len(schedules)} schedules")
87
+
88
+ # Get current beat schedule
89
+ current_schedule = current_app.conf.beat_schedule
90
+
91
+ # Remove existing scheduled jobs (except the loader job)
92
+ # In a production environment, you might want to be more selective about this
93
+ loader_job = current_schedule.get('load-schedules', {})
94
+ new_schedule = {'load-schedules': loader_job}
95
+
96
+ # Create jobs for each schedule
97
+ for schedule in schedules:
98
+ try:
99
+ schedule_id = schedule.get('id')
100
+ schedule_time = schedule.get('schedule_time')
101
+ adjusted_time = schedule.get('adjusted_time')
102
+
103
+ if not schedule_time or not adjusted_time:
104
+ logger.warning(f"Invalid schedule format for schedule {schedule_id}")
105
+ continue
106
+
107
+ # Parse schedule times
108
+ content_gen_time = parse_schedule_time(adjusted_time)
109
+ publish_time = parse_schedule_time(schedule_time)
110
+
111
+ # Create content generation job (5 minutes before publishing)
112
+ gen_job_id = f"gen_{schedule_id}"
113
+ new_schedule[gen_job_id] = {
114
+ 'task': 'celery_tasks.content_tasks.generate_content_task',
115
+ 'schedule': crontab(
116
+ minute=content_gen_time['minute'],
117
+ hour=content_gen_time['hour'],
118
+ day_of_week=content_gen_time['day_of_week']
119
+ ),
120
+ 'args': (
121
+ schedule.get('Social_network', {}).get('id_utilisateur'),
122
+ schedule_id,
123
+ supabase_config
124
+ )
125
+ }
126
+ logger.info(f"Created content generation job: {gen_job_id}")
127
+
128
+ # Create publishing job
129
+ pub_job_id = f"pub_{schedule_id}"
130
+ new_schedule[pub_job_id] = {
131
+ 'task': 'celery_tasks.content_tasks.publish_post_task',
132
+ 'schedule': crontab(
133
+ minute=publish_time['minute'],
134
+ hour=publish_time['hour'],
135
+ day_of_week=publish_time['day_of_week']
136
+ ),
137
+ 'args': (
138
+ schedule_id,
139
+ supabase_config
140
+ )
141
+ }
142
+ logger.info(f"Created publishing job: {pub_job_id}")
143
+
144
+ except Exception as e:
145
+ logger.error(f"Error creating jobs for schedule {schedule.get('id')}: {str(e)}")
146
+
147
+ # Update the beat schedule
148
+ current_app.conf.beat_schedule = new_schedule
149
+ logger.info("Updated Celery Beat schedule")
150
+
151
+ return {
152
+ 'status': 'success',
153
+ 'message': f'Loaded {len(schedules)} schedules',
154
+ 'schedules_count': len(schedules)
155
+ }
156
+
157
+ except Exception as e:
158
+ logger.error(f"Error loading schedules: {str(e)}")
159
+ return {
160
+ 'status': 'error',
161
+ 'message': f'Error loading schedules: {str(e)}'
162
+ }
backend/celery_tasks/scheduler.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timedelta
2
+ from celery import chain
3
+ import logging
4
+ from celery_app import celery
5
+ from celery_tasks.content_tasks import generate_content_task, publish_post_task
6
+
7
+ # Configure logging
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
11
+ def init_celery_scheduler(supabase_client):
12
+ """
13
+ Initialize the Celery-based task scheduler.
14
+
15
+ Args:
16
+ supabase_client: Supabase client instance
17
+ """
18
+ logger.info("Initializing Celery scheduler")
19
+ # In a Celery-based approach, we don't need to initialize a scheduler here
20
+ # Tasks will be scheduled through Celery Beat or called directly
21
+
22
+ def schedule_content_generation(schedule: dict, supabase_client_config: dict):
23
+ """
24
+ Schedule content generation task using Celery.
25
+
26
+ Args:
27
+ schedule (dict): Schedule data
28
+ supabase_client_config (dict): Supabase client configuration
29
+
30
+ Returns:
31
+ dict: Celery task result
32
+ """
33
+ schedule_id = schedule.get('id')
34
+ user_id = schedule.get('Social_network', {}).get('id_utilisateur')
35
+
36
+ if not user_id:
37
+ logger.warning(f"No user ID found for schedule {schedule_id}")
38
+ return None
39
+
40
+ logger.info(f"Scheduling content generation for schedule {schedule_id}")
41
+
42
+ # Schedule the content generation task
43
+ task = generate_content_task.delay(user_id, schedule_id, supabase_client_config)
44
+ return {
45
+ 'task_id': task.id,
46
+ 'status': 'scheduled',
47
+ 'message': f'Content generation scheduled for schedule {schedule_id}'
48
+ }
49
+
50
+ def schedule_post_publishing(schedule: dict, supabase_client_config: dict):
51
+ """
52
+ Schedule post publishing task using Celery.
53
+
54
+ Args:
55
+ schedule (dict): Schedule data
56
+ supabase_client_config (dict): Supabase client configuration
57
+
58
+ Returns:
59
+ dict: Celery task result
60
+ """
61
+ schedule_id = schedule.get('id')
62
+ logger.info(f"Scheduling post publishing for schedule {schedule_id}")
63
+
64
+ # Schedule the post publishing task
65
+ task = publish_post_task.delay(schedule_id, supabase_client_config)
66
+ return {
67
+ 'task_id': task.id,
68
+ 'status': 'scheduled',
69
+ 'message': f'Post publishing scheduled for schedule {schedule_id}'
70
+ }
71
+
72
+ def schedule_content_and_publish(schedule: dict, supabase_client_config: dict):
73
+ """
74
+ Schedule both content generation and post publishing as a chain.
75
+
76
+ Args:
77
+ schedule (dict): Schedule data
78
+ supabase_client_config (dict): Supabase client configuration
79
+
80
+ Returns:
81
+ dict: Celery task result
82
+ """
83
+ schedule_id = schedule.get('id')
84
+ user_id = schedule.get('Social_network', {}).get('id_utilisateur')
85
+
86
+ if not user_id:
87
+ logger.warning(f"No user ID found for schedule {schedule_id}")
88
+ return None
89
+
90
+ logger.info(f"Scheduling content generation and publishing chain for schedule {schedule_id}")
91
+
92
+ # Create a chain of tasks: generate content first, then publish
93
+ task_chain = chain(
94
+ generate_content_task.s(user_id, schedule_id, supabase_client_config),
95
+ publish_post_task.s(supabase_client_config)
96
+ )
97
+
98
+ # Apply the chain asynchronously
99
+ result = task_chain.apply_async()
100
+
101
+ return {
102
+ 'task_id': result.id,
103
+ 'status': 'scheduled',
104
+ 'message': f'Content generation and publishing chain scheduled for schedule {schedule_id}'
105
+ }
backend/config.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import platform
3
+ from dotenv import load_dotenv
4
+
5
+ # Load environment variables from .env file
6
+ load_dotenv()
7
+
8
+ def get_system_encoding():
9
+ """Get the system's preferred encoding with UTF-8 fallback."""
10
+ try:
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
18
+ if platform.system() == 'Windows':
19
+ try:
20
+ os.environ['PYTHONIOENCODING'] = 'utf-8'
21
+ preferred_encoding = 'utf-8'
22
+ except:
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
backend/models/__init__.py ADDED
File without changes
backend/models/post.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+ from datetime import datetime
4
+
5
+ @dataclass
6
+ class Post:
7
+ """Post model representing a social media post."""
8
+ id: str
9
+ social_account_id: str
10
+ Text_content: str
11
+ is_published: bool = True
12
+ sched: Optional[str] = None
13
+ image_content_url: Optional[str] = None
14
+ created_at: Optional[datetime] = None
15
+ scheduled_at: Optional[datetime] = None
16
+
17
+ @classmethod
18
+ def from_dict(cls, data: dict):
19
+ """Create a Post instance from a dictionary."""
20
+ return cls(
21
+ id=data['id'],
22
+ social_account_id=data['social_account_id'],
23
+ Text_content=data['Text_content'],
24
+ is_published=data.get('is_published', False),
25
+ sched=data.get('sched'),
26
+ image_content_url=data.get('image_content_url'),
27
+ created_at=datetime.fromisoformat(data['created_at'].replace('Z', '+00:00')) if data.get('created_at') else None,
28
+ scheduled_at=datetime.fromisoformat(data['scheduled_at'].replace('Z', '+00:00')) if data.get('scheduled_at') else None
29
+ )
30
+
31
+ def to_dict(self):
32
+ """Convert Post instance to dictionary."""
33
+ return {
34
+ 'id': self.id,
35
+ 'social_account_id': self.social_account_id,
36
+ 'Text_content': self.Text_content,
37
+ 'is_published': self.is_published,
38
+ 'sched': self.sched,
39
+ 'image_content_url': self.image_content_url,
40
+ 'created_at': self.created_at.isoformat() if self.created_at else None,
41
+ 'scheduled_at': self.scheduled_at.isoformat() if self.scheduled_at else None
42
+ }
backend/models/schedule.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+ from datetime import datetime
4
+
5
+ @dataclass
6
+ class Schedule:
7
+ """Schedule model representing a post scheduling configuration."""
8
+ id: str
9
+ social_account_id: str
10
+ schedule_time: str
11
+ adjusted_time: str
12
+ created_at: Optional[datetime] = None
13
+
14
+ @classmethod
15
+ def from_dict(cls, data: dict):
16
+ """Create a Schedule instance from a dictionary."""
17
+ return cls(
18
+ id=data['id'],
19
+ social_account_id=data['social_account_id'],
20
+ schedule_time=data['schedule_time'],
21
+ adjusted_time=data['adjusted_time'],
22
+ created_at=datetime.fromisoformat(data['created_at'].replace('Z', '+00:00')) if data.get('created_at') else None
23
+ )
24
+
25
+ def to_dict(self):
26
+ """Convert Schedule instance to dictionary."""
27
+ return {
28
+ 'id': self.id,
29
+ 'social_account_id': self.social_account_id,
30
+ 'schedule_time': self.schedule_time,
31
+ 'adjusted_time': self.adjusted_time,
32
+ 'created_at': self.created_at.isoformat() if self.created_at else None
33
+ }
backend/models/social_account.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+ from datetime import datetime
4
+
5
+ @dataclass
6
+ class SocialAccount:
7
+ """Social account model representing a social media account."""
8
+ id: str
9
+ user_id: str
10
+ social_network: str
11
+ account_name: str
12
+ token: Optional[str] = None
13
+ sub: Optional[str] = None
14
+ given_name: Optional[str] = None
15
+ family_name: Optional[str] = None
16
+ picture: Optional[str] = None
17
+ created_at: Optional[datetime] = None
18
+
19
+ @classmethod
20
+ def from_dict(cls, data: dict):
21
+ """Create a SocialAccount instance from a dictionary."""
22
+ return cls(
23
+ id=data['id'],
24
+ user_id=data['user_id'],
25
+ social_network=data['social_network'],
26
+ account_name=data['account_name'],
27
+ token=data.get('token'),
28
+ sub=data.get('sub'),
29
+ given_name=data.get('given_name'),
30
+ family_name=data.get('family_name'),
31
+ picture=data.get('picture'),
32
+ created_at=datetime.fromisoformat(data['created_at'].replace('Z', '+00:00')) if data.get('created_at') else None
33
+ )
34
+
35
+ def to_dict(self):
36
+ """Convert SocialAccount instance to dictionary."""
37
+ return {
38
+ 'id': self.id,
39
+ 'user_id': self.user_id,
40
+ 'social_network': self.social_network,
41
+ 'account_name': self.account_name,
42
+ 'token': self.token,
43
+ 'sub': self.sub,
44
+ 'given_name': self.given_name,
45
+ 'family_name': self.family_name,
46
+ 'picture': self.picture,
47
+ 'created_at': self.created_at.isoformat() if self.created_at else None
48
+ }
backend/models/source.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+ from datetime import datetime
4
+
5
+ @dataclass
6
+ class Source:
7
+ """Source model representing an RSS source."""
8
+ id: str
9
+ user_id: str
10
+ source: str
11
+ category: Optional[str] = None
12
+ last_update: Optional[datetime] = None
13
+ created_at: Optional[datetime] = None
14
+
15
+ @classmethod
16
+ def from_dict(cls, data: dict):
17
+ """Create a Source instance from a dictionary."""
18
+ return cls(
19
+ id=data['id'],
20
+ user_id=data['user_id'],
21
+ source=data['source'],
22
+ category=data.get('category'),
23
+ last_update=datetime.fromisoformat(data['last_update'].replace('Z', '+00:00')) if data.get('last_update') else None,
24
+ created_at=datetime.fromisoformat(data['created_at'].replace('Z', '+00:00')) if data.get('created_at') else None
25
+ )
26
+
27
+ def to_dict(self):
28
+ """Convert Source instance to dictionary."""
29
+ return {
30
+ 'id': self.id,
31
+ 'user_id': self.user_id,
32
+ 'source': self.source,
33
+ 'category': self.category,
34
+ 'last_update': self.last_update.isoformat() if self.last_update else None,
35
+ 'created_at': self.created_at.isoformat() if self.created_at else None
36
+ }
backend/models/user.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+ from datetime import datetime
4
+
5
+ @dataclass
6
+ class User:
7
+ """User model representing a user in the system."""
8
+ id: str
9
+ email: str
10
+ created_at: datetime
11
+ email_confirmed_at: Optional[datetime] = None
12
+
13
+ @classmethod
14
+ def from_dict(cls, data: dict):
15
+ """Create a User instance from a dictionary."""
16
+ return cls(
17
+ id=data['id'],
18
+ email=data['email'],
19
+ created_at=data['created_at'] if data.get('created_at') else None,
20
+ email_confirmed_at=data['email_confirmed_at'] if data.get('email_confirmed_at') else None
21
+ )
22
+
23
+ def to_dict(self):
24
+ """Convert User instance to dictionary."""
25
+ return {
26
+ 'id': self.id,
27
+ 'email': self.email,
28
+ 'created_at': self.created_at if self.created_at else None,
29
+ 'email_confirmed_at': self.email_confirmed_at if self.email_confirmed_at else None
30
+ }
backend/requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Flask>=2.3.2
2
+ Flask-CORS>=4.0.0
3
+ Flask-JWT-Extended>=4.5.2
4
+ Flask-SQLAlchemy>=3.0.5
5
+ Flask-Migrate>=4.0.4
6
+ python-dotenv>=1.0.0
7
+ requests>=2.31.0
8
+ requests-oauthlib>=1.3.1
9
+ apscheduler>=3.10.1
10
+ pandas>=2.0.3
11
+ gradio-client>=0.5.0
12
+ supabase>=2.4.0
13
+ bcrypt>=4.0.1
14
+ pytest>=7.4.0
15
+ pytest-cov>=4.1.0
16
+ celery>=5.3.0
17
+ redis>=4.5.0
backend/scheduler/__init__.py ADDED
File without changes
backend/scheduler/task_scheduler.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from datetime import datetime, timedelta
3
+ from celery import current_app
4
+ from celery.schedules import crontab
5
+ from services.content_service import ContentService
6
+ from services.linkedin_service import LinkedInService
7
+ from config import Config
8
+
9
+ # Configure logging
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+ def get_supabase_config():
14
+ """Get Supabase configuration from environment."""
15
+ return {
16
+ 'SUPABASE_URL': Config.SUPABASE_URL,
17
+ 'SUPABASE_KEY': Config.SUPABASE_KEY
18
+ }
19
+
20
+ def init_scheduler(supabase_client):
21
+ """
22
+ Initialize the Celery-based task scheduler.
23
+
24
+ Args:
25
+ supabase_client: Supabase client instance
26
+ """
27
+ logger.info("Initializing Celery scheduler")
28
+ # In a Celery-based approach, we don't need to initialize a scheduler here
29
+ # Tasks will be scheduled through Celery Beat or called directly
30
+
31
+ def parse_schedule_time(schedule_time):
32
+ """
33
+ Parse schedule time string into crontab format.
34
+
35
+ Args:
36
+ schedule_time (str): Schedule time in format "Day HH:MM"
37
+
38
+ Returns:
39
+ dict: Crontab parameters
40
+ """
41
+ try:
42
+ day_name, time_str = schedule_time.split()
43
+ hour, minute = map(int, time_str.split(':'))
44
+
45
+ # Map day names to crontab format
46
+ day_map = {
47
+ 'Monday': 1,
48
+ 'Tuesday': 2,
49
+ 'Wednesday': 3,
50
+ 'Thursday': 4,
51
+ 'Friday': 5,
52
+ 'Saturday': 6,
53
+ 'Sunday': 0
54
+ }
55
+
56
+ day_of_week = day_map.get(day_name, '*')
57
+
58
+ return {
59
+ 'minute': minute,
60
+ 'hour': hour,
61
+ 'day_of_week': day_of_week
62
+ }
63
+ except Exception as e:
64
+ logger.error(f"Error parsing schedule time {schedule_time}: {str(e)}")
65
+ # Default to every minute for error cases
66
+ return {
67
+ 'minute': '*',
68
+ 'hour': '*',
69
+ 'day_of_week': '*'
70
+ }
71
+
72
+ def load_schedules(supabase_client):
73
+ """
74
+ Load schedules from the database and create periodic tasks.
75
+ This function is called by the Celery Beat scheduler.
76
+
77
+ Args:
78
+ supabase_client: Supabase client instance
79
+ """
80
+ try:
81
+ logger.info("Loading schedules from database...")
82
+
83
+ # Get Supabase configuration
84
+ supabase_config = get_supabase_config()
85
+
86
+ # Fetch all schedules from Supabase
87
+ response = (
88
+ supabase_client
89
+ .table("Scheduling")
90
+ .select("*, Social_network(id_utilisateur, token, sub)")
91
+ .execute()
92
+ )
93
+
94
+ schedules = response.data if response.data else []
95
+ logger.info(f"Found {len(schedules)} schedules")
96
+
97
+ # Get current beat schedule
98
+ current_schedule = current_app.conf.beat_schedule
99
+
100
+ # Remove existing scheduled jobs (except the loader job)
101
+ # In a production environment, you might want to be more selective about this
102
+ loader_job = current_schedule.get('load-schedules', {})
103
+ new_schedule = {'load-schedules': loader_job}
104
+
105
+ # Create jobs for each schedule
106
+ for schedule in schedules:
107
+ try:
108
+ schedule_id = schedule.get('id')
109
+ schedule_time = schedule.get('schedule_time')
110
+ adjusted_time = schedule.get('adjusted_time')
111
+
112
+ if not schedule_time or not adjusted_time:
113
+ logger.warning(f"Invalid schedule format for schedule {schedule_id}")
114
+ continue
115
+
116
+ # Parse schedule times
117
+ content_gen_time = parse_schedule_time(adjusted_time)
118
+ publish_time = parse_schedule_time(schedule_time)
119
+
120
+ # Create content generation job (5 minutes before publishing)
121
+ gen_job_id = f"gen_{schedule_id}"
122
+ new_schedule[gen_job_id] = {
123
+ 'task': 'celery_tasks.content_tasks.generate_content_task',
124
+ 'schedule': crontab(
125
+ minute=content_gen_time['minute'],
126
+ hour=content_gen_time['hour'],
127
+ day_of_week=content_gen_time['day_of_week']
128
+ ),
129
+ 'args': (
130
+ schedule.get('Social_network', {}).get('id_utilisateur'),
131
+ schedule_id,
132
+ supabase_config
133
+ )
134
+ }
135
+ logger.info(f"Created content generation job: {gen_job_id}")
136
+
137
+ # Create publishing job
138
+ pub_job_id = f"pub_{schedule_id}"
139
+ new_schedule[pub_job_id] = {
140
+ 'task': 'celery_tasks.content_tasks.publish_post_task',
141
+ 'schedule': crontab(
142
+ minute=publish_time['minute'],
143
+ hour=publish_time['hour'],
144
+ day_of_week=publish_time['day_of_week']
145
+ ),
146
+ 'args': (
147
+ schedule_id,
148
+ supabase_config
149
+ )
150
+ }
151
+ logger.info(f"Created publishing job: {pub_job_id}")
152
+
153
+ except Exception as e:
154
+ logger.error(f"Error creating jobs for schedule {schedule.get('id')}: {str(e)}")
155
+
156
+ # Update the beat schedule
157
+ current_app.conf.beat_schedule = new_schedule
158
+ logger.info("Updated Celery Beat schedule")
159
+
160
+ except Exception as e:
161
+ logger.error(f"Error loading schedules: {str(e)}")
162
+
163
+ def generate_content_job(schedule: dict, supabase_client):
164
+ """
165
+ Job to generate content for a scheduled post.
166
+ This function is kept for backward compatibility but should be replaced with Celery tasks.
167
+
168
+ Args:
169
+ schedule (dict): Schedule data
170
+ supabase_client: Supabase client instance
171
+ """
172
+ try:
173
+ schedule_id = schedule.get('id')
174
+ user_id = schedule.get('Social_network', {}).get('id_utilisateur')
175
+
176
+ if not user_id:
177
+ logger.warning(f"No user ID found for schedule {schedule_id}")
178
+ return
179
+
180
+ logger.info(f"Generating content for schedule {schedule_id}")
181
+
182
+ # Generate content using content service
183
+ content_service = ContentService()
184
+ generated_content = content_service.generate_post_content(user_id)
185
+
186
+ # Store generated content in database
187
+ social_account_id = schedule.get('id_social')
188
+
189
+ response = (
190
+ supabase_client
191
+ .table("Post_content")
192
+ .insert({
193
+ "social_account_id": social_account_id,
194
+ "Text_content": generated_content,
195
+ "is_published": False,
196
+ "sched": schedule_id
197
+ })
198
+ .execute()
199
+ )
200
+
201
+ if response.data:
202
+ logger.info(f"Content generated and stored for schedule {schedule_id}")
203
+ else:
204
+ logger.error(f"Failed to store generated content for schedule {schedule_id}")
205
+
206
+ except Exception as e:
207
+ logger.error(f"Error in content generation job for schedule {schedule.get('id')}: {str(e)}")
208
+
209
+ def publish_post_job(schedule: dict, supabase_client):
210
+ """
211
+ Job to publish a scheduled post.
212
+ This function is kept for backward compatibility but should be replaced with Celery tasks.
213
+
214
+ Args:
215
+ schedule (dict): Schedule data
216
+ supabase_client: Supabase client instance
217
+ """
218
+ try:
219
+ schedule_id = schedule.get('id')
220
+ logger.info(f"Publishing post for schedule {schedule_id}")
221
+
222
+ # Fetch the post to publish
223
+ response = (
224
+ supabase_client
225
+ .table("Post_content")
226
+ .select("*")
227
+ .eq("sched", schedule_id)
228
+ .eq("is_published", False)
229
+ .order("created_at", desc=True)
230
+ .limit(1)
231
+ .execute()
232
+ )
233
+
234
+ if not response.data:
235
+ logger.info(f"No unpublished posts found for schedule {schedule_id}")
236
+ return
237
+
238
+ post = response.data[0]
239
+ post_id = post.get('id')
240
+ text_content = post.get('Text_content')
241
+ image_url = post.get('image_content_url')
242
+
243
+ # Get social network credentials
244
+ access_token = schedule.get('Social_network', {}).get('token')
245
+ user_sub = schedule.get('Social_network', {}).get('sub')
246
+
247
+ if not access_token or not user_sub:
248
+ logger.error(f"Missing social network credentials for schedule {schedule_id}")
249
+ return
250
+
251
+ # Publish to LinkedIn
252
+ linkedin_service = LinkedInService()
253
+ publish_response = linkedin_service.publish_post(
254
+ access_token, user_sub, text_content, image_url
255
+ )
256
+
257
+ # Update post status in database
258
+ update_response = (
259
+ supabase_client
260
+ .table("Post_content")
261
+ .update({"is_published": True})
262
+ .eq("id", post_id)
263
+ .execute()
264
+ )
265
+
266
+ logger.info(f"Post published successfully for schedule {schedule_id}")
267
+
268
+ except Exception as e:
269
+ logger.error(f"Error in publishing job for schedule {schedule.get('id')}: {str(e)}")
backend/scheduler/task_scheduler.py.bak ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from apscheduler.schedulers.background import BackgroundScheduler
2
+ from apscheduler.triggers.cron import CronTrigger
3
+ from datetime import datetime, timedelta
4
+ import logging
5
+ from services.content_service import ContentService
6
+ from services.linkedin_service import LinkedInService
7
+
8
+ # Configure logging
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger(__name__)
11
+
12
+ def init_scheduler(scheduler: BackgroundScheduler, supabase_client):
13
+ """
14
+ Initialize the task scheduler with jobs.
15
+
16
+ Args:
17
+ scheduler (BackgroundScheduler): The scheduler instance
18
+ supabase_client: Supabase client instance
19
+ """
20
+ # Add a job to load schedules from database
21
+ scheduler.add_job(
22
+ func=load_schedules,
23
+ trigger=CronTrigger(minute='*/5'), # Run every 5 minutes
24
+ id='load_schedules',
25
+ name='Load schedules from database',
26
+ args=[scheduler, supabase_client]
27
+ )
28
+
29
+ # Load initial schedules
30
+ load_schedules(scheduler, supabase_client)
31
+
32
+ def load_schedules(scheduler: BackgroundScheduler, supabase_client):
33
+ """
34
+ Load schedules from the database and create jobs.
35
+
36
+ Args:
37
+ scheduler (BackgroundScheduler): The scheduler instance
38
+ supabase_client: Supabase client instance
39
+ """
40
+ try:
41
+ logger.info("Loading schedules from database...")
42
+
43
+ # Fetch all schedules from Supabase
44
+ response = (
45
+ supabase_client
46
+ .table("Scheduling")
47
+ .select("*, Social_network(id_utilisateur, token, sub)")
48
+ .execute()
49
+ )
50
+
51
+ schedules = response.data if response.data else []
52
+ logger.info(f"Found {len(schedules)} schedules")
53
+
54
+ # Remove existing scheduled jobs (except the loader job)
55
+ job_ids = [job.id for job in scheduler.get_jobs() if job.id != 'load_schedules']
56
+ for job_id in job_ids:
57
+ scheduler.remove_job(job_id)
58
+ logger.info(f"Removed job: {job_id}")
59
+
60
+ # Create jobs for each schedule
61
+ for schedule in schedules:
62
+ try:
63
+ create_scheduling_jobs(scheduler, schedule, supabase_client)
64
+ except Exception as e:
65
+ logger.error(f"Error creating jobs for schedule {schedule.get('id')}: {str(e)}")
66
+
67
+ except Exception as e:
68
+ logger.error(f"Error loading schedules: {str(e)}")
69
+
70
+ def create_scheduling_jobs(scheduler: BackgroundScheduler, schedule: dict, supabase_client):
71
+ """
72
+ Create jobs for a specific schedule.
73
+
74
+ Args:
75
+ scheduler (BackgroundScheduler): The scheduler instance
76
+ schedule (dict): Schedule data
77
+ supabase_client: Supabase client instance
78
+ """
79
+ schedule_id = schedule.get('id')
80
+ schedule_time = schedule.get('schedule_time')
81
+ adjusted_time = schedule.get('adjusted_time')
82
+
83
+ if not schedule_time or not adjusted_time:
84
+ logger.warning(f"Invalid schedule format for schedule {schedule_id}")
85
+ return
86
+
87
+ # Parse schedule times
88
+ try:
89
+ # Parse main schedule time (publishing)
90
+ day_name, time_str = schedule_time.split()
91
+ hour, minute = map(int, time_str.split(':'))
92
+
93
+ # Parse adjusted time (content generation)
94
+ adj_day_name, adj_time_str = adjusted_time.split()
95
+ adj_hour, adj_minute = map(int, adj_time_str.split(':'))
96
+
97
+ # Map day names to cron format
98
+ day_map = {
99
+ 'Monday': 'mon',
100
+ 'Tuesday': 'tue',
101
+ 'Wednesday': 'wed',
102
+ 'Thursday': 'thu',
103
+ 'Friday': 'fri',
104
+ 'Saturday': 'sat',
105
+ 'Sunday': 'sun'
106
+ }
107
+
108
+ if day_name not in day_map or adj_day_name not in day_map:
109
+ logger.warning(f"Invalid day name in schedule {schedule_id}")
110
+ return
111
+
112
+ day_cron = day_map[day_name]
113
+ adj_day_cron = day_map[adj_day_name]
114
+
115
+ # Create content generation job (5 minutes before publishing)
116
+ gen_job_id = f"gen_{schedule_id}"
117
+ scheduler.add_job(
118
+ func=generate_content_job,
119
+ trigger=CronTrigger(
120
+ day_of_week=adj_day_cron,
121
+ hour=adj_hour,
122
+ minute=adj_minute
123
+ ),
124
+ id=gen_job_id,
125
+ name=f"Generate content for schedule {schedule_id}",
126
+ args=[schedule, supabase_client]
127
+ )
128
+ logger.info(f"Created content generation job: {gen_job_id}")
129
+
130
+ # Create publishing job
131
+ pub_job_id = f"pub_{schedule_id}"
132
+ scheduler.add_job(
133
+ func=publish_post_job,
134
+ trigger=CronTrigger(
135
+ day_of_week=day_cron,
136
+ hour=hour,
137
+ minute=minute
138
+ ),
139
+ id=pub_job_id,
140
+ name=f"Publish post for schedule {schedule_id}",
141
+ args=[schedule, supabase_client]
142
+ )
143
+ logger.info(f"Created publishing job: {pub_job_id}")
144
+
145
+ except Exception as e:
146
+ logger.error(f"Error creating jobs for schedule {schedule_id}: {str(e)}")
147
+
148
+ def generate_content_job(schedule: dict, supabase_client):
149
+ """
150
+ Job to generate content for a scheduled post.
151
+
152
+ Args:
153
+ schedule (dict): Schedule data
154
+ supabase_client: Supabase client instance
155
+ """
156
+ try:
157
+ schedule_id = schedule.get('id')
158
+ user_id = schedule.get('Social_network', {}).get('id_utilisateur')
159
+
160
+ if not user_id:
161
+ logger.warning(f"No user ID found for schedule {schedule_id}")
162
+ return
163
+
164
+ logger.info(f"Generating content for schedule {schedule_id}")
165
+
166
+ # Generate content using content service
167
+ content_service = ContentService()
168
+ generated_content = content_service.generate_post_content(user_id)
169
+
170
+ # Store generated content in database
171
+ social_account_id = schedule.get('id_social')
172
+
173
+ response = (
174
+ supabase_client
175
+ .table("Post_content")
176
+ .insert({
177
+ "social_account_id": social_account_id,
178
+ "Text_content": generated_content,
179
+ "is_published": False,
180
+ "sched": schedule_id
181
+ })
182
+ .execute()
183
+ )
184
+
185
+ if response.data:
186
+ logger.info(f"Content generated and stored for schedule {schedule_id}")
187
+ else:
188
+ logger.error(f"Failed to store generated content for schedule {schedule_id}")
189
+
190
+ except Exception as e:
191
+ logger.error(f"Error in content generation job for schedule {schedule.get('id')}: {str(e)}")
192
+
193
+ def publish_post_job(schedule: dict, supabase_client):
194
+ """
195
+ Job to publish a scheduled post.
196
+
197
+ Args:
198
+ schedule (dict): Schedule data
199
+ supabase_client: Supabase client instance
200
+ """
201
+ try:
202
+ schedule_id = schedule.get('id')
203
+ logger.info(f"Publishing post for schedule {schedule_id}")
204
+
205
+ # Fetch the post to publish
206
+ response = (
207
+ supabase_client
208
+ .table("Post_content")
209
+ .select("*")
210
+ .eq("sched", schedule_id)
211
+ .eq("is_published", False)
212
+ .order("created_at", desc=True)
213
+ .limit(1)
214
+ .execute()
215
+ )
216
+
217
+ if not response.data:
218
+ logger.info(f"No unpublished posts found for schedule {schedule_id}")
219
+ return
220
+
221
+ post = response.data[0]
222
+ post_id = post.get('id')
223
+ text_content = post.get('Text_content')
224
+ image_url = post.get('image_content_url')
225
+
226
+ # Get social network credentials
227
+ access_token = schedule.get('Social_network', {}).get('token')
228
+ user_sub = schedule.get('Social_network', {}).get('sub')
229
+
230
+ if not access_token or not user_sub:
231
+ logger.error(f"Missing social network credentials for schedule {schedule_id}")
232
+ return
233
+
234
+ # Publish to LinkedIn
235
+ linkedin_service = LinkedInService()
236
+ publish_response = linkedin_service.publish_post(
237
+ access_token, user_sub, text_content, image_url
238
+ )
239
+
240
+ # Update post status in database
241
+ update_response = (
242
+ supabase_client
243
+ .table("Post_content")
244
+ .update({"is_published": True})
245
+ .eq("id", post_id)
246
+ .execute()
247
+ )
248
+
249
+ logger.info(f"Post published successfully for schedule {schedule_id}")
250
+
251
+ except Exception as e:
252
+ logger.error(f"Error in publishing job for schedule {schedule.get('id')}: {str(e)}")
backend/services/__init__.py ADDED
File without changes
backend/services/auth_service.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import current_app, request
2
+ from flask_jwt_extended import create_access_token, get_jwt
3
+ import bcrypt
4
+ from datetime import datetime, timedelta
5
+ from models.user import User
6
+ from utils.database import authenticate_user, create_user
7
+
8
+ def register_user(email: str, password: str) -> dict:
9
+ """
10
+ Register a new user.
11
+
12
+ Args:
13
+ email (str): User email
14
+ password (str): User password
15
+
16
+ Returns:
17
+ dict: Registration result with user data or error message
18
+ """
19
+ try:
20
+ # Check if user already exists
21
+ # In Supabase, we'll try to create the user directly
22
+ response = create_user(current_app.supabase, email, password)
23
+
24
+ if response.user:
25
+ user = User.from_dict({
26
+ 'id': response.user.id,
27
+ 'email': response.user.email,
28
+ 'created_at': response.user.created_at
29
+ })
30
+
31
+ return {
32
+ 'success': True,
33
+ 'message': 'User registered successfully. Please check your email for confirmation.',
34
+ 'user': user.to_dict()
35
+ }
36
+ else:
37
+ return {
38
+ 'success': False,
39
+ 'message': 'Failed to register user'
40
+ }
41
+ except Exception as e:
42
+ # Check if it's a duplicate user error
43
+ if 'already registered' in str(e).lower():
44
+ return {
45
+ 'success': False,
46
+ 'message': 'User with this email already exists'
47
+ }
48
+ else:
49
+ return {
50
+ 'success': False,
51
+ 'message': f'Registration failed: {str(e)}'
52
+ }
53
+
54
+ def login_user(email: str, password: str, remember_me: bool = False) -> dict:
55
+ """
56
+ Authenticate and login a user.
57
+
58
+ Args:
59
+ email (str): User email
60
+ password (str): User password
61
+ remember_me (bool): Remember me flag for extended session
62
+
63
+ Returns:
64
+ dict: Login result with token and user data or error message
65
+ """
66
+ try:
67
+ # Authenticate user with Supabase
68
+ response = authenticate_user(current_app.supabase, email, password)
69
+
70
+ if response.user:
71
+ # Check if email is confirmed
72
+ if not response.user.email_confirmed_at:
73
+ return {
74
+ 'success': False,
75
+ 'message': 'Please confirm your email before logging in'
76
+ }
77
+
78
+ # Set token expiration based on remember me flag
79
+ if remember_me:
80
+ # Extended token expiration (7 days)
81
+ expires_delta = timedelta(days=7)
82
+ token_type = "remember"
83
+ else:
84
+ # Standard token expiration (1 hour)
85
+ expires_delta = timedelta(hours=1)
86
+ token_type = "session"
87
+
88
+ # Create JWT token with proper expiration and claims
89
+ access_token = create_access_token(
90
+ identity=response.user.id,
91
+ additional_claims={
92
+ 'email': response.user.email,
93
+ 'email_confirmed_at': response.user.email_confirmed_at.isoformat() if response.user.email_confirmed_at else None,
94
+ 'remember_me': remember_me,
95
+ 'token_type': token_type
96
+ },
97
+ expires_delta=expires_delta
98
+ )
99
+
100
+ user = User.from_dict({
101
+ 'id': response.user.id,
102
+ 'email': response.user.email,
103
+ 'created_at': response.user.created_at,
104
+ 'email_confirmed_at': response.user.email_confirmed_at
105
+ })
106
+
107
+ return {
108
+ 'success': True,
109
+ 'token': access_token,
110
+ 'user': user.to_dict(),
111
+ 'rememberMe': remember_me,
112
+ 'expiresAt': (datetime.now() + expires_delta).isoformat(),
113
+ 'tokenType': token_type
114
+ }
115
+ else:
116
+ return {
117
+ 'success': False,
118
+ 'message': 'Invalid email or password'
119
+ }
120
+ except Exception as e:
121
+ current_app.logger.error(f"Login error: {str(e)}")
122
+ return {
123
+ 'success': False,
124
+ 'message': f'Login failed: {str(e)}'
125
+ }
126
+
127
+ def get_user_by_id(user_id: str) -> dict:
128
+ """
129
+ Get user by ID.
130
+
131
+ Args:
132
+ user_id (str): User ID
133
+
134
+ Returns:
135
+ dict: User data or None if not found
136
+ """
137
+ try:
138
+ # Get user from Supabase Auth
139
+ response = current_app.supabase.auth.get_user(user_id)
140
+
141
+ if response.user:
142
+ user = User.from_dict({
143
+ 'id': response.user.id,
144
+ 'email': response.user.email,
145
+ 'created_at': response.user.created_at,
146
+ 'email_confirmed_at': response.user.email_confirmed_at
147
+ })
148
+ return user.to_dict()
149
+ else:
150
+ return None
151
+ except Exception:
152
+ return None
backend/services/content_service.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import json
3
+ import unicodedata
4
+ from flask import current_app
5
+ from gradio_client import Client
6
+ import pandas as pd
7
+
8
+ class ContentService:
9
+ """Service for AI content generation using Hugging Face models."""
10
+
11
+ def __init__(self, hugging_key=None):
12
+ # Use provided key or fall back to app config
13
+ self.hugging_key = hugging_key or current_app.config.get('HUGGING_KEY')
14
+ # Initialize the Gradio client for content generation
15
+ self.client = Client("Zelyanoth/Linkedin_poster_dev", hf_token=self.hugging_key)
16
+
17
+ def validate_unicode_content(self, content):
18
+ """Validate Unicode content while preserving original formatting and spaces."""
19
+ if not content or not isinstance(content, str):
20
+ return content
21
+
22
+ try:
23
+ # Test if content can be encoded as UTF-8
24
+ content.encode('utf-8')
25
+ return content # Return original content if it's valid UTF-8
26
+ except UnicodeEncodeError:
27
+ try:
28
+ # If encoding fails, try to preserve as much as possible
29
+ return content.encode('utf-8', errors='replace').decode('utf-8')
30
+ except:
31
+ # Ultimate fallback
32
+ return str(content)
33
+
34
+ def preserve_formatting(self, content):
35
+ """Preserve spaces, line breaks, and paragraph formatting."""
36
+ if not content:
37
+ return content
38
+
39
+ # Preserve all whitespace characters including spaces, tabs, and newlines
40
+ # This ensures that paragraph breaks and indentation are maintained
41
+ try:
42
+ # Test encoding first
43
+ content.encode('utf-8')
44
+ return content
45
+ except UnicodeEncodeError:
46
+ # Fallback with error replacement but preserve whitespace
47
+ return content.encode('utf-8', errors='replace').decode('utf-8')
48
+
49
+ def sanitize_content_for_api(self, content):
50
+ """Sanitize content for API calls while preserving original text, spaces, and formatting."""
51
+ if not content:
52
+ return content
53
+
54
+ # First preserve formatting and spaces
55
+ preserved = self.preserve_formatting(content)
56
+
57
+ # Only validate Unicode, don't remove spaces or formatting
58
+ validated = self.validate_unicode_content(preserved)
59
+
60
+ # Only remove null bytes that might cause issues in API calls
61
+ if '\x00' in validated:
62
+ validated = validated.replace('\x00', '')
63
+
64
+ # Ensure line breaks and spaces are preserved
65
+ validated = validated.replace('\r\n', '\n').replace('\r', '\n')
66
+
67
+ return validated
68
+
69
+ def generate_post_content(self, user_id: str) -> str:
70
+ """
71
+ Generate post content using AI.
72
+
73
+ Args:
74
+ user_id (str): User ID for personalization
75
+
76
+ Returns:
77
+ str: Generated post content
78
+ """
79
+ try:
80
+ # Call the Hugging Face model to generate content
81
+ result = self.client.predict(
82
+ code=user_id,
83
+ api_name="/poster_linkedin"
84
+ )
85
+
86
+ # Parse the result (assuming it returns a list with content as first element)
87
+ # First try to parse as JSON
88
+ try:
89
+ parsed_result = json.loads(result)
90
+ except json.JSONDecodeError:
91
+ # If JSON parsing fails, check if it's already a Python list/object
92
+ try:
93
+ # Try to evaluate as Python literal (safe for lists/dicts)
94
+ import ast
95
+ parsed_result = ast.literal_eval(result)
96
+ except (ValueError, SyntaxError):
97
+ # If that fails, treat the result as a plain string
98
+ parsed_result = [result]
99
+
100
+ # Extract the first element if it's a list
101
+ if isinstance(parsed_result, list):
102
+ generated_content = parsed_result[0] if parsed_result and parsed_result[0] is not None else "Generated content will appear here..."
103
+ else:
104
+ generated_content = str(parsed_result) if parsed_result is not None else "Generated content will appear here..."
105
+
106
+ # Validate, sanitize, and preserve formatting of the generated content
107
+ sanitized_content = self.sanitize_content_for_api(generated_content)
108
+
109
+ # Ensure paragraph breaks and formatting are preserved
110
+ final_content = self.preserve_formatting(sanitized_content)
111
+
112
+ return final_content
113
+
114
+ except Exception as e:
115
+ error_message = str(e)
116
+ current_app.logger.error(f"Content generation failed: {error_message}")
117
+ raise Exception(f"Content generation failed: {error_message}")
118
+
119
+ def add_rss_source(self, rss_link: str, user_id: str) -> str:
120
+ """
121
+ Add an RSS source for content generation.
122
+
123
+ Args:
124
+ rss_link (str): RSS feed URL
125
+ user_id (str): User ID
126
+
127
+ Returns:
128
+ str: Result message
129
+ """
130
+ try:
131
+ # Call the Hugging Face model to add RSS source
132
+ rss_input = f"{rss_link}__thi_irrh'èçs_my_id__! {user_id}"
133
+ sanitized_rss_input = self.sanitize_content_for_api(rss_input)
134
+
135
+ result = self.client.predict(
136
+ rss_link=sanitized_rss_input,
137
+ api_name="/ajouter_rss"
138
+ )
139
+
140
+ # Sanitize and preserve formatting of the result
141
+ sanitized_result = self.sanitize_content_for_api(result)
142
+ return self.preserve_formatting(sanitized_result)
143
+
144
+ except Exception as e:
145
+ raise Exception(f"Failed to add RSS source: {str(e)}")
backend/services/linkedin_service.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import current_app
2
+ import requests
3
+ from requests_oauthlib import OAuth2Session
4
+ from urllib.parse import urlencode
5
+
6
+ class LinkedInService:
7
+ """Service for LinkedIn API integration."""
8
+
9
+ def __init__(self):
10
+ self.client_id = current_app.config['CLIENT_ID']
11
+ self.client_secret = current_app.config['CLIENT_SECRET']
12
+ self.redirect_uri = current_app.config['REDIRECT_URL']
13
+ self.scope = ['openid', 'profile', 'email', 'w_member_social']
14
+
15
+ def get_authorization_url(self, state: str) -> str:
16
+ """
17
+ Get LinkedIn authorization URL.
18
+
19
+ Args:
20
+ state (str): State parameter for security
21
+
22
+ Returns:
23
+ str: Authorization URL
24
+ """
25
+ linkedin = OAuth2Session(
26
+ self.client_id,
27
+ redirect_uri=self.redirect_uri,
28
+ scope=self.scope,
29
+ state=state
30
+ )
31
+
32
+ authorization_url, _ = linkedin.authorization_url(
33
+ 'https://www.linkedin.com/oauth/v2/authorization'
34
+ )
35
+
36
+ return authorization_url
37
+
38
+ def get_access_token(self, code: str) -> dict:
39
+ """
40
+ Exchange authorization code for access token.
41
+
42
+ Args:
43
+ code (str): Authorization code
44
+
45
+ Returns:
46
+ dict: Token response
47
+ """
48
+ url = "https://www.linkedin.com/oauth/v2/accessToken"
49
+ headers = {
50
+ "Content-Type": "application/x-www-form-urlencoded"
51
+ }
52
+ data = {
53
+ "grant_type": "authorization_code",
54
+ "code": code,
55
+ "redirect_uri": self.redirect_uri,
56
+ "client_id": self.client_id,
57
+ "client_secret": self.client_secret
58
+ }
59
+
60
+ response = requests.post(url, headers=headers, data=data)
61
+ response.raise_for_status()
62
+ return response.json()
63
+
64
+ def get_user_info(self, access_token: str) -> dict:
65
+ """
66
+ Get user information from LinkedIn.
67
+
68
+ Args:
69
+ access_token (str): LinkedIn access token
70
+
71
+ Returns:
72
+ dict: User information
73
+ """
74
+ url = "https://api.linkedin.com/v2/userinfo"
75
+ headers = {
76
+ "Authorization": f"Bearer {access_token}"
77
+ }
78
+
79
+ response = requests.get(url, headers=headers)
80
+ response.raise_for_status()
81
+ return response.json()
82
+
83
+ def publish_post(self, access_token: str, user_id: str, text_content: str, image_url: str = None) -> dict:
84
+ """
85
+ Publish a post to LinkedIn.
86
+
87
+ Args:
88
+ access_token (str): LinkedIn access token
89
+ user_id (str): LinkedIn user ID
90
+ text_content (str): Post content
91
+ image_url (str, optional): Image URL
92
+
93
+ Returns:
94
+ dict: Publish response
95
+ """
96
+ url = "https://api.linkedin.com/v2/ugcPosts"
97
+ headers = {
98
+ "Authorization": f"Bearer {access_token}",
99
+ "X-Restli-Protocol-Version": "2.0.0",
100
+ "Content-Type": "application/json"
101
+ }
102
+
103
+ if image_url:
104
+ # Handle image upload
105
+ register_body = {
106
+ "registerUploadRequest": {
107
+ "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"],
108
+ "owner": f"urn:li:person:{user_id}",
109
+ "serviceRelationships": [{
110
+ "relationshipType": "OWNER",
111
+ "identifier": "urn:li:userGeneratedContent"
112
+ }]
113
+ }
114
+ }
115
+
116
+ r = requests.post(
117
+ "https://api.linkedin.com/v2/assets?action=registerUpload",
118
+ headers=headers,
119
+ json=register_body
120
+ )
121
+
122
+ if r.status_code not in (200, 201):
123
+ raise Exception(f"Failed to register upload: {r.status_code} {r.text}")
124
+
125
+ datar = r.json()["value"]
126
+ upload_url = datar["uploadMechanism"]["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"]["uploadUrl"]
127
+ asset_urn = datar["asset"]
128
+
129
+ # Upload image
130
+ upload_headers = {
131
+ "Authorization": f"Bearer {access_token}",
132
+ "X-Restli-Protocol-Version": "2.0.0",
133
+ "Content-Type": "application/octet-stream"
134
+ }
135
+
136
+ # Download image and upload to LinkedIn
137
+ image_response = requests.get(image_url)
138
+ if image_response.status_code == 200:
139
+ upload_response = requests.put(upload_url, headers=upload_headers, data=image_response.content)
140
+ if upload_response.status_code not in (200, 201):
141
+ raise Exception(f"Failed to upload image: {upload_response.status_code} {upload_response.text}")
142
+
143
+ # Create post with image
144
+ post_body = {
145
+ "author": f"urn:li:person:{user_id}",
146
+ "lifecycleState": "PUBLISHED",
147
+ "specificContent": {
148
+ "com.linkedin.ugc.ShareContent": {
149
+ "shareCommentary": {"text": text_content},
150
+ "shareMediaCategory": "IMAGE",
151
+ "media": [{
152
+ "status": "READY",
153
+ "media": asset_urn,
154
+ "description": {"text": "Post image"},
155
+ "title": {"text": "Post image"}
156
+ }]
157
+ }
158
+ },
159
+ "visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"}
160
+ }
161
+ else:
162
+ # Create text-only post
163
+ post_body = {
164
+ "author": f"urn:li:person:{user_id}",
165
+ "lifecycleState": "PUBLISHED",
166
+ "specificContent": {
167
+ "com.linkedin.ugc.ShareContent": {
168
+ "shareCommentary": {
169
+ "text": text_content
170
+ },
171
+ "shareMediaCategory": "NONE"
172
+ }
173
+ },
174
+ "visibility": {
175
+ "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
176
+ }
177
+ }
178
+
179
+ response = requests.post(url, headers=headers, json=post_body)
180
+ response.raise_for_status()
181
+ return response.json()