Merge pull request #16 from uja-dev-practices/feature/backend-v5
Feature/backend v5
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
|
||||
# --- PYTHON BACKEND ---
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
# ORCID SWORD System
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<strong>Full-stack platform for ORCID authentication, researcher synchronization, and publication export in SWORD XML / ZIP formats.</strong>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## . Overview: What is this project meant
|
||||
|
||||
`orcid-system` is designed for research workflows where ORCID data must be ingested, normalized, and exported.
|
||||
|
||||
Core capabilities:
|
||||
|
||||
- ORCID OAuth 3-legged login
|
||||
- researcher search and synchronization against ORCID
|
||||
- publication export by selection or by researcher
|
||||
- export formats: **SWORD XML** and **ZIP**
|
||||
- dual access model: user JWT or service API key (for export)
|
||||
|
||||
> [!NOTE]
|
||||
> The stack is local-first with Docker, but includes production-oriented hardening (CORS policy, trusted hosts, security headers, rate limiting, etc.).
|
||||
|
||||
---
|
||||
|
||||
## . Tech Stack
|
||||
|
||||
### Backend
|
||||
- FastAPI
|
||||
- SQLAlchemy
|
||||
- PostgreSQL
|
||||
- Redis
|
||||
- python-jose (JWT)
|
||||
- slowapi (rate limit)
|
||||
- APScheduler
|
||||
- httpx
|
||||
|
||||
### Frontend
|
||||
- React 19
|
||||
- Vite 8
|
||||
- React Router
|
||||
- TailwindCSS 4
|
||||
- Sonner (notifications)
|
||||
|
||||
### Infrastructure
|
||||
- Docker / Docker Compose
|
||||
|
||||
---
|
||||
|
||||
## . Quick Start
|
||||
|
||||
From the project root:
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Default local URLs:
|
||||
|
||||
- Frontend: `http://localhost:5173`
|
||||
- Backend: `http://localhost:8000`
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Current compose mapping uses loopback binding:
|
||||
> - `127.0.0.1:5173:5173`
|
||||
> - `127.0.0.1:8000:8000`
|
||||
> This means services are reachable from the host machine, not exposed publicly by default.
|
||||
|
||||
---
|
||||
|
||||
## . Environment Configuration
|
||||
|
||||
Backend:
|
||||
- Main file: `backend/.env`
|
||||
- Reference: `backend/.env.example`
|
||||
|
||||
Frontend:
|
||||
- Compose/dev file: `frontend/.env`
|
||||
- Optional local override for host dev: `frontend/.env.local`
|
||||
|
||||
Important backend variables:
|
||||
|
||||
- `ORCID_CLIENT_ID`
|
||||
- `ORCID_CLIENT_SECRET`
|
||||
- `ORCID_REDIRECT_URI`
|
||||
- `JWT_SECRET`
|
||||
- `API_KEY_NAME`
|
||||
- `API_KEY_VALUE`
|
||||
- `CORS_ALLOWED_ORIGINS`
|
||||
- `TRUSTED_HOSTS`
|
||||
- `DATABASE_URL`
|
||||
- `REDIS_URL`
|
||||
|
||||
Important frontend variables:
|
||||
|
||||
- `VITE_API_URL` (empty in Docker setup, so Vite proxy handles `/api`)
|
||||
- `VITE_API_PROXY_TARGET` (defaults to `http://backend:8000` in compose network)
|
||||
- `VITE_API_KEY` (must match backend `API_KEY_VALUE` when using API key mode)
|
||||
- `VITE_USE_MOCKS`
|
||||
- `VITE_ORCID_PUBLIC_API_BASE` (optional override)
|
||||
|
||||
> [!WARNING]
|
||||
> Never commit real production secrets. Rotate `JWT_SECRET`, `API_KEY_VALUE`, and `ORCID_CLIENT_SECRET` before deployment.
|
||||
|
||||
---
|
||||
|
||||
## . ngrok Bridge for Local OAuth Callback
|
||||
|
||||
To test OAuth callback from ORCID in local environments, compose can inject a public callback URL:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
ORCID_REDIRECT_URI: https://jargon-supreme-palpable.ngrok-free.dev/callback
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Values under `docker-compose.yml -> services.backend.environment` override `backend/.env` inside the container.
|
||||
|
||||
---
|
||||
|
||||
## . API Endpoints
|
||||
|
||||
Base backend URL: `http://localhost:8000`
|
||||
|
||||
| Module | Method | Endpoint | Auth | Parameters | Body | Notes |
|
||||
| :--- | :---: | :--- | :--- | :--- | :--- | ---: |
|
||||
| Health | `GET` | `/health` | None | None | None | Liveness check |
|
||||
| ORCID Auth | `GET` | `/api/auth/orcid/authorize` | None | None | None | Redirects to ORCID |
|
||||
| ORCID Auth | `GET` | `/api/auth/orcid/callback` | None | `code`, `state` | None | Exchanges OAuth code for backend JWT |
|
||||
| ORCID Auth | `GET` | `/callback` | None | `code`, `state` | None | Alias for callback flow |
|
||||
| Researchers | `POST` | `/api/researchers/search` | Optional Bearer | None | `{"orcid_ids":[...]}` | Batch search/sync |
|
||||
| Researchers | `POST` | `/api/researchers/{orcid_id}/sync` | Optional Bearer | `orcid_id` | None | Full sync of one researcher |
|
||||
| Export SWORD | `POST` | `/api/export/sword/publications` | Bearer or API key | None | `["publication_uuid", ...]` | Export selected publications |
|
||||
| Export SWORD | `GET` | `/api/export/sword/researcher/{orcid_id}` | Bearer or API key | `orcid_id` | None | Export all by researcher |
|
||||
| Export ZIP | `POST` | `/api/export/zip/publications` | Bearer or API key | None | `["publication_uuid", ...]` | Export selected publications |
|
||||
| Export ZIP | `GET` | `/api/export/zip/researcher/{orcid_id}` | Bearer or API key | `orcid_id` | None | Export all by researcher |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> `.../publications` endpoints require **publication IDs**, not `researcher.id`.
|
||||
|
||||
---
|
||||
|
||||
## . Request Examples
|
||||
|
||||
### Health
|
||||
```bash
|
||||
curl http://localhost:8000/health
|
||||
```
|
||||
|
||||
### Search one or more researchers
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/researchers/search" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"orcid_ids\":[\"0009-0000-0793-5376\"]}"
|
||||
```
|
||||
|
||||
### Sync one researcher
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/researchers/0009-0000-0793-5376/sync"
|
||||
```
|
||||
|
||||
### Export SWORD by researcher (API key mode)
|
||||
```bash
|
||||
curl "http://localhost:8000/api/export/sword/researcher/0009-0000-0793-5376" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-o sword.xml
|
||||
```
|
||||
|
||||
### Export ZIP by publication IDs (Bearer mode)
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/export/zip/publications" \
|
||||
-H "Authorization: Bearer YOUR_JWT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "[\"04f6a2a6-b753-4432-982b-b88160f627fe\"]" \
|
||||
-o export.zip
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## . Security Controls
|
||||
|
||||
Implemented controls in backend:
|
||||
|
||||
- strict CORS allowlist
|
||||
- trusted host filtering
|
||||
- request body size limit
|
||||
- OAuth `state` validation
|
||||
- JWT validation with issuer/audience claims
|
||||
- API key validation via constant-time comparison
|
||||
- rate limiting
|
||||
- security headers middleware
|
||||
- non-root container and reduced privileges
|
||||
|
||||
> [!WARNING]
|
||||
> For production, enforce HTTPS behind a reverse proxy and set concrete values for `CORS_ALLOWED_ORIGINS` and `TRUSTED_HOSTS`.
|
||||
|
||||
---
|
||||
|
||||
## . Frontend Details
|
||||
|
||||
The frontend is a React SPA using route-based navigation and a centralized API client.
|
||||
|
||||
### Frontend routing
|
||||
|
||||
- `/` → landing page
|
||||
- `/dashboard/:orcid` → researcher dashboard
|
||||
- `/group` → multi-researcher results
|
||||
- `/callback` → OAuth callback handler
|
||||
|
||||
### Frontend API behavior (`frontend/src/services/api.js`)
|
||||
|
||||
- central HTTP wrapper with `ApiError`
|
||||
- includes `X-API-Key` on requests when configured
|
||||
- includes `Authorization: Bearer <token>` when token exists in `localStorage`
|
||||
- supports mock mode through `VITE_USE_MOCKS`
|
||||
- supports Vite proxy mode when `VITE_API_URL` is empty
|
||||
|
||||
### Vite proxy (`frontend/vite.config.js`)
|
||||
|
||||
- `/api` proxied to `VITE_API_PROXY_TARGET` (`http://backend:8000` in compose)
|
||||
- `/health` proxied to same target
|
||||
- dev host settings allow tunnel scenarios (ngrok callback testing)
|
||||
|
||||
---
|
||||
|
||||
## . Project Structure
|
||||
|
||||
```text
|
||||
orcid-system/
|
||||
├── backend/
|
||||
│ ├── app/
|
||||
│ │ ├── api/
|
||||
│ │ │ ├── auth.py
|
||||
│ │ │ ├── researchers.py
|
||||
│ │ │ └── export.py
|
||||
│ │ ├── core/
|
||||
│ │ │ ├── config.py
|
||||
│ │ │ ├── rate_limit.py
|
||||
│ │ │ ├── security_headers.py
|
||||
│ │ │ ├── error_handlers.py
|
||||
│ │ │ └── body_size.py
|
||||
│ │ ├── db/
|
||||
│ │ │ ├── models.py
|
||||
│ │ │ ├── session.py
|
||||
│ │ │ └── repositories/
|
||||
│ │ ├── security/
|
||||
│ │ │ ├── jwt.py
|
||||
│ │ │ ├── api_key.py
|
||||
│ │ │ └── oauth_state.py
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── orcid_client.py
|
||||
│ │ │ ├── sword_generator.py
|
||||
│ │ │ ├── zip_generator.py
|
||||
│ │ │ └── sync_service.py
|
||||
│ │ ├── utils/
|
||||
│ │ └── main.py
|
||||
│ ├── .env
|
||||
│ ├── .env.example
|
||||
│ ├── .env.production
|
||||
│ ├── Dockerfile
|
||||
│ └── requirements.txt
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── dashboard/
|
||||
│ │ │ ├── layout/
|
||||
│ │ │ └── ui/
|
||||
│ │ ├── contexts/
|
||||
│ │ │ └── AuthContext.jsx
|
||||
│ │ ├── pages/
|
||||
│ │ │ ├── LandingPage.jsx
|
||||
│ │ │ ├── DashboardPage.jsx
|
||||
│ │ │ ├── GroupResultsPage.jsx
|
||||
│ │ │ └── AuthCallbackPage.jsx
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── api.js
|
||||
│ │ │ └── mocks.js
|
||||
│ │ ├── utils/
|
||||
│ │ ├── App.jsx
|
||||
│ │ └── main.jsx
|
||||
│ ├── .env
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.js
|
||||
│ └── eslint.config.js
|
||||
├── docker-compose.yml
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## . Production Checklist
|
||||
|
||||
- [ ] `ENVIRONMENT=production`
|
||||
- [ ] `DEBUG=false`
|
||||
- [ ] rotate all secrets
|
||||
- [ ] define strict `CORS_ALLOWED_ORIGINS`
|
||||
- [ ] define strict `TRUSTED_HOSTS`
|
||||
- [ ] enforce HTTPS via reverse proxy
|
||||
- [ ] keep DB/Redis private
|
||||
- [ ] configure monitoring and backups
|
||||
|
||||
---
|
||||
|
||||
## . Authors and Team
|
||||
|
||||
This project is the result of the collaboration with the **University of Jaén**.
|
||||
|
||||
| Role | Developer | GitHub |
|
||||
| :--- | :--- | :--- |
|
||||
| **Frontend** | Alexis López Moral | [@AlexisLopez-Dev](https://github.com/AlexisLopez-Dev) |
|
||||
| **Backend** | Mireya Cueto Garrido | [@MireyaCueto](https://github.com/MireyaCueto) |
|
||||
|
||||
### Direction
|
||||
* **Proyect Supervisor:** Luis Martínez López
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
Built with professional care and ❤️ for secure research data workflows at the University of Jaén.
|
||||
</p>
|
||||
+2
-2
@@ -5,7 +5,7 @@ services:
|
||||
container_name: orcid-backend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:8000:8000"
|
||||
- "127.0.0.1:8072:8000"
|
||||
env_file:
|
||||
- ./backend/.env
|
||||
environment:
|
||||
@@ -36,7 +36,7 @@ services:
|
||||
container_name: orcid-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:5173:5173"
|
||||
- "127.0.0.1:8073:5173"
|
||||
depends_on:
|
||||
- backend
|
||||
env_file:
|
||||
|
||||
Reference in New Issue
Block a user