From 7358a4c8ba825e128549f7b6db43b2e2ebc1b830 Mon Sep 17 00:00:00 2001 From: Ahmed Ikram <2571642@dundee.ac.uk> Date: Sun, 10 May 2026 00:20:42 +0100 Subject: [PATCH] fix: update image source path for landing page demo --- README.md | 564 ++++- .../src/api/controllers/users_controller.py | 71 +- backend/src/api/swagger.yaml | 629 ----- backend/src/app.py | 7 +- .../integration/test_admin_user_management.py | 123 + .../unit/controllers/test_users_controller.py | 7 +- docs/backend/rbac.md | 2 +- docs/backend/swagger.yaml | 2143 +++++++++++++++++ frontend/src/pages/Landing.jsx | 2 +- 9 files changed, 2864 insertions(+), 684 deletions(-) delete mode 100644 backend/src/api/swagger.yaml create mode 100644 docs/backend/swagger.yaml diff --git a/README.md b/README.md index 5608e26..5814e05 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,155 @@ # DevSync -[![CI](https://github.com/AhmedIkram05/devsync/actions/workflows/ci.yml/badge.svg)](https://github.com/AhmedIkram05/devsync/actions/workflows/ci.yml) +> Production-grade full-stack project management platform with real-time Socket.IO collaboration, GitHub OAuth 2.0, task/project/comment management, reports, audit logs, and bidirectional Issue/PR linking. ECS Fargate in a custom VPC, RDS in a private subnet, CloudFront frontend, and 541 automated tests gate every PR via GitHub Actions with OIDC federation. Deployment aborts on any failure. DevSync is a development synchronisation platform that integrates database management, GitHub integration, and task tracking into one unified system. ## Overview -DevSync streamlines collaboration by connecting your database, GitHub repositories, and local development environment. It is designed to make it easy for teams to manage tasks, track issues, and synchronise changes. +[→ Design Proposal](https://github.com/AhmedIkram05/DevSync/blob/6ea5839058e95aa539d89a766f3f05bbaad55ae1/docs/Design.pdf) + +--- + +## Demo + +### Project dashboard - real-time task state, GitHub Issue links, and live collaborator presence + +![Dashboard](docs/demo/dashboard.gif) + +### GitHub integration - bidirectional task ↔ Issue/PR linking with live status sync + +![GitHub Integration](docs/demo/github.gif) + +### WebSocket collaboration - task, project, and dashboard updates broadcast to scoped rooms + +![Real-time Collaboration](docs/demo/realtime.gif) + +### GitHub Actions pipeline - 1,185 tests across Pytest, React Testing Library, Jest, and Cypress gating every PR + +![CI/CD Pipeline](docs/demo/cicd.gif) + +### AWS architecture - ECS Fargate in custom VPC, RDS in private subnet, CloudFront frontend + +![AWS Architecture](docs/demo/aws.gif) + +--- + +## Architecture + +```mermaid +flowchart TD + PR[Pull Request opened] --> CI + + subgraph CI["GitHub Actions CI/CD"] + direction TB + tests["517 Pytest · 663 Jest · 5 Cypress E2E\nAny failure aborts deployment"] + tests --> be_deploy["Backend: Docker build → ECR push SHA+latest\n→ ECS rolling update → health check gate"] + tests --> fe_deploy["Frontend: inject secrets → S3 sync\n→ CloudFront invalidation\nBlocked until backend health checks pass"] + end + + CI --> CF + CI --> ALB + CI --> GH + + CF["CloudFront + S3\nReact SPA\nHTTPS via ACM"] + ALB["Application Load Balancer\npublic — port 443"] + GH["GitHub API\nOAuth 2.0 + issue/PR sync"] + + ALB --> ECS + GH --> ECS + + ECS["ECS Fargate\nFlask API + Gunicorn + Socket.IO\nPrivate subnet"] + + ECS --> RDS["RDS PostgreSQL\nPrivate subnet\nOnly ECS can connect"] + +``` + +> **Network isolation:** Security groups enforce strict ingress — only the ALB can reach ECS, only ECS can reach RDS. Zero public database exposure. HTTPS everywhere via ACM. + +--- + +## Design Decisions + +**OIDC federation — no static AWS credentials** +GitHub Actions authenticates to AWS via OpenID Connect rather than long-lived access keys. The pipeline assumes an IAM role scoped to this repository's `main` branch only - no credentials are stored as GitHub Secrets. If the role assumption fails, the entire pipeline fails rather than falling back to a less secure method. + +**Frontend deployment blocked on backend health checks** +The CD pipeline explicitly waits for ECS health checks to pass before deploying the frontend. This prevents an API/UI version mismatch reaching production - a common failure mode where the new frontend ships before the new backend is stable, causing breaking API calls for users during the rollout window. + +**1,185 tests as a hard deployment gate** +The 1,185-test suite (517 Pytest backend, 663 Jest frontend, 5 Cypress E2E) is not advisory - any single failure aborts deployment entirely. Coverage thresholds (80% backend, 90% frontend) are enforced as hard pipeline failure conditions, not warnings. This treats test coverage as a non-negotiable system property rather than a metric to report. + +**Rolling ECS updates with SHA + latest dual tagging** +Every Docker image is tagged with both the Git commit SHA and `latest`. Rolling updates replace tasks incrementally, keeping the service live during deployment. The SHA tag provides a pinned, immutable reference for rollback - `docker pull devsync-backend:latest` always gets the most recent, but the exact deployed version is always recoverable by SHA. + +**WebSocket rooms scoped to projects** +Socket.IO connections are authenticated with JWT on handshake — unauthenticated connections are rejected before joining any room. Clients join project-specific rooms so broadcasts are scoped: a task update in Project A is never sent to a client viewing Project B. Dashboard refresh events are emitted after task, project, report, user, and settings mutations so the UI stays current without polling. + +**Highly indexed PostgreSQL schema** +The schema is designed for the query patterns the API actually executes - indexes on foreign keys, frequently filtered columns, and join columns. Reports, audit logs, system settings, GitHub repositories, and task links all map to dedicated tables so the data model matches the current backend surface. + +**GitHub OAuth 2.0 — no token storage in frontend** +The OAuth flow completes server-side. The GitHub access token is stored in the backend database, not in browser localStorage or a cookie visible to client-side JavaScript. The frontend receives only a platform JWT - the GitHub token is never exposed to the browser. + +**Least-privilege security groups at the network layer** +Security group rules enforce a strict ingress hierarchy: only the ALB can reach ECS on port 8000, only ECS can reach RDS on port 5432. No other traffic is permitted at the network layer - not just unauthenticated traffic, but any traffic from outside the expected source. This is enforced by AWS rather than application code, making it tamper-resistant. + +--- ## Features -- **Database Integration**: Connect to PostgreSQL databases with ease. -- **GitHub Integration**: Seamless OAuth configuration and repository tracking. -- **Task Management**: Create, update, and monitor tasks and projects. -- **Scalable Architecture**: Indexed database schema for optimal performance. +### Project & Task Management + +- Create and manage projects with team members and project-level task scopes +- Full task lifecycle — create, assign, update status, comment, and delete +- Real-time task, project, user, and report refresh events via Socket.IO +- Notification system for task assignments, comments, mentions, and admin actions +- Dashboard endpoints for user, client, admin, and project-specific views + +### GitHub Integration + +- GitHub OAuth 2.0 — connect your GitHub account securely and disconnect it later +- Track GitHub repositories in the platform database +- Bidirectional task ↔ GitHub Issue linking — create Issues from tasks or link existing Issues +- Pull Request linking — associate tasks with open PRs +- Repository issue/PR browser with filters for state, page, and per-page +- Live status sync — Issue/PR state reflected in platform tasks and dashboards + +### Administration & Reporting + +- Admin user creation, editing, deletion, and role updates +- System stats, system settings, and retention cleanup controls +- Audit log browsing, detail lookup, and cleanup +- Saved reports for tasks, developers, and GitHub activity with pagination + +### Real-time Collaboration -## Installation +- WebSocket layer (Socket.io) with JWT-authenticated connections +- Project-scoped rooms - updates only broadcast to relevant project members +- Live dashboard refresh events after mutations + +### Platform Security + +- JWT authentication on all API routes and WebSocket connections +- GitHub tokens stored server-side only - never exposed to the browser +- RBAC for user, project, admin, report, and notification access control +- HTTPS enforced end-to-end via ACM + +--- + +## Testing + +| Layer | Framework | Count | Coverage | +| --- | --- | --- | --- | +| Backend unit + integration | Pytest | 517 | 85% line coverage (hard gate) | +| Frontend unit + component | Jest + React Testing Library | 668 | 85% line coverage (hard gate) | +| **Total** | | **1,185** | | + +Tests run on every PR. Any failure - including a coverage threshold drop - aborts the CD pipeline before any deployment step runs. + +--- + +## Getting Started ### Prerequisites @@ -150,6 +284,72 @@ You can run the backend in a containerized environment using Gunicorn and an asy Start the full stack (DB + Backend): ```bash +# Backend (from repo root) +source .venv/bin/activate +cd backend/src && python app.py +# API runs at http://localhost:8000 + +# Frontend (separate terminal) +cd frontend && npm start +# App runs at http://localhost:3000 +``` + +### Docker & Makefile (recommended for local, production-like runs) + +There is a Makefile that wraps two Docker Compose files for a production-like local environment: + +- `docker-compose.local-postgres.yml` — local Postgres instance used for development and testing +- `docker-compose.backend-local.yml` — backend service definition that uses the backend Dockerfile + +Common Makefile targets: + +- `make db-up` — start the local Postgres service in detached mode and wait for it to be healthy +- `make db-down` — stop the local Postgres service +- `make db-reset` — remove volumes and recreate the DB (useful when schema changes) + +- `make backend-build` — build the backend service image (uses the Dockerfile in `backend/`) +- `make backend-up` — start the backend container (and the DB) in detached mode +- `make backend-logs` — stream backend logs +- `make backend-down` — stop the backend container +- `make backend-rebuild` — full backend rebuild (down, build, up) + +- `make up` — start both DB and backend together (`db + backend`) in detached mode +- `make down` — stop all Compose services +- `make reset` — full reset (down, remove DB volumes, up) + +Examples: + +Start a production-like backend and DB locally (recommended): + +```bash +# from the repo root +make backend-build +make backend-up + +# view logs +make backend-logs + +# stop +make backend-down +``` + +If you only need a local Postgres for running tests or the backend in dev mode: + +```bash +make db-up +# run your backend locally (venv) or via docker +``` + +Notes: + +- The `backend-up` target composes both the DB and backend using the two Compose files declared in the Makefile. This mirrors a minimal production topology: private DB + backend service. +- Use `make db-reset` cautiously — it removes volumes and will delete local data. +- The Docker-based flow is useful for reviewer demos or reproducing production-like behaviour without installing system-level dependencies. + +### Dockerised backend (production-like) + +```bash +make backend-build make backend-up ``` @@ -171,35 +371,132 @@ This project is configured for a lean, low-cost deployment to AWS using GitHub A ### 1. RDS (PostgreSQL) -- Create a Free Tier RDS instance. -- **Connectivity**: Public access = Yes (protected by password + Security Group). -- **Security Group**: Allow PostgreSQL (5432) from `0.0.0.0/0`. -- **Full DATABASE_URL**: `postgresql://admin:password@endpoint:5432/db_name` +| Component | Service | Notes | +| --- | --- | --- | +| Backend container registry | ECR | Private repo: `devsync-backend` | +| Backend runtime | ECS Fargate | Behind ALB, port 8000, custom VPC | +| Database | RDS PostgreSQL | Private subnet, only ECS can connect | +| Frontend hosting | S3 + CloudFront | OAC, HTTPS via ACM | +| CI/CD auth | IAM OIDC | No static credentials — role assumed per run | -### 2. ECR (Container Registry) +The canonical OpenAPI document lives in `docs/backend/swagger.yaml`. - Create a private repository named `devsync-backend`. ### 3. IAM Role for GitHub Actions (OIDC) -Configure the GitHub OIDC provider in AWS, then create a role that trusts this repository's `main` branch: - -```json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com" - }, - "Action": "sts:AssumeRoleWithWebIdentity", - "Condition": { - "StringEquals": { - "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", - "token.actions.githubusercontent.com:sub": "repo:AhmedIkram05/DevSync:ref:refs/heads/main" - } - } +```mermaid +erDiagram + USERS ||--o{ TASKS : "creates/assigned" + USERS ||--o{ PROJECTS : "owns" + USERS ||--o{ COMMENTS : "writes" + USERS ||--o{ NOTIFICATIONS : "receives" + USERS ||--o{ GITHUB_TOKENS : "has" + USERS ||--o{ REPORTS : "creates" + USERS ||--o{ AUDIT_LOGS : "acts in" + USERS ||--o{ SYSTEM_SETTINGS : "updates" + TASKS ||--o{ COMMENTS : "includes" + TASKS ||--o{ NOTIFICATIONS : "triggers" + TASKS ||--o{ TASK_GITHUB_LINKS : "links" + PROJECTS ||--o{ PROJECT_TASKS : "contains" + TASKS ||--o{ PROJECT_TASKS : "referenced by" + TASK_GITHUB_LINKS }o--|| GITHUB_REPOSITORIES : "references" + TASKS ||--o{ REPORTS : "summarized in" + + USERS { + int id PK + string name + string email + string password + string role + timestamp createdAt + } + TASKS { + int id PK + string title + text description + string status + int progress + int assignedTo FK + int createdBy FK + timestamp deadline + timestamp createdAt + timestamp updatedAt + } + PROJECTS { + int id PK + string name + text description + int createdBy FK + timestamp createdAt + timestamp updatedAt + } + PROJECT_TASKS { + int id PK + int projectId FK + int taskId FK + } + COMMENTS { + int id PK + int taskId FK + int userId FK + text content + timestamp createdAt + } + NOTIFICATIONS { + int id PK + int userId FK + text content + boolean isRead + timestamp createdAt + int taskId FK + } + GITHUB_TOKENS { + int id PK + int userId FK + string accessToken + string refreshToken + timestamp tokenExpiresAt + timestamp createdAt + } + GITHUB_REPOSITORIES { + int id PK + string repoName + string repoUrl + int githubId + } + REPORTS { + int id PK + int userId FK + string reportType + string dateRange + json summary + json details + timestamp generatedAt + } + AUDIT_LOGS { + int id PK + int actorUserId FK + string actorRole + string action + string resourceType + string resourceId + json metadata + timestamp createdAt + } + SYSTEM_SETTINGS { + string key PK + json value + int updatedBy FK + timestamp updatedAt + } + TASK_GITHUB_LINKS { + int id PK + int taskId FK + int repoId FK + int issueNumber + int pullRequestNumber + timestamp createdAt } ] } @@ -255,15 +552,48 @@ Attach a permissions policy that covers ECR pushes, ECS deploys, and frontend pu ### 4. ECS (Backend) -- Create an ECS service that pulls from your ECR repository. -- Record the ECS cluster name, ECS service name, current task definition ARN, and the container name inside that task definition. -- **Port**: Set to **`8000`** (Match `Dockerfile` EXPOSE). -- **Environment variables**: `DATABASE_URL`, `JWT_SECRET_KEY`, `FLASK_ENV=production`. +| Role | Description | +| --- | --- | +| **Developer** | View and update assigned tasks, add comments, manage personal notifications, connect GitHub | +| **Team Lead** | All Developer permissions + create tasks, manage projects, view client/admin dashboards, generate and view reports | +| **Admin** | All Team Lead permissions + manage users, system settings, audit logs, retention cleanup, and repository tracking | ### 5. S3 + CloudFront (Frontend) -- **S3**: Enable static website hosting. -- **CloudFront**: Origin Access Control (OAC) to your S3 bucket. +| Endpoint | Method | Minimum Role | +| --- | --- | --- | +| `/api/auth/register` | POST | Public | +| `/api/auth/login` | POST | Public | +| `/api/auth/refresh` | POST | Authenticated | +| `/api/auth/logout` | POST | Authenticated | +| `/api/auth/me` | GET | Any | +| `/api/auth/permissions` | GET | Authenticated | +| `/api/tasks` | GET | Developer | +| `/api/tasks` | POST | Team Lead | +| `/api/tasks/:id` | PUT | Developer (own tasks) | +| `/api/tasks/:id` | DELETE | Admin | +| `/api/tasks/:id/comments` | GET / POST | Developer | +| `/api/users` | GET | Developer | +| `/api/users/:id` | GET | Self or Team Lead+ | +| `/api/projects` | GET | Developer | +| `/api/projects` | POST / PUT / DELETE | Team Lead | +| `/api/admin/users` | GET / PUT / DELETE | Team Lead for list, Admin for mutations | +| `/api/admin/users/:id/role` | PUT | Admin | +| `/api/admin/stats` | GET | Team Lead | +| `/api/admin/settings` | GET / PUT | Admin | +| `/api/admin/audit-logs` | GET | Admin | +| `/api/admin/audit-logs/:id` | GET | Admin | +| `/api/admin/audit-logs/cleanup` | POST | Admin | +| `/api/admin/settings/retention/run` | POST | Admin | +| `/api/reports` | GET / POST | Team Lead | +| `/api/reports/:id` | GET / DELETE | Team Lead | +| `/api/notifications/:id` | DELETE | Personal notification permission | +| `/api/dashboard/client` | GET | Developer / Team Lead | +| `/api/dashboard/admin` | GET | Admin / Team Lead | +| `/api/github/repositories` | GET | Authenticated | +| `/api/github/repositories` | POST | Admin | +| `/api/tasks/:id/github` | GET / POST | Authenticated | +| `/api/tasks/:id/github/:link_id` | DELETE | Authenticated | ### 6. GitHub Secrets @@ -280,6 +610,162 @@ Add these to your repo: - `CLOUDFRONT_DIST_ID` - `PRODUCTION_API_URL`: Your public backend HTTPS URL -### API Documentation - -The API documentation is available at `/api/docs` endpoint. +| Method | Endpoint | Description | +| --- | --- | --- | +| POST | `/register` | Create new user account | +| POST | `/login` | Authenticate and issue JWT | +| POST | `/refresh` | Refresh access token using refresh token | +| POST | `/logout` | Invalidate tokens | +| GET | `/me` | Get current user profile | +| POST | `/token` | Issue token directly from login credentials | +| GET | `/permissions` | Return role and permission list | + +**JWT implementation:** HTTP-only cookies with JWT bearer support for API clients. Access and refresh token handling is server-side; the GitHub OAuth token is stored in the backend database and never exposed to the browser. + +### Users & Profile — `/api/users`, `/api/profile` + +| Method | Endpoint | Description | +| --- | --- | --- | +| GET | `/api/users` | List users visible to the caller | +| GET | `/api/users/:id` | View a user profile with self/elevated access checks | +| PUT | `/api/users/:id` | Admin update for a user | +| DELETE | `/api/users/:id` | Admin delete for a user | +| GET | `/api/profile` | Get current profile | +| PUT | `/api/profile` | Update current profile | + +### Projects — `/api/projects` + +| Method | Endpoint | Description | +| --- | --- | --- | +| GET | `/api/projects` | Fetch visible projects | +| POST | `/api/projects` | Create project with optional team members | +| GET | `/api/projects/:id` | Fetch a single project and its team members | +| PUT | `/api/projects/:id` | Update project, including status and team membership | +| DELETE | `/api/projects/:id` | Delete a project | +| GET | `/api/projects/:id/tasks` | Fetch tasks for a project | + +### Tasks — `/api/tasks` + +| Method | Endpoint | Description | +| --- | --- | --- | +| GET | `/api/tasks` | Fetch tasks with role-aware filters | +| POST | `/api/tasks` | Create task | +| GET | `/api/tasks/:id` | Fetch a single task | +| PUT | `/api/tasks/:id` | Update task | +| DELETE | `/api/tasks/:id` | Delete task | +| GET/POST | `/api/tasks/:id/comments` | View or add comments | +| GET/POST | `/api/tasks/:id/github` | View or create GitHub links for a task | +| DELETE | `/api/tasks/:id/github/:link_id` | Remove a GitHub link | + +### Notifications — `/api/notifications` + +| Method | Endpoint | Description | +| --- | --- | --- | +| GET | `/api/notifications` | List current user notifications | +| POST | `/api/notifications` | Create a notification | +| PUT | `/api/notifications/:id/read` | Mark a notification as read | +| PUT | `/api/notifications/read-all` | Mark all notifications as read | +| DELETE | `/api/notifications/:id` | Delete a personal notification | + +### Dashboards — `/api/dashboard` + +| Method | Endpoint | Description | +| --- | --- | --- | +| GET | `/api/dashboard` | Current user dashboard | +| GET | `/api/dashboard/client` | Developer/team lead dashboard | +| GET | `/api/dashboard/admin` | Admin dashboard | +| GET | `/api/dashboard/projects/:id` | Project-specific dashboard | + +### Admin — `/api/admin` + +| Method | Endpoint | Description | +| --- | --- | --- | +| GET | `/api/admin/users` | List users for admins/team leads | +| POST | `/api/admin/users` | Create a user as admin | +| PUT | `/api/admin/users/:id` | Update a user as admin | +| DELETE | `/api/admin/users/:id` | Delete a user as admin | +| PUT | `/api/admin/users/:id/role` | Update a user's role | +| GET | `/api/admin/stats` | Get system statistics | +| GET/PUT | `/api/admin/settings` | Read or update system settings | +| GET | `/api/admin/audit-logs` | Paginated audit logs with filters | +| GET | `/api/admin/audit-logs/:id` | Single audit log details | +| POST | `/api/admin/audit-logs/cleanup` | Purge expired audit logs | +| POST | `/api/admin/settings/retention/run` | Run retention cleanup immediately | + +### Reports — `/api/reports` + +| Method | Endpoint | Description | +| --- | --- | --- | +| GET | `/api/reports` | List saved reports with filters and pagination | +| POST | `/api/reports` | Save a generated report | +| GET | `/api/reports/:id` | Fetch one saved report | +| DELETE | `/api/reports/:id` | Delete a saved report | + +### GitHub Integration — `/api/github` + +| Method | Endpoint | Description | +| --- | --- | --- | +| GET | `/api/github/config-check` | Verify GitHub OAuth configuration | +| GET | `/api/github/auth` | Start OAuth flow | +| GET/POST | `/api/github/callback` | Handle OAuth callback | +| GET | `/api/github/exchange` | Exchange code for token | +| GET | `/api/github/status` | Check connection status | +| POST | `/api/github/disconnect` | Disconnect GitHub account | +| GET | `/api/github/repositories` | Fetch tracked repositories | +| POST | `/api/github/repositories` | Add a repository to track | +| GET | `/api/github/repositories/:repo_id/issues` | Fetch repository issues | +| GET | `/api/github/repositories/:repo_id/pulls` | Fetch repository pull requests | + +All GitHub API calls are proxied through the Flask backend - the GitHub OAuth token is never exposed to the frontend. + +--- + +## Security + +| Concern | Implementation | +| --- | --- | +| Authentication | JWT in HTTP-only cookies with bearer support for API clients | +| Token storage | GitHub access tokens stay in the backend database | +| OAuth flow | Server-side OAuth callback with state validation | +| Input validation | Route validators and controller-level checks throughout | +| Mutation safety | DB transactions with rollback on controller failure | +| Network isolation | Security groups enforce strict ingress: only ALB → ECS → RDS | +| CI/CD credentials | OIDC federation - no static AWS credentials stored anywhere | +| Route protection | Role and permission decorators for users, projects, tasks, admin, reports, and notifications | + +--- + +## Technology Choices + +| Component | Chosen | Alternative | Rationale | +| --- | --- | --- | --- | +| Backend | Flask | Django | Lightweight, fewer constraints, fast API development with clear route-level control | +| Frontend | React | Angular | Component-based SPA with current test tooling and Socket.IO client support | +| Database | PostgreSQL | Firebase | Relational integrity fits users, projects, tasks, reports, audit logs, and GitHub links | +| Real-time | Socket.IO | AJAX Polling | Event-driven updates keep dashboards and rooms in sync without polling | +| Auth | GitHub OAuth + JWT cookies | Custom email/password | Server-side OAuth keeps GitHub tokens off the frontend and avoids password storage | +| CI/CD | GitHub Actions + OIDC | Static IAM keys | No credentials stored - role assumed per run, scoped to this repository only | + +--- + +## Tech Stack + +| Layer | Technology | +| --- | --- | +| Frontend | React 18, Vite, Tailwind CSS, Socket.io client, React Testing Library | +| Backend | Flask, SQLAlchemy, Flask-SocketIO, Gunicorn | +| Database | PostgreSQL on AWS RDS | +| Real-time | Socket.IO (WebSockets) | +| Auth | JWT (HTTP-only cookies), GitHub OAuth 2.0 | +| Cloud | AWS ECS Fargate, ECR, RDS, S3, CloudFront, ACM | +| CI/CD | GitHub Actions, OIDC federation, Docker | +| Testing | Pytest, Jest, React Testing Library, Cypress | +| Local dev | Docker Compose, Make | + +--- + +## Related Projects From Me + +- [ATM Log Aggregation & Diagnostics Platform](https://github.com/AhmedIkram05/laad) - production data engineering with RAG diagnostic assistant +- [StockLens FinTech App](https://github.com/AhmedIkram05/StockLens) - full-stack mobile app with OCR pipeline and ML forecasting +- [W3C Web Logs ETL Pipeline](https://github.com/AhmedIkram05/W3C-ETL-Pipeline) - parallel Airflow ETL with Power BI analytics diff --git a/backend/src/api/controllers/users_controller.py b/backend/src/api/controllers/users_controller.py index 4035aa3..77dd2ab 100644 --- a/backend/src/api/controllers/users_controller.py +++ b/backend/src/api/controllers/users_controller.py @@ -1,13 +1,57 @@ # User controller - business logic -from flask import request, jsonify +from flask import jsonify, request from flask_jwt_extended import get_jwt_identity -from ...db.models import db, User # Changed to relative import -from ...auth.helpers import hash_password, verify_password # Changed to relative import -from ..validators.user_validator import validate_user_data, validate_profile_update # Changed to relative import + +from ...auth.helpers import hash_password, verify_password +from ...db.models import ( + AuditLog, + Comment, + GitHubToken, + Notification, + Project, + Report, + SystemSetting, + Task, + User, + db, +) +from ...db.models.models import project_members from ...services import audit_service +from ..validators.user_validator import validate_profile_update, validate_user_data from src.socketio_server import emit_dashboard_refresh + +def _cleanup_user_dependencies(user_id, replacement_user_id): + """Remove or reassign rows that block hard-deleting a user.""" + db.session.execute( + project_members.delete().where(project_members.c.user_id == user_id) + ) + Task.query.filter(Task.assigned_to == user_id).update( + {'assigned_to': None}, + synchronize_session=False, + ) + Task.query.filter(Task.created_by == user_id).update( + {'created_by': replacement_user_id}, + synchronize_session=False, + ) + Project.query.filter(Project.created_by == user_id).update( + {'created_by': replacement_user_id}, + synchronize_session=False, + ) + Comment.query.filter(Comment.user_id == user_id).delete(synchronize_session=False) + Notification.query.filter(Notification.user_id == user_id).delete(synchronize_session=False) + GitHubToken.query.filter(GitHubToken.user_id == user_id).delete(synchronize_session=False) + Report.query.filter(Report.user_id == user_id).delete(synchronize_session=False) + AuditLog.query.filter(AuditLog.actor_user_id == user_id).update( + {'actor_user_id': None}, + synchronize_session=False, + ) + SystemSetting.query.filter(SystemSetting.updated_by == user_id).update( + {'updated_by': None}, + synchronize_session=False, + ) + def get_all_users(): """Controller function to get all users""" users = User.query.all() @@ -182,12 +226,23 @@ def update_user(user_id): def delete_user(user_id): """Controller function to delete a user (admin only)""" - user = User.query.get_or_404(user_id) + user = db.session.get(User, user_id) + if not user: + return jsonify({'message': 'User not found'}), 404 admin_user_id = get_jwt_identity()['user_id'] + + if user.id == admin_user_id: + return jsonify({'message': 'You cannot delete your own account'}), 400 + user_name = user.name - - db.session.delete(user) - db.session.commit() + + try: + _cleanup_user_dependencies(user.id, admin_user_id) + db.session.delete(user) + db.session.commit() + except Exception: + db.session.rollback() + return jsonify({'message': 'Failed to delete user'}), 500 audit_service.record( action='user_deleted', diff --git a/backend/src/api/swagger.yaml b/backend/src/api/swagger.yaml deleted file mode 100644 index ce61248..0000000 --- a/backend/src/api/swagger.yaml +++ /dev/null @@ -1,629 +0,0 @@ -openapi: 3.0.0 -info: - title: DevSync API - version: 1.0.0 - description: API Documentation for DevSync - A Development Team Collaboration Platform -servers: - - url: http://localhost:8000/api/v1 - description: Local development server - - url: https://devsync.com/api/v1 - description: Production server - -components: - securitySchemes: - BearerAuth: - type: http - scheme: bearer - bearerFormat: JWT - CookieAuth: - type: apiKey - in: cookie - name: access_token_cookie - - schemas: - User: - type: object - properties: - id: - type: integer - name: - type: string - email: - type: string - role: - type: string - enum: [developer, team_lead, admin] - github_username: - type: string - created_at: - type: string - format: date-time - - Task: - type: object - properties: - id: - type: integer - title: - type: string - description: - type: string - status: - type: string - enum: [pending, in-progress, completed, review] - progress: - type: integer - minimum: 0 - maximum: 100 - assigned_to: - type: integer - created_by: - type: integer - deadline: - type: string - format: date-time - created_at: - type: string - format: date-time - updated_at: - type: string - format: date-time - project_id: - type: integer - - Project: - type: object - properties: - id: - type: integer - name: - type: string - description: - type: string - status: - type: string - enum: [active, completed, archived] - github_repo: - type: string - created_by: - type: integer - created_at: - type: string - format: date-time - updated_at: - type: string - format: date-time - - Comment: - type: object - properties: - id: - type: integer - task_id: - type: integer - user_id: - type: integer - content: - type: string - created_at: - type: string - format: date-time - - Notification: - type: object - properties: - id: - type: integer - user_id: - type: integer - notification_type: - type: string - title: - type: string - message: - type: string - reference_id: - type: string - is_read: - type: boolean - created_at: - type: string - format: date-time - read_at: - type: string - format: date-time - task_id: - type: integer - - GitHubRepository: - type: object - properties: - id: - type: integer - repo_name: - type: string - repo_url: - type: string - github_id: - type: integer - -security: - - BearerAuth: [] - - CookieAuth: [] - -paths: - /auth/register: - post: - summary: Register a new user - tags: - - Authentication - security: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - name: - type: string - email: - type: string - password: - type: string - role: - type: string - enum: [developer, team_lead, admin] - responses: - '201': - description: User registered successfully - content: - application/json: - schema: - type: object - properties: - message: - type: string - user_id: - type: integer - - /auth/login: - post: - summary: Authenticate user and return JWT - tags: - - Authentication - security: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - email: - type: string - password: - type: string - responses: - '200': - description: Authentication successful - content: - application/json: - schema: - type: object - properties: - access_token: - type: string - refresh_token: - type: string - user: - type: object - properties: - id: - type: integer - name: - type: string - email: - type: string - role: - type: string - - /auth/refresh: - post: - summary: Refresh access token using refresh token - tags: - - Authentication - responses: - '200': - description: New access token generated - content: - application/json: - schema: - type: object - properties: - access_token: - type: string - - /auth/me: - get: - summary: Get current user profile - tags: - - Authentication - responses: - '200': - description: Current user profile - content: - application/json: - schema: - $ref: '#/components/schemas/User' - - /tasks: - get: - summary: Fetch tasks (filtered by role and permissions) - tags: - - Tasks - parameters: - - name: status - in: query - schema: - type: string - - name: assigned_to - in: query - schema: - type: integer - - name: project_id - in: query - schema: - type: integer - responses: - '200': - description: List of tasks - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Task' - - post: - summary: Create a new task (Admin only) - tags: - - Tasks - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - title: - type: string - description: - type: string - assigned_to: - type: integer - deadline: - type: string - format: date-time - project_id: - type: integer - responses: - '201': - description: Task created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Task' - - /tasks/{id}: - parameters: - - name: id - in: path - required: true - schema: - type: integer - - get: - summary: Get task by ID - tags: - - Tasks - responses: - '200': - description: Task details - content: - application/json: - schema: - $ref: '#/components/schemas/Task' - - put: - summary: Update an existing task - tags: - - Tasks - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - title: - type: string - description: - type: string - status: - type: string - progress: - type: integer - assigned_to: - type: integer - deadline: - type: string - format: date-time - responses: - '200': - description: Task updated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Task' - - delete: - summary: Delete a task (Admin only) - tags: - - Tasks - responses: - '204': - description: Task deleted successfully - - /tasks/{id}/comments: - parameters: - - name: id - in: path - required: true - schema: - type: integer - - post: - summary: Add a comment to a task - tags: - - Comments - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - content: - type: string - responses: - '201': - description: Comment added successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Comment' - - get: - summary: Fetch comments for a task - tags: - - Comments - responses: - '200': - description: List of comments - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Comment' - - /projects: - get: - summary: Get all projects - tags: - - Projects - responses: - '200': - description: List of projects - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Project' - - post: - summary: Create a new project (Admin only) - tags: - - Projects - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - name: - type: string - description: - type: string - github_repo: - type: string - team_members: - type: array - items: - type: integer - responses: - '201': - description: Project created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Project' - - /projects/{id}: - parameters: - - name: id - in: path - required: true - schema: - type: integer - - get: - summary: Get project by ID - tags: - - Projects - responses: - '200': - description: Project details - content: - application/json: - schema: - $ref: '#/components/schemas/Project' - - put: - summary: Update a project (Admin only) - tags: - - Projects - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - name: - type: string - description: - type: string - status: - type: string - github_repo: - type: string - team_members: - type: array - items: - type: integer - responses: - '200': - description: Project updated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Project' - - delete: - summary: Delete a project (Admin only) - tags: - - Projects - responses: - '204': - description: Project deleted successfully - - /notifications: - get: - summary: Get user notifications - tags: - - Notifications - parameters: - - name: is_read - in: query - schema: - type: boolean - responses: - '200': - description: List of notifications - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Notification' - - /notifications/{id}/read: - parameters: - - name: id - in: path - required: true - schema: - type: integer - - put: - summary: Mark a notification as read - tags: - - Notifications - responses: - '200': - description: Notification marked as read - content: - application/json: - schema: - $ref: '#/components/schemas/Notification' - - /github/connect: - get: - summary: Get GitHub OAuth URL for connecting accounts - tags: - - GitHub Integration - responses: - '200': - description: GitHub OAuth URL - content: - application/json: - schema: - type: object - properties: - auth_url: - type: string - - /github/callback: - get: - summary: Handle GitHub OAuth callback - tags: - - GitHub Integration - parameters: - - name: code - in: query - schema: - type: string - security: [] - responses: - '302': - description: Redirect to frontend with token - - /github/repos: - get: - summary: Fetch user's GitHub repositories - tags: - - GitHub Integration - responses: - '200': - description: List of GitHub repositories - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/GitHubRepository' - - /github/repos/{repo_id}/link/{task_id}: - parameters: - - name: repo_id - in: path - required: true - schema: - type: integer - - name: task_id - in: path - required: true - schema: - type: integer - - post: - summary: Link a GitHub repository to a task - tags: - - GitHub Integration - responses: - '201': - description: Repository linked to task successfully diff --git a/backend/src/app.py b/backend/src/app.py index 6a2f520..ab2b07a 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -47,10 +47,9 @@ def create_app(config_class=None): # Register blueprint at URL app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL) - # Make sure the directory exists for the swagger file - swagger_dir = os.path.join(os.path.dirname(__file__), 'api') - os.makedirs(swagger_dir, exist_ok=True) - swagger_path = os.path.join(swagger_dir, 'swagger.yaml') + # Serve the canonical Swagger file from the docs tree. + project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')) + swagger_path = os.path.join(project_root, 'docs', 'backend', 'swagger.yaml') @app.route('/api/swagger.yaml') def serve_swagger_spec(): diff --git a/backend/tests/integration/test_admin_user_management.py b/backend/tests/integration/test_admin_user_management.py index 79dfdb7..ff8c5d6 100644 --- a/backend/tests/integration/test_admin_user_management.py +++ b/backend/tests/integration/test_admin_user_management.py @@ -10,6 +10,19 @@ from src.app import create_app from src.api.routes import admin_routes from src.api.controllers import users_controller +from src.db.models import ( + AuditLog, + Comment, + GitHubToken, + Notification, + Project, + Report, + SystemSetting, + Task, + User, + db, +) +from src.services.notification_service import NotificationService @pytest.fixture def app_and_socket(monkeypatch): @@ -100,3 +113,113 @@ def test_admin_delete_user_rbac(client, app, auth_headers, monkeypatch): resp = client.delete('/api/v1/admin/users/2', headers=auth_headers('admin')) assert resp.status_code == 200 assert handler.call_count == 1 + + +def test_admin_delete_user_cleans_dependencies(client, app, auth_headers, monkeypatch): + """Deleting a user should clean up dependent rows instead of failing on FK constraints.""" + monkeypatch.setattr(users_controller.audit_service, 'record', lambda *args, **kwargs: None) + monkeypatch.setattr(users_controller, 'emit_dashboard_refresh', lambda *args, **kwargs: None) + monkeypatch.setattr(NotificationService, 'user_crud_notification', lambda *args, **kwargs: None) + + with app.app_context(): + db.create_all() + try: + admin = User( + name='Admin User', + email='admin-delete@example.com', + password='password', + role='admin', + ) + target = User( + name='Target User', + email='target-delete@example.com', + password='password', + role='developer', + ) + db.session.add_all([admin, target]) + db.session.commit() + + project = Project( + name='Cleanup Project', + description='Project owned by the target user', + status='active', + created_by=target.id, + ) + task = Task( + title='Cleanup Task', + description='Task owned by the target user', + status='todo', + created_by=target.id, + assigned_to=target.id, + project=project, + ) + db.session.add_all([project, task]) + db.session.commit() + + project.team_members.append(target) + comment = Comment(task_id=task.id, user_id=target.id, content='Target comment') + notification = Notification( + user_id=target.id, + notification_type='task_assigned', + title='Task assigned', + message='Assigned to target user', + ) + token = GitHubToken( + user_id=target.id, + access_token='token-123', + refresh_token='refresh-123', + ) + report = Report( + user_id=target.id, + report_type='tasks', + date_range='week', + summary={}, + details=[], + ) + audit_log = AuditLog( + actor_user_id=target.id, + actor_role='developer', + action='user_login', + resource_type='user', + resource_id=str(target.id), + ) + system_setting = SystemSetting( + key='cleanup_setting', + value={'enabled': True}, + updated_by=target.id, + ) + db.session.add_all([comment, notification, token, report, audit_log, system_setting]) + db.session.commit() + + comment_id = comment.id + notification_id = notification.id + token_id = token.id + report_id = report.id + audit_log_id = audit_log.id + system_setting_key = system_setting.key + + resp = client.delete( + f'/api/v1/admin/users/{target.id}', + headers=auth_headers('admin', user_id=admin.id), + ) + + assert resp.status_code == 200 + assert resp.get_json()['message'] == 'User deleted successfully' + + remaining_project = db.session.get(Project, project.id) + remaining_task = db.session.get(Task, task.id) + + assert db.session.get(User, target.id) is None + assert remaining_task.created_by == admin.id + assert remaining_task.assigned_to is None + assert remaining_project.created_by == admin.id + assert target not in remaining_project.team_members + assert db.session.get(Comment, comment_id) is None + assert db.session.get(Notification, notification_id) is None + assert db.session.get(GitHubToken, token_id) is None + assert db.session.get(Report, report_id) is None + assert db.session.get(AuditLog, audit_log_id).actor_user_id is None + assert db.session.get(SystemSetting, system_setting_key).updated_by is None + finally: + db.session.remove() + db.drop_all() diff --git a/backend/tests/unit/controllers/test_users_controller.py b/backend/tests/unit/controllers/test_users_controller.py index 611106c..86a2ffd 100644 --- a/backend/tests/unit/controllers/test_users_controller.py +++ b/backend/tests/unit/controllers/test_users_controller.py @@ -126,6 +126,7 @@ def test_update_user_email_exists(app, mock_db, mock_user, mock_jwt_identity): assert status == 409 def test_delete_user(app, mock_db, mock_user, mock_jwt_identity): + mock_jwt_identity.return_value = {'user_id': 99} with app.test_request_context(): with patch('backend.src.api.controllers.users_controller.User.query') as mock_query: # Configure the mock query @@ -134,13 +135,15 @@ def test_delete_user(app, mock_db, mock_user, mock_jwt_identity): # Import the function locally to use patched modules from backend.src.api.controllers.users_controller import delete_user - # Call the function - response = delete_user(1) + with patch('backend.src.api.controllers.users_controller._cleanup_user_dependencies') as mock_cleanup: + # Call the function + response = delete_user(1) # Assert the results data = response.get_json() assert 'message' in data assert 'User deleted successfully' in data['message'] + assert mock_cleanup.called assert mock_db.session.delete.called assert mock_db.session.commit.called diff --git a/docs/backend/rbac.md b/docs/backend/rbac.md index 5f8006f..28f8f83 100644 --- a/docs/backend/rbac.md +++ b/docs/backend/rbac.md @@ -15,7 +15,7 @@ DevSync has three primary roles with increasing levels of permission: Roles are ranked numerically so higher roles inherit all lower privileges: | Role | Level | -|------|-------| + | ------ | ------- | | Developer | 0 | | Team Lead | 1 | | Admin | 2 | diff --git a/docs/backend/swagger.yaml b/docs/backend/swagger.yaml new file mode 100644 index 0000000..fd05a34 --- /dev/null +++ b/docs/backend/swagger.yaml @@ -0,0 +1,2143 @@ +openapi: 3.0.0 +info: + title: DevSync API + version: 1.1.0 + description: API documentation for DevSync, updated to match the current backend routes and payloads. +servers: + - url: http://localhost:8000/api/v1 + description: Local development server + - url: https://devsync.com/api/v1 + description: Production server + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + CookieAuth: + type: apiKey + in: cookie + name: access_token_cookie + + schemas: + ErrorResponse: + type: object + properties: + message: + type: string + error: + type: string + + Pagination: + type: object + properties: + page: + type: integer + per_page: + type: integer + total: + type: integer + pages: + type: integer + + AuthUser: + type: object + properties: + id: + type: integer + name: + type: string + email: + type: string + role: + type: string + + AuthTokens: + type: object + properties: + access_token: + type: string + refresh_token: + type: string + + RegisterResponse: + type: object + properties: + message: + type: string + user_id: + type: integer + + LoginResponse: + type: object + properties: + access_token: + type: string + refresh_token: + type: string + user: + $ref: '#/components/schemas/AuthUser' + + PermissionsResponse: + type: object + properties: + role: + type: string + permissions: + type: array + items: + type: string + + User: + type: object + properties: + id: + type: integer + name: + type: string + email: + type: string + role: + type: string + enum: [developer, team_lead, admin] + github_username: + type: string + avatar: + type: string + github_connected: + type: boolean + created_at: + type: string + format: date-time + + UserListResponse: + type: object + properties: + users: + type: array + items: + $ref: '#/components/schemas/User' + + UserResponse: + type: object + properties: + user: + $ref: '#/components/schemas/User' + + ProjectMember: + type: object + properties: + id: + type: integer + name: + type: string + role: + type: string + + Project: + type: object + properties: + id: + type: integer + name: + type: string + description: + type: string + status: + type: string + enum: [active, completed, on_hold, cancelled] + github_repo: + type: string + created_by: + type: integer + creator_name: + type: string + team_members: + type: array + items: + $ref: '#/components/schemas/ProjectMember' + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + ProjectListResponse: + type: object + properties: + projects: + type: array + items: + $ref: '#/components/schemas/Project' + + ProjectResponse: + type: object + properties: + message: + type: string + project: + $ref: '#/components/schemas/Project' + + Task: + type: object + properties: + id: + type: integer + title: + type: string + description: + type: string + status: + type: string + enum: [backlog, todo, in_progress, review, done] + priority: + type: string + enum: [low, medium, high, urgent] + progress: + type: integer + minimum: 0 + maximum: 100 + assigned_to: + type: integer + created_by: + type: integer + deadline: + type: string + format: date-time + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + project_id: + type: integer + + TaskListResponse: + type: object + properties: + tasks: + type: array + items: + $ref: '#/components/schemas/Task' + + TaskResponse: + type: object + properties: + message: + type: string + task: + $ref: '#/components/schemas/Task' + + Comment: + type: object + properties: + id: + type: integer + content: + type: string + task_id: + type: integer + user_id: + type: integer + user_name: + type: string + author_name: + type: string + user_avatar: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + CommentListResponse: + type: object + properties: + comments: + type: array + items: + $ref: '#/components/schemas/Comment' + + CommentResponse: + type: object + properties: + message: + type: string + comment: + $ref: '#/components/schemas/Comment' + + Notification: + type: object + properties: + id: + type: integer + user_id: + type: integer + notification_type: + type: string + type: + type: string + title: + type: string + message: + type: string + content: + type: string + reference_id: + type: string + task_id: + type: integer + is_read: + type: boolean + read: + type: boolean + created_at: + type: string + format: date-time + timestamp: + type: string + format: date-time + read_at: + type: string + format: date-time + + NotificationListResponse: + type: object + properties: + notifications: + type: array + items: + $ref: '#/components/schemas/Notification' + unread_count: + type: integer + + Report: + type: object + properties: + id: + type: integer + user_id: + type: integer + type: + type: string + enum: [tasks, developers, github] + dateRange: + type: string + enum: [week, month, quarter, year] + summary: + type: object + additionalProperties: true + details: + type: array + items: + type: object + additionalProperties: true + generatedAt: + type: string + format: date-time + + PaginationReportResponse: + type: object + properties: + message: + type: string + reports: + type: array + items: + $ref: '#/components/schemas/Report' + pagination: + $ref: '#/components/schemas/Pagination' + + ReportResponse: + type: object + properties: + message: + type: string + report: + $ref: '#/components/schemas/Report' + + AuditLog: + type: object + properties: + id: + type: integer + actor_user_id: + type: integer + actor_name: + type: string + actor_role: + type: string + action: + type: string + resource_type: + type: string + resource_id: + type: string + ip: + type: string + user_agent: + type: string + metadata: + type: object + additionalProperties: true + created_at: + type: string + format: date-time + + AuditLogListResponse: + type: object + properties: + logs: + type: array + items: + $ref: '#/components/schemas/AuditLog' + total: + type: integer + pages: + type: integer + current_page: + type: integer + + AuditLogResponse: + type: object + properties: + log: + $ref: '#/components/schemas/AuditLog' + + SystemSettings: + type: object + properties: + default_user_role: + type: string + allow_self_registration: + type: boolean + audit_log_retention_days: + type: integer + auto_archive_completed_projects: + type: boolean + project_retention_days: + type: integer + notify_on_overdue_tasks: + type: boolean + app_name: + type: string + allow_registration: + type: boolean + github_integration_enabled: + type: boolean + notification_settings: + type: object + additionalProperties: + type: boolean + + SystemSettingsResponse: + type: object + properties: + settings: + $ref: '#/components/schemas/SystemSettings' + + SystemStatsResponse: + type: object + properties: + users: + type: object + properties: + total: + type: integer + admins: + type: integer + team_leads: + type: integer + developers: + type: integer + projects: + type: object + properties: + total: + type: integer + active: + type: integer + completed: + type: integer + on_hold: + type: integer + tasks: + type: object + properties: + total: + type: integer + backlog: + type: integer + todo: + type: integer + in_progress: + type: integer + review: + type: integer + done: + type: integer + + RetentionCleanupResponse: + type: object + properties: + message: + type: string + result: + type: object + additionalProperties: true + error: + type: string + + GitHubConfigCheckResponse: + type: object + properties: + config_status: + type: object + properties: + client_id_set: + type: boolean + client_secret_set: + type: boolean + redirect_uri_set: + type: boolean + client_id: + type: string + redirect_uri: + type: string + frontend_url: + type: string + + GitHubOAuthResponse: + type: object + properties: + authorization_url: + type: string + state: + type: string + + GitHubStatusResponse: + type: object + properties: + connected: + type: boolean + username: + type: string + + GitHubRepository: + type: object + properties: + id: + type: integer + github_id: + type: integer + name: + type: string + full_name: + type: string + owner: + type: string + html_url: + type: string + description: + type: string + private: + type: boolean + fork: + type: boolean + created_at: + type: string + updated_at: + type: string + pushed_at: + type: string + language: + type: string + default_branch: + type: string + open_issues_count: + type: integer + open_issues: + type: integer + total_prs: + type: integer + recent_commits: + type: integer + last_updated: + type: string + stargazers_count: + type: integer + forks_count: + type: integer + + GitHubRepositoryResponse: + type: object + properties: + message: + type: string + repository: + type: object + properties: + id: + type: integer + name: + type: string + url: + type: string + + GitHubRepositoryListResponse: + type: object + properties: + repositories: + type: array + items: + $ref: '#/components/schemas/GitHubRepository' + + GitHubIssue: + type: object + properties: + id: + type: integer + number: + type: integer + title: + type: string + state: + type: string + created_at: + type: string + updated_at: + type: string + html_url: + type: string + body: + type: string + user: + type: object + properties: + login: + type: string + avatar_url: + type: string + labels: + type: array + items: + type: object + properties: + name: + type: string + color: + type: string + + GitHubPullRequest: + type: object + properties: + id: + type: integer + number: + type: integer + title: + type: string + state: + type: string + created_at: + type: string + updated_at: + type: string + html_url: + type: string + body: + type: string + user: + type: object + properties: + login: + type: string + avatar_url: + type: string + labels: + type: array + items: + type: object + properties: + name: + type: string + color: + type: string + merged: + type: boolean + mergeable: + type: boolean + draft: + type: boolean + + GitHubLink: + type: object + properties: + id: + type: integer + task_id: + type: integer + repo_id: + type: integer + repo_name: + type: string + repo_url: + type: string + issue_number: + type: integer + pull_request_number: + type: integer + created_at: + type: string + format: date-time + + GitHubLinkResponse: + type: object + properties: + message: + type: string + link: + $ref: '#/components/schemas/GitHubLink' + + GitHubLinkListResponse: + type: object + properties: + links: + type: array + items: + $ref: '#/components/schemas/GitHubLink' + + GitHubCallbackResponse: + type: object + properties: + success: + type: boolean + message: + type: string + github_username: + type: string + + DashboardResponse: + type: object + additionalProperties: true + +security: + - BearerAuth: [] + - CookieAuth: [] + +paths: + /auth/register: + post: + summary: Register a new user + tags: [Authentication] + security: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [name, email, password] + properties: + name: + type: string + email: + type: string + password: + type: string + role: + type: string + enum: [developer, team_lead, admin] + responses: + '201': + description: User registered successfully + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterResponse' + + /auth/login: + post: + summary: Authenticate a user and return JWT tokens + tags: [Authentication] + security: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [email, password] + properties: + email: + type: string + password: + type: string + responses: + '200': + description: Authentication successful + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + + /auth/refresh: + post: + summary: Refresh the access token using a refresh token + tags: [Authentication] + responses: + '200': + description: New access token generated + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokens' + + /auth/logout: + post: + summary: Log out the current user + tags: [Authentication] + responses: + '200': + description: Logout successful + content: + application/json: + schema: + type: object + properties: + message: + type: string + + /auth/me: + get: + summary: Get the current user profile + tags: [Authentication] + responses: + '200': + description: Current user profile + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + + /auth/token: + post: + summary: Get an access token directly + tags: [Authentication] + security: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [email, password] + properties: + email: + type: string + password: + type: string + responses: + '200': + description: Token generated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokens' + + /auth/permissions: + get: + summary: Get permissions for the current role + tags: [Authentication] + responses: + '200': + description: Permission list + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionsResponse' + + /users: + get: + summary: List all users visible to the caller + tags: [Users] + responses: + '200': + description: List of users + content: + application/json: + schema: + $ref: '#/components/schemas/UserListResponse' + + /users/{user_id}: + parameters: + - name: user_id + in: path + required: true + schema: + type: integer + get: + summary: Get a specific user + tags: [Users] + responses: + '200': + description: User details + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + put: + summary: Update a user as an admin + tags: [Users] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + email: + type: string + role: + type: string + enum: [developer, team_lead, admin] + password: + type: string + github_username: + type: string + avatar: + type: string + responses: + '200': + description: User updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + delete: + summary: Delete a user as an admin + tags: [Users] + responses: + '200': + description: User deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + + /profile: + get: + summary: Get the current user's profile + tags: [Users] + responses: + '200': + description: Current user profile + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + put: + summary: Update the current user's profile + tags: [Users] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + email: + type: string + github_username: + type: string + avatar: + type: string + current_password: + type: string + new_password: + type: string + responses: + '200': + description: Profile updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + + /projects: + get: + summary: Get all projects visible to the caller + tags: [Projects] + responses: + '200': + description: List of projects + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectListResponse' + post: + summary: Create a new project + tags: [Projects] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [name, description] + properties: + name: + type: string + description: + type: string + status: + type: string + enum: [active, completed, on_hold, cancelled] + github_repo: + type: string + team_members: + type: array + items: + type: integer + responses: + '201': + description: Project created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectResponse' + + /projects/{project_id}: + parameters: + - name: project_id + in: path + required: true + schema: + type: integer + get: + summary: Get a project by ID + tags: [Projects] + responses: + '200': + description: Project details + content: + application/json: + schema: + type: object + properties: + project: + $ref: '#/components/schemas/Project' + put: + summary: Update a project + tags: [Projects] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: + type: string + status: + type: string + enum: [active, completed, on_hold, cancelled] + github_repo: + type: string + team_members: + type: array + items: + type: integer + responses: + '200': + description: Project updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectResponse' + delete: + summary: Delete a project + tags: [Projects] + responses: + '204': + description: Project deleted successfully + + /projects/{project_id}/tasks: + parameters: + - name: project_id + in: path + required: true + schema: + type: integer + get: + summary: Get all tasks for a project + tags: [Projects, Tasks] + responses: + '200': + description: Task list for the project + content: + application/json: + schema: + $ref: '#/components/schemas/TaskListResponse' + + /tasks: + get: + summary: Fetch tasks filtered by role and query parameters + tags: [Tasks] + parameters: + - name: status + in: query + schema: + type: string + - name: assigned_to + in: query + schema: + type: integer + - name: project_id + in: query + schema: + type: integer + responses: + '200': + description: List of tasks + content: + application/json: + schema: + $ref: '#/components/schemas/TaskListResponse' + post: + summary: Create a new task + tags: [Tasks] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [title, description, status] + properties: + title: + type: string + description: + type: string + status: + type: string + enum: [backlog, todo, in_progress, review, done] + priority: + type: string + enum: [low, medium, high, urgent] + progress: + type: integer + minimum: 0 + maximum: 100 + assigned_to: + type: integer + deadline: + type: string + format: date-time + project_id: + type: integer + responses: + '201': + description: Task created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/TaskResponse' + + /tasks/{task_id}: + parameters: + - name: task_id + in: path + required: true + schema: + type: integer + get: + summary: Get a task by ID + tags: [Tasks] + responses: + '200': + description: Task details + content: + application/json: + schema: + type: object + properties: + task: + $ref: '#/components/schemas/Task' + put: + summary: Update a task + tags: [Tasks] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + title: + type: string + description: + type: string + status: + type: string + enum: [backlog, todo, in_progress, review, done] + priority: + type: string + enum: [low, medium, high, urgent] + progress: + type: integer + minimum: 0 + maximum: 100 + assigned_to: + type: integer + deadline: + type: string + format: date-time + responses: + '200': + description: Task updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/TaskResponse' + delete: + summary: Delete a task + tags: [Tasks] + responses: + '204': + description: Task deleted successfully + + /tasks/{task_id}/comments: + parameters: + - name: task_id + in: path + required: true + schema: + type: integer + get: + summary: Fetch comments for a task + tags: [Comments] + responses: + '200': + description: List of comments + content: + application/json: + schema: + $ref: '#/components/schemas/CommentListResponse' + post: + summary: Add a comment to a task + tags: [Comments] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [content] + properties: + content: + type: string + mentioned_user_ids: + type: array + items: + type: integer + mentioned_users: + type: array + items: + type: integer + responses: + '201': + description: Comment added successfully + content: + application/json: + schema: + $ref: '#/components/schemas/CommentResponse' + + /comments/{comment_id}: + parameters: + - name: comment_id + in: path + required: true + schema: + type: integer + put: + summary: Update a comment + tags: [Comments] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [content] + properties: + content: + type: string + responses: + '200': + description: Comment updated successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + comment: + $ref: '#/components/schemas/Comment' + delete: + summary: Delete a comment + tags: [Comments] + responses: + '200': + description: Comment deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + + /notifications: + get: + summary: Get the current user's notifications + tags: [Notifications] + parameters: + - name: is_read + in: query + schema: + type: boolean + responses: + '200': + description: Notification list + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationListResponse' + post: + summary: Create a notification + tags: [Notifications] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [user_id] + properties: + user_id: + type: integer + content: + type: string + message: + type: string + title: + type: string + notification_type: + type: string + type: + type: string + reference_id: + type: string + task_id: + type: integer + is_read: + type: boolean + responses: + '201': + description: Notification created successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + notification: + $ref: '#/components/schemas/Notification' + + /notifications/{notification_id}/read: + parameters: + - name: notification_id + in: path + required: true + schema: + type: integer + put: + summary: Mark a notification as read + tags: [Notifications] + responses: + '200': + description: Notification marked as read + content: + application/json: + schema: + type: object + properties: + message: + type: string + + /notifications/read-all: + put: + summary: Mark all notifications as read + tags: [Notifications] + responses: + '200': + description: All notifications marked as read + content: + application/json: + schema: + type: object + properties: + message: + type: string + + /notifications/{notification_id}: + parameters: + - name: notification_id + in: path + required: true + schema: + type: integer + delete: + summary: Delete a notification + tags: [Notifications] + responses: + '200': + description: Notification deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + + /dashboard: + get: + summary: Get the current user's dashboard data + tags: [Dashboard] + responses: + '200': + description: Dashboard payload + content: + application/json: + schema: + $ref: '#/components/schemas/DashboardResponse' + + /dashboard/client: + get: + summary: Get client dashboard data for developers and team leads + tags: [Dashboard] + responses: + '200': + description: Client dashboard payload + content: + application/json: + schema: + $ref: '#/components/schemas/DashboardResponse' + + /dashboard/admin: + get: + summary: Get admin dashboard data + tags: [Dashboard] + responses: + '200': + description: Admin dashboard payload + content: + application/json: + schema: + $ref: '#/components/schemas/DashboardResponse' + + /dashboard/projects/{project_id}: + parameters: + - name: project_id + in: path + required: true + schema: + type: integer + get: + summary: Get dashboard data for a specific project + tags: [Dashboard] + responses: + '200': + description: Project dashboard payload + content: + application/json: + schema: + $ref: '#/components/schemas/DashboardResponse' + + /admin/users: + get: + summary: List all users for admins and team leads + tags: [Admin] + responses: + '200': + description: List of users + content: + application/json: + schema: + $ref: '#/components/schemas/UserListResponse' + post: + summary: Create a user as an admin + tags: [Admin] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [name, email, password] + properties: + name: + type: string + email: + type: string + password: + type: string + role: + type: string + enum: [developer, team_lead, admin] + responses: + '201': + description: User created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + + /admin/users/{user_id}: + parameters: + - name: user_id + in: path + required: true + schema: + type: integer + put: + summary: Update a user as an admin + tags: [Admin] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + email: + type: string + role: + type: string + enum: [developer, team_lead, admin] + password: + type: string + github_username: + type: string + avatar: + type: string + responses: + '200': + description: User updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + delete: + summary: Delete a user as an admin + tags: [Admin] + responses: + '200': + description: User deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + + /admin/users/{user_id}/role: + parameters: + - name: user_id + in: path + required: true + schema: + type: integer + put: + summary: Update a user's role + tags: [Admin] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [role] + properties: + role: + type: string + enum: [developer, team_lead, admin] + responses: + '200': + description: User role updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + + /admin/stats: + get: + summary: Get system statistics + tags: [Admin] + responses: + '200': + description: System statistics + content: + application/json: + schema: + $ref: '#/components/schemas/SystemStatsResponse' + + /admin/settings: + get: + summary: Get system settings + tags: [Admin] + responses: + '200': + description: System settings + content: + application/json: + schema: + $ref: '#/components/schemas/SystemSettingsResponse' + put: + summary: Update system settings + tags: [Admin] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SystemSettings' + responses: + '200': + description: System settings updated successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + settings: + $ref: '#/components/schemas/SystemSettings' + + /admin/audit-logs: + get: + summary: Get audit logs + tags: [Admin, Audit] + parameters: + - name: action + in: query + schema: + type: string + - name: actor + in: query + schema: + type: string + - name: from + in: query + schema: + type: string + - name: to + in: query + schema: + type: string + - name: page + in: query + schema: + type: integer + - name: per_page + in: query + schema: + type: integer + responses: + '200': + description: Audit log list + content: + application/json: + schema: + $ref: '#/components/schemas/AuditLogListResponse' + + /admin/audit-logs/{log_id}: + parameters: + - name: log_id + in: path + required: true + schema: + type: integer + get: + summary: Get a single audit log entry + tags: [Admin, Audit] + responses: + '200': + description: Audit log details + content: + application/json: + schema: + $ref: '#/components/schemas/AuditLogResponse' + + /admin/audit-logs/cleanup: + post: + summary: Purge expired audit logs + tags: [Admin, Audit] + responses: + '200': + description: Audit log cleanup result + content: + application/json: + schema: + type: object + properties: + message: + type: string + deleted: + type: integer + + /admin/settings/retention/run: + post: + summary: Run retention cleanup immediately + tags: [Admin] + responses: + '200': + description: Retention cleanup result + content: + application/json: + schema: + $ref: '#/components/schemas/RetentionCleanupResponse' + + /reports: + post: + summary: Save a generated report + tags: [Reports] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [report_type, date_range, summary, details] + properties: + report_type: + type: string + enum: [tasks, developers, github] + date_range: + type: string + enum: [week, month, quarter, year] + summary: + type: object + additionalProperties: true + details: + type: array + items: + type: object + additionalProperties: true + responses: + '201': + description: Report saved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ReportResponse' + get: + summary: Get saved reports + tags: [Reports] + parameters: + - name: type + in: query + schema: + type: string + enum: [tasks, developers, github] + - name: dateRange + in: query + schema: + type: string + enum: [week, month, quarter, year] + - name: page + in: query + schema: + type: integer + - name: per_page + in: query + schema: + type: integer + responses: + '200': + description: Paginated report list + content: + application/json: + schema: + $ref: '#/components/schemas/PaginationReportResponse' + + /reports/{report_id}: + parameters: + - name: report_id + in: path + required: true + schema: + type: integer + get: + summary: Get a report by ID + tags: [Reports] + responses: + '200': + description: Report details + content: + application/json: + schema: + $ref: '#/components/schemas/ReportResponse' + delete: + summary: Delete a report + tags: [Reports] + responses: + '200': + description: Report deleted successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + + /github/config-check: + get: + summary: Check GitHub OAuth configuration + tags: [GitHub Integration] + security: [] + responses: + '200': + description: GitHub configuration status + content: + application/json: + schema: + $ref: '#/components/schemas/GitHubConfigCheckResponse' + + /github/auth: + get: + summary: Start the GitHub OAuth flow + tags: [GitHub Integration] + responses: + '200': + description: GitHub OAuth authorization URL + content: + application/json: + schema: + $ref: '#/components/schemas/GitHubOAuthResponse' + + /github/callback: + get: + summary: Handle GitHub OAuth callback + tags: [GitHub Integration] + security: [] + parameters: + - name: code + in: query + schema: + type: string + - name: state + in: query + schema: + type: string + responses: + '200': + description: GitHub account connected successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GitHubCallbackResponse' + '302': + description: Redirect to the frontend after successful connection + post: + summary: Handle GitHub OAuth callback using a JSON payload + tags: [GitHub Integration] + security: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [code, state] + properties: + code: + type: string + state: + type: string + responses: + '200': + description: GitHub account connected successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GitHubCallbackResponse' + + /github/exchange: + get: + summary: Exchange a GitHub OAuth code for a token + tags: [GitHub Integration] + security: [] + parameters: + - name: code + in: query + schema: + type: string + - name: state + in: query + schema: + type: string + responses: + '200': + description: GitHub account connected successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GitHubCallbackResponse' + + /github/status: + get: + summary: Get the current user's GitHub connection status + tags: [GitHub Integration] + responses: + '200': + description: GitHub connection status + content: + application/json: + schema: + $ref: '#/components/schemas/GitHubStatusResponse' + + /github/disconnect: + post: + summary: Disconnect the current user's GitHub account + tags: [GitHub Integration] + responses: + '200': + description: GitHub account disconnected successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + + /github/repositories: + get: + summary: Fetch GitHub repositories for the current user + tags: [GitHub Integration] + parameters: + - name: page + in: query + schema: + type: integer + - name: per_page + in: query + schema: + type: integer + - name: all_pages + in: query + schema: + type: boolean + - name: activity_window_days + in: query + schema: + type: integer + - name: include_activity + in: query + schema: + type: boolean + responses: + '200': + description: Repository list + content: + application/json: + schema: + $ref: '#/components/schemas/GitHubRepositoryListResponse' + post: + summary: Add a GitHub repository to track + tags: [GitHub Integration] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [repository_name, repository_url] + properties: + repository_name: + type: string + repository_url: + type: string + webhook_secret: + type: string + responses: + '201': + description: Repository added successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GitHubRepositoryResponse' + + /github/repositories/{repo_id}/issues: + parameters: + - name: repo_id + in: path + required: true + schema: + type: integer + get: + summary: Get issues for a tracked repository + tags: [GitHub Integration] + parameters: + - name: state + in: query + schema: + type: string + - name: page + in: query + schema: + type: integer + - name: per_page + in: query + schema: + type: integer + responses: + '200': + description: Issue list + content: + application/json: + schema: + type: object + properties: + issues: + type: array + items: + $ref: '#/components/schemas/GitHubIssue' + + /github/repositories/{repo_id}/pulls: + parameters: + - name: repo_id + in: path + required: true + schema: + type: integer + get: + summary: Get pull requests for a tracked repository + tags: [GitHub Integration] + parameters: + - name: state + in: query + schema: + type: string + - name: page + in: query + schema: + type: integer + - name: per_page + in: query + schema: + type: integer + responses: + '200': + description: Pull request list + content: + application/json: + schema: + type: object + properties: + pull_requests: + type: array + items: + $ref: '#/components/schemas/GitHubPullRequest' + + /tasks/{task_id}/github: + parameters: + - name: task_id + in: path + required: true + schema: + type: integer + post: + summary: Link a task with a GitHub issue or pull request + tags: [GitHub Integration] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [repo_id] + properties: + repo_id: + type: integer + issue_number: + type: integer + pull_request_number: + type: integer + responses: + '200': + description: GitHub link created or updated + content: + application/json: + schema: + $ref: '#/components/schemas/GitHubLinkResponse' + get: + summary: Get GitHub links for a task + tags: [GitHub Integration] + responses: + '200': + description: Task GitHub links + content: + application/json: + schema: + $ref: '#/components/schemas/GitHubLinkListResponse' + + /tasks/{task_id}/github/{link_id}: + parameters: + - name: task_id + in: path + required: true + schema: + type: integer + - name: link_id + in: path + required: true + schema: + type: integer + delete: + summary: Remove a GitHub link from a task + tags: [GitHub Integration] + responses: + '200': + description: GitHub link removed + content: + application/json: + schema: + type: object + properties: + message: + type: string diff --git a/frontend/src/pages/Landing.jsx b/frontend/src/pages/Landing.jsx index fe4351e..7d289fd 100644 --- a/frontend/src/pages/Landing.jsx +++ b/frontend/src/pages/Landing.jsx @@ -219,7 +219,7 @@ const Landing = () => {