fff
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env.hf +9 -0
- .gitignore +168 -0
- Dockerfile +31 -0
- FINAL_DOCUMENTATION.md +692 -0
- GEMINI.md +160 -0
- HEADER_CSS_ANALYSIS.md +128 -0
- HEADER_FIX_SUMMARY.md +68 -0
- HUGGING_FACE_DEPLOYMENT.md +197 -0
- IMPLEMENTATION_SUMMARY.md +215 -0
- LINKEDIN_AUTH_GUIDE.md +258 -0
- REACT_DEVELOPMENT_GUIDE.md +568 -0
- README.md +307 -6
- SETUP_GUIDE.md +628 -0
- UI_COMPONENT_SNAPSHOT.md +208 -0
- api_design.md +348 -0
- app.py +26 -0
- architecture_summary.md +111 -0
- backend/.env.example +27 -0
- backend/Dockerfile +40 -0
- backend/README.md +275 -0
- backend/TASK_SCHEDULING_EVOLUTION.md +124 -0
- backend/api/__init__.py +0 -0
- backend/api/accounts.py +311 -0
- backend/api/auth.py +186 -0
- backend/api/posts.py +574 -0
- backend/api/schedules.py +233 -0
- backend/api/sources.py +181 -0
- backend/app.py +139 -0
- backend/app.py.bak +141 -0
- backend/celery_app.py +35 -0
- backend/celery_beat_config.py +21 -0
- backend/celery_tasks/__init__.py +1 -0
- backend/celery_tasks/content_tasks.py +190 -0
- backend/celery_tasks/schedule_loader.py +162 -0
- backend/celery_tasks/scheduler.py +105 -0
- backend/config.py +78 -0
- backend/models/__init__.py +0 -0
- backend/models/post.py +42 -0
- backend/models/schedule.py +33 -0
- backend/models/social_account.py +48 -0
- backend/models/source.py +36 -0
- backend/models/user.py +30 -0
- backend/requirements.txt +17 -0
- backend/scheduler/__init__.py +0 -0
- backend/scheduler/task_scheduler.py +269 -0
- backend/scheduler/task_scheduler.py.bak +252 -0
- backend/services/__init__.py +0 -0
- backend/services/auth_service.py +152 -0
- backend/services/content_service.py +145 -0
- 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 |
-
|
|
|
8 |
---
|
9 |
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|