diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..c5152a4a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,384 @@ +# Contributing to GenLayer Points + +Thank you for your interest in contributing to the GenLayer Points system. This document provides guidelines and instructions for contributing. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [Development Setup](#development-setup) +- [Project Architecture](#project-architecture) +- [Making Contributions](#making-contributions) +- [Pull Request Process](#pull-request-process) +- [Coding Standards](#coding-standards) +- [Testing Guidelines](#testing-guidelines) +- [Security Considerations](#security-considerations) +- [Common Issues](#common-issues) + +## Code of Conduct + +By participating in this project, you agree to: + +- Be respectful and inclusive in all interactions +- Provide constructive feedback +- Focus on the best outcomes for the community +- Report any unacceptable behavior + +## Getting Started + +### Prerequisites + +- **Python 3.8+** - Backend runtime +- **Node.js 16+** - Frontend build tools +- **PostgreSQL** (optional) - Production database (SQLite works for development) +- **Git** - Version control + +### Forking and Cloning + +1. Fork the repository on GitHub +2. Clone your fork: + ```bash + git clone https://github.com/YOUR_USERNAME/points.git + cd points + ``` +3. Add the upstream remote: + ```bash + git remote add upstream https://github.com/genlayer-foundation/points.git + ``` + +## Development Setup + +### Backend Setup + +```bash +cd backend + +# Create virtual environment +python -m venv env + +# Activate virtual environment +# On Windows: +env\Scripts\activate +# On macOS/Linux: +source env/bin/activate + +# Install dependencies +pip install -r requirements.txt + +# Copy environment variables +cp .env.example .env + +# Run migrations +python manage.py migrate + +# Create superuser (optional) +python manage.py createsuperuser + +# Start development server +python manage.py runserver +``` + +### Frontend Setup + +```bash +cd frontend + +# Install dependencies +npm install + +# Copy environment variables +cp .env.example .env + +# Start development server +npm run dev +``` + +### Environment Variables + +#### Backend (.env) + +```env +DEBUG=True +SECRET_KEY=your-secret-key +DATABASE_URL=sqlite:///db.sqlite3 +ALLOWED_HOSTS=localhost,127.0.0.1 +FRONTEND_URL=http://localhost:5173 +SIWE_DOMAIN=localhost:5173 +RECAPTCHA_SECRET_KEY=your-recaptcha-secret +``` + +#### Frontend (.env) + +```env +VITE_API_URL=http://localhost:8000 +VITE_RECAPTCHA_SITE_KEY=your-recaptcha-site-key +``` + +## Project Architecture + +### Backend (Django REST Framework) + +``` +backend/ +├── api/ # Main API configuration and metrics +├── contributions/ # Contribution tracking system +│ ├── models.py # ContributionType, Contribution, Evidence +│ ├── views.py # ViewSets for contributions +│ └── serializers.py +├── ethereum_auth/ # SIWE authentication +├── leaderboard/ # Points and rankings +├── users/ # User management +├── validators/ # Validator profiles +├── builders/ # Builder profiles +├── stewards/ # Steward management +└── tally/ # Django project settings +``` + +### Frontend (Svelte 5) + +``` +frontend/ +├── src/ +│ ├── components/ # Reusable UI components +│ ├── routes/ # Page components +│ ├── lib/ # Utilities and API clients +│ │ ├── api.js # API endpoint definitions +│ │ ├── auth.js # Authentication logic +│ │ └── wallet/ # Wallet integration +│ └── assets/ # Static assets +└── public/ # Public files +``` + +## Making Contributions + +### Branch Naming + +**Important**: The `dev` branch is the main development branch. Create feature branches from `dev`. + +```bash +# Update your local dev branch +git checkout dev +git pull upstream dev + +# Create feature branch +git checkout -b feature/your-feature-name + +# Or for bug fixes +git checkout -b fix/issue-description +``` + +### Types of Contributions + +1. **Bug Fixes** - Fix issues in existing functionality +2. **Features** - Add new functionality +3. **Documentation** - Improve docs, comments, or examples +4. **Tests** - Add or improve test coverage +5. **Performance** - Optimize existing code +6. **Security** - Fix vulnerabilities or improve security + +### Finding Issues + +- Check the [Issues](https://github.com/genlayer-foundation/points/issues) page +- Look for issues labeled `good first issue` or `help wanted` +- Browse the codebase for `TODO` or `FIXME` comments + +## Pull Request Process + +### Before Submitting + +1. **Sync with upstream**: + ```bash + git fetch upstream + git rebase upstream/dev + ``` + +2. **Run tests**: + ```bash + # Backend + cd backend + python manage.py test + + # Frontend + cd frontend + npm test + ``` + +3. **Check linting**: + ```bash + # Backend + flake8 . + + # Frontend + npm run lint + ``` + +### PR Guidelines + +1. **Target the `dev` branch** - Not `main` +2. **Write clear titles** - Use conventional commits (feat:, fix:, docs:, etc.) +3. **Include description** - Explain what and why +4. **Link issues** - Reference related issues with "Fixes #123" +5. **Keep PRs focused** - One feature or fix per PR +6. **Add tests** - For new functionality +7. **Update docs** - If behavior changes + +### Commit Message Format + +``` +type(scope): subject + +body (optional) + +footer (optional) +``` + +**Types**: feat, fix, docs, style, refactor, test, chore + +**Examples**: +``` +feat(contributions): add evidence URL validation +fix(auth): resolve wallet connection timeout +docs(readme): update setup instructions +``` + +## Coding Standards + +### Python (Backend) + +- Follow PEP 8 style guide +- Use type hints where appropriate +- Document functions with docstrings +- Keep functions focused and small +- Use meaningful variable names + +```python +def calculate_points( + contribution_type: ContributionType, + base_points: int, + multiplier: Decimal +) -> int: + """ + Calculate frozen global points for a contribution. + + Args: + contribution_type: The type of contribution + base_points: Raw points before multiplier + multiplier: Current multiplier value + + Returns: + Calculated frozen global points + """ + return int(base_points * multiplier) +``` + +### JavaScript/Svelte (Frontend) + +- Use modern ES6+ syntax +- Follow Svelte 5 runes patterns (`$state`, `$derived`, `$effect`) +- Use meaningful component and variable names +- Keep components focused and reusable + +```svelte + + + +``` + +## Testing Guidelines + +### Backend Testing + +```python +from django.test import TestCase +from rest_framework.test import APITestCase + +class ContributionTestCase(APITestCase): + def setUp(self): + self.user = User.objects.create_user( + email='test@example.com', + address='0x1234...' + ) + + def test_create_contribution(self): + response = self.client.post('/api/v1/contributions/', { + 'contribution_type': 1, + 'points': 10 + }) + self.assertEqual(response.status_code, 201) +``` + +### Frontend Testing + +```javascript +import { render, screen } from '@testing-library/svelte'; +import { describe, it, expect } from 'vitest'; +import Component from './Component.svelte'; + +describe('Component', () => { + it('renders correctly', () => { + render(Component, { props: { value: 'test' } }); + expect(screen.getByText('test')).toBeInTheDocument(); + }); +}); +``` + +## Security Considerations + +### Authentication + +- Never expose sensitive data in client-side code +- Validate all user inputs on the server +- Use SIWE (Sign-In With Ethereum) for authentication +- Store session data securely + +### API Security + +- Implement proper permission checks +- Use rate limiting for sensitive endpoints +- Sanitize user inputs to prevent injection +- Validate file uploads and URLs + +### Common Vulnerabilities to Avoid + +1. **SQL Injection** - Use Django ORM, never raw queries with user input +2. **XSS** - Sanitize user-generated content +3. **CSRF** - Django handles this, ensure it's not disabled +4. **Sensitive Data Exposure** - Don't log or expose private keys + +## Common Issues + +### Wallet Connection Issues + +Multiple wallets installed can cause conflicts. See the wallet detection logic in `frontend/src/components/WalletSelector.svelte` for handling. + +### CAPTCHA Validation Fails + +Ensure reCAPTCHA keys match between frontend and backend environments. + +### Session Authentication Errors + +Check CORS and cookie settings if authentication state isn't persisting. + +### Database Migration Errors + +Always run migrations after pulling new changes: +```bash +python manage.py migrate +``` + +## Questions? + +- Open a [Discussion](https://github.com/genlayer-foundation/points/discussions) +- Join the GenLayer community channels +- Review existing issues and PRs + +Thank you for contributing to GenLayer Points! diff --git a/README.md b/README.md index 6ba1602d..4a7c662f 100644 --- a/README.md +++ b/README.md @@ -66,60 +66,69 @@ eventually be part of the Deepthought DAO. ### Backend Setup 1. Clone the repository: - ``` - git clone https://github.com/yourusername/points.git + ```bash + git clone https://github.com/genlayer-foundation/points.git cd points ``` 2. Create and activate a virtual environment: - ``` + ```bash cd backend python -m venv env source env/bin/activate # On Windows: env\Scripts\activate ``` 3. Install dependencies: - ``` + ```bash pip install -r requirements.txt ``` -4. Setup Node.js environment using nodeenv: - ``` - nodeenv -p +4. Configure environment variables: + ```bash + cp .env.example .env + # Edit .env with your settings ``` 5. Run migrations: - ``` + ```bash python manage.py migrate ``` -6. Create a superuser: - ``` +6. Create a superuser (optional): + ```bash python manage.py createsuperuser ``` 7. Run the development server: - ``` + ```bash python manage.py runserver ``` ### Frontend Setup 1. Navigate to the frontend directory: - ``` + ```bash cd frontend ``` 2. Install dependencies: - ``` + ```bash npm install ``` -3. Run the development server: +3. Configure environment variables: + ```bash + cp .env.example .env + # Edit .env to point to your backend ``` + +4. Run the development server: + ```bash npm run dev ``` +5. Open http://localhost:5173 in your browser + ## Development ### Backend Development @@ -159,15 +168,22 @@ The frontend is built with Svelte 5 and will include: ## Contributing -Contributions are welcome! Please feel free to submit a Pull Request. +Contributions are welcome! Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed guidelines. + +### Quick Start + +1. Fork the repository +2. Create a feature branch from `dev`: `git checkout -b feature/your-feature-name` +3. Make your changes +4. Run tests: `python manage.py test` and `npm test` +5. Push your branch: `git push origin feature/your-feature-name` +6. Submit a pull request to the `dev` branch + +**Note**: All pull requests should target the `dev` branch, not `main`. -### Development Workflow +## Security -1. Create a feature branch: `git checkout -b feature/your-feature-name` -2. Make your changes -3. Run tests: `python manage.py test` -4. Push your branch: `git push origin feature/your-feature-name` -5. Submit a pull request +For security concerns, please see [SECURITY.md](./SECURITY.md). ## License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..15beaa32 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,151 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| latest | :white_check_mark: | + +## Reporting a Vulnerability + +We take security seriously. If you discover a security vulnerability, please report it responsibly. + +### How to Report + +1. **Do NOT create a public issue** for security vulnerabilities +2. Email security concerns to the GenLayer Foundation team +3. Include detailed steps to reproduce the vulnerability +4. Provide any relevant proof-of-concept code + +### What to Include + +- Type of vulnerability +- Full path of affected file(s) +- Steps to reproduce +- Potential impact +- Suggested fix (if any) + +### Response Timeline + +- **Initial Response**: Within 48 hours +- **Assessment**: Within 7 days +- **Fix Timeline**: Depends on severity, typically 30-90 days + +## Security Best Practices + +### Authentication + +The Points system uses Sign-In With Ethereum (SIWE) for authentication: + +1. **Nonce Handling** + - Nonces are generated server-side with cryptographic randomness + - Nonces expire after 5 minutes + - Each nonce can only be used once + +2. **Session Management** + - Sessions are stored server-side + - Session cookies are httpOnly and secure in production + - Sessions expire after inactivity + +3. **Wallet Integration** + - No private keys are ever transmitted to the server + - Only signed messages are verified + +### API Security + +1. **Input Validation** + - All user inputs are validated and sanitized + - File uploads are restricted by type and size + - URLs are validated before storage + +2. **Rate Limiting** + - Consider implementing rate limiting on sensitive endpoints: + - `/api/auth/nonce/` - Nonce generation + - `/api/auth/login/` - Login attempts + - `/api/v1/submissions/` - Submission creation + +3. **CORS Configuration** + - CORS is restricted to known frontend origins + - Credentials are only allowed for specific origins + +### Known Security Considerations + +#### Nonce Cleanup + +Expired nonces should be periodically cleaned from the database to prevent storage bloat: + +```python +# Add to a management command or scheduled task +from django.utils import timezone +from ethereum_auth.models import Nonce + +def cleanup_expired_nonces(): + """Remove expired nonces older than 1 hour.""" + cutoff = timezone.now() - timedelta(hours=1) + Nonce.objects.filter(expires_at__lt=cutoff).delete() +``` + +#### Email Generation + +Auto-generated emails for wallet users follow a predictable pattern. While this doesn't expose any sensitive data, consider: + +- Not using these emails for any communication +- Implementing email verification for actual contact + +#### Session Fixation + +The application regenerates session IDs on login to prevent session fixation attacks. + +### Development Security + +1. **Environment Variables** + - Never commit `.env` files + - Use different secrets for development and production + - Rotate secrets periodically + +2. **Dependencies** + - Regularly update dependencies + - Run security audits: + ```bash + # Python + pip-audit + + # Node.js + npm audit + ``` + +3. **Code Review** + - All changes require code review + - Security-sensitive changes require additional review + +### Deployment Security + +1. **HTTPS Only** + - All production traffic must use HTTPS + - HSTS headers are set + +2. **Content Security Policy** + - Implement appropriate CSP headers + - Restrict script sources + +3. **Database** + - Use parameterized queries (Django ORM handles this) + - Regular backups with encryption + - Principle of least privilege for database users + +## Security Checklist for Contributors + +Before submitting a PR, ensure: + +- [ ] No sensitive data in logs or error messages +- [ ] User inputs are validated on the server +- [ ] Authentication/authorization checks are in place +- [ ] No hardcoded secrets or credentials +- [ ] SQL queries use ORM or parameterized queries +- [ ] File uploads validate content type and size +- [ ] External URLs are validated before use +- [ ] Error messages don't leak implementation details + +## Acknowledgments + +We appreciate security researchers who responsibly disclose vulnerabilities. Contributors who report valid security issues will be acknowledged (unless they prefer to remain anonymous). diff --git a/backend/ethereum_auth/management/__init__.py b/backend/ethereum_auth/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/ethereum_auth/management/commands/__init__.py b/backend/ethereum_auth/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/ethereum_auth/management/commands/cleanup_nonces.py b/backend/ethereum_auth/management/commands/cleanup_nonces.py new file mode 100644 index 00000000..7c58fd68 --- /dev/null +++ b/backend/ethereum_auth/management/commands/cleanup_nonces.py @@ -0,0 +1,78 @@ +""" +Management command to clean up expired authentication nonces. + +Run manually: + python manage.py cleanup_nonces + +Schedule with cron (recommended daily): + 0 3 * * * cd /path/to/backend && python manage.py cleanup_nonces + +Or use Django-celery-beat for scheduled tasks. +""" +from django.core.management.base import BaseCommand +from django.utils import timezone +from datetime import timedelta + +from ethereum_auth.models import Nonce + + +class Command(BaseCommand): + help = 'Clean up expired and used nonces from the database' + + def add_arguments(self, parser): + parser.add_argument( + '--hours', + type=int, + default=1, + help='Delete nonces that expired more than this many hours ago (default: 1)' + ) + parser.add_argument( + '--dry-run', + action='store_true', + help='Show what would be deleted without actually deleting' + ) + + def handle(self, *args, **options): + hours = options['hours'] + dry_run = options['dry_run'] + + cutoff_time = timezone.now() - timedelta(hours=hours) + + # Find nonces to delete: + # 1. Expired nonces older than cutoff + # 2. Used nonces (regardless of age, they're no longer needed) + expired_nonces = Nonce.objects.filter(expires_at__lt=cutoff_time) + used_nonces = Nonce.objects.filter(used=True, created_at__lt=cutoff_time) + + expired_count = expired_nonces.count() + used_count = used_nonces.count() + + # Get total count of nonces to delete (combine queries) + nonces_to_delete = Nonce.objects.filter( + models.Q(expires_at__lt=cutoff_time) | + models.Q(used=True, created_at__lt=cutoff_time) + ) + total_count = nonces_to_delete.count() + + if dry_run: + self.stdout.write( + self.style.WARNING( + f'DRY RUN: Would delete {total_count} nonces ' + f'({expired_count} expired, {used_count} used)' + ) + ) + else: + deleted_count, _ = nonces_to_delete.delete() + self.stdout.write( + self.style.SUCCESS( + f'Successfully deleted {deleted_count} nonces' + ) + ) + + # Report remaining nonces + remaining = Nonce.objects.count() + self.stdout.write(f'Remaining nonces in database: {remaining}') + + +# Import models for Q objects +from django.db import models diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md new file mode 100644 index 00000000..b2ae80c3 --- /dev/null +++ b/docs/API_REFERENCE.md @@ -0,0 +1,515 @@ +# API Reference + +This document provides a comprehensive reference for the GenLayer Points API. + +## Base URL + +``` +Production: https://api.points.genlayer.com/api/v1 +Development: http://localhost:8000/api/v1 +``` + +## Authentication + +The API uses Sign-In With Ethereum (SIWE) for authentication with session-based cookies. + +### Authentication Endpoints + +#### Get Nonce + +```http +GET /api/auth/nonce/ +``` + +Returns a nonce for SIWE message signing. + +**Response:** +```json +{ + "nonce": "abc123xyz..." +} +``` + +#### Login + +```http +POST /api/auth/login/ +Content-Type: application/json +``` + +**Request Body:** +```json +{ + "message": "domain wants you to sign in...", + "signature": "0x...", + "referral_code": "ABC12345" // optional +} +``` + +**Response:** +```json +{ + "authenticated": true, + "address": "0x123...", + "user_id": 1, + "created": false, + "referral_code": "XYZ98765", + "referred_by": null +} +``` + +#### Verify Authentication + +```http +GET /api/auth/verify/ +``` + +**Response:** +```json +{ + "authenticated": true, + "address": "0x123...", + "user_id": 1 +} +``` + +#### Logout + +```http +POST /api/auth/logout/ +``` + +**Response:** +```json +{ + "message": "Logged out successfully." +} +``` + +--- + +## Users + +### Get Current User + +```http +GET /api/v1/users/me/ +``` + +Requires authentication. + +**Response:** +```json +{ + "id": 1, + "email": "user@example.com", + "name": "John Doe", + "address": "0x123...", + "description": "Bio text", + "profile_image_url": "https://...", + "banner_image_url": "https://...", + "website": "https://example.com", + "twitter_handle": "johndoe", + "discord_handle": "johndoe#1234", + "github_username": "johndoe", + "referral_code": "ABC12345" +} +``` + +### Update Profile + +```http +PATCH /api/v1/users/me/ +Content-Type: application/json +``` + +**Request Body:** +```json +{ + "name": "New Name", + "description": "Updated bio", + "twitter_handle": "newhandle" +} +``` + +### Get User by Address + +```http +GET /api/v1/users/by-address/{address}/ +``` + +**Response:** Same as current user object + +### Get User Highlights + +```http +GET /api/v1/users/by-address/{address}/highlights/ +``` + +**Query Parameters:** +- `limit` (default: 5) - Number of highlights to return + +--- + +## Contributions + +### List Contributions + +```http +GET /api/v1/contributions/ +``` + +**Query Parameters:** +- `page` (default: 1) +- `page_size` (default: 10) +- `user_address` - Filter by user +- `category` - Filter by category slug +- `group_consecutive` (default: false) - Group same-type contributions + +**Response:** +```json +{ + "count": 100, + "next": "http://.../contributions/?page=2", + "previous": null, + "results": [ + { + "id": 1, + "user": 1, + "user_details": {...}, + "contribution_type": 1, + "contribution_type_name": "Blog Post", + "points": 50, + "frozen_global_points": 100, + "multiplier_at_creation": "2.00", + "contribution_date": "2024-01-15T10:30:00Z", + "notes": "Published article about...", + "evidence_items": [...], + "created_at": "2024-01-15T10:30:00Z" + } + ] +} +``` + +### Get Contribution Highlights + +```http +GET /api/v1/contributions/highlights/ +``` + +**Query Parameters:** +- `limit` (default: 10) +- `category` - Filter by category slug +- `waitlist_only` (default: false) + +--- + +## Contribution Types + +### List Contribution Types + +```http +GET /api/v1/contribution-types/ +``` + +**Query Parameters:** +- `category` - Filter by category slug +- `is_submittable` - Filter by submittable status + +**Response:** +```json +{ + "count": 10, + "results": [ + { + "id": 1, + "name": "Blog Post", + "slug": "blog-post", + "description": "Write articles about GenLayer", + "category": "builder", + "min_points": 10, + "max_points": 100, + "current_multiplier": 2.0, + "is_submittable": true, + "examples": ["Tutorial", "Review"] + } + ] +} +``` + +### Get Type Statistics + +```http +GET /api/v1/contribution-types/statistics/ +``` + +**Query Parameters:** +- `category` - Filter by category slug + +### Get Top Contributors for Type + +```http +GET /api/v1/contribution-types/{id}/top_contributors/ +``` + +Returns top 10 contributors for a specific contribution type. + +--- + +## User Submissions + +Endpoints for managing user-submitted contributions. + +### List My Submissions + +```http +GET /api/v1/submissions/my/ +``` + +Requires authentication. + +**Query Parameters:** +- `state` - Filter by state (pending, accepted, rejected, more_info_needed) +- `page` (default: 1) +- `page_size` (default: 20) + +**Response:** +```json +{ + "count": 5, + "results": [ + { + "id": "uuid...", + "contribution_type": 1, + "contribution_type_name": "Blog Post", + "contribution_date": "2024-01-15", + "notes": "My submission notes", + "state": "pending", + "state_display": "Pending Review", + "staff_reply": "", + "evidence_items": [...], + "can_edit": true, + "created_at": "2024-01-15T10:30:00Z" + } + ] +} +``` + +### Create Submission + +```http +POST /api/v1/submissions/ +Content-Type: application/json +``` + +**Request Body:** +```json +{ + "contribution_type": 1, + "contribution_date": "2024-01-15", + "notes": "Description of my contribution", + "recaptcha": "recaptcha-token", + "mission": 1 // optional +} +``` + +### Update Submission + +```http +PATCH /api/v1/submissions/{id}/ +Content-Type: application/json +``` + +Only allowed when state is `pending` or `more_info_needed`. + +**Request Body:** +```json +{ + "notes": "Updated description", + "evidence_items": [ + {"description": "Link to work", "url": "https://..."} + ] +} +``` + +### Cancel Submission + +```http +DELETE /api/v1/submissions/{id}/ +``` + +Soft deletes by marking as rejected. + +### Add Evidence + +```http +POST /api/v1/submissions/{id}/add-evidence/ +Content-Type: application/json +``` + +**Request Body:** +```json +{ + "description": "Additional proof", + "url": "https://example.com/proof" +} +``` + +--- + +## Leaderboard + +### Get Leaderboard + +```http +GET /api/v1/leaderboard/ +``` + +**Query Parameters:** +- `type` - Leaderboard type (global, validator, builder, etc.) +- `page` (default: 1) +- `page_size` (default: 50) +- `order` (default: asc) - Rank order + +**Response:** +```json +{ + "count": 1000, + "results": [ + { + "rank": 1, + "user": {...}, + "total_points": 5000, + "contribution_count": 25 + } + ] +} +``` + +### Get Stats + +```http +GET /api/v1/leaderboard/stats/ +``` + +**Query Parameters:** +- `type` - Filter by leaderboard type + +**Response:** +```json +{ + "participant_count": 1500, + "contribution_count": 10000, + "total_points": 500000 +} +``` + +### Get Trending Contributors + +```http +GET /api/v1/leaderboard/trending/ +``` + +**Query Parameters:** +- `limit` (default: 10) + +--- + +## Missions + +### List Missions + +```http +GET /api/v1/missions/ +``` + +**Query Parameters:** +- `contribution_type` - Filter by contribution type ID +- `active` (default: false) - Only show active missions + +### Get Mission + +```http +GET /api/v1/missions/{id}/ +``` + +--- + +## Error Responses + +### 400 Bad Request + +```json +{ + "error": "Validation failed", + "details": { + "field_name": ["Error message"] + } +} +``` + +### 401 Unauthorized + +```json +{ + "detail": "Authentication credentials were not provided." +} +``` + +### 403 Forbidden + +```json +{ + "error": "You do not have permission to perform this action." +} +``` + +### 404 Not Found + +```json +{ + "detail": "Not found." +} +``` + +### 500 Internal Server Error + +```json +{ + "error": "An unexpected error occurred." +} +``` + +--- + +## Rate Limiting + +API endpoints may be rate limited. When rate limited, you'll receive: + +```http +HTTP/1.1 429 Too Many Requests +Retry-After: 60 +``` + +```json +{ + "detail": "Request was throttled. Expected available in 60 seconds." +} +``` + +--- + +## Pagination + +List endpoints return paginated responses: + +```json +{ + "count": 100, + "next": "http://api/v1/resource/?page=2", + "previous": null, + "results": [...] +} +``` + +Use `page` and `page_size` query parameters to navigate. diff --git a/docs/BEST_PRACTICES.md b/docs/BEST_PRACTICES.md new file mode 100644 index 00000000..1fcbcc26 --- /dev/null +++ b/docs/BEST_PRACTICES.md @@ -0,0 +1,354 @@ +# Developer Best Practices + +This document outlines best practices for developing and maintaining the GenLayer Points system. + +## Table of Contents + +- [Code Organization](#code-organization) +- [Django Best Practices](#django-best-practices) +- [Svelte 5 Best Practices](#svelte-5-best-practices) +- [API Design](#api-design) +- [Database Optimization](#database-optimization) +- [Error Handling](#error-handling) +- [Testing](#testing) +- [Performance](#performance) + +## Code Organization + +### Directory Structure + +Follow the existing project structure: + +``` +backend/ +├── app_name/ +│ ├── models.py # Data models +│ ├── views.py # ViewSets and views +│ ├── serializers.py # DRF serializers +│ ├── permissions.py # Custom permissions +│ ├── admin.py # Admin configuration +│ ├── urls.py # URL routing +│ └── tests/ # Test files +``` + +### File Naming + +- Use lowercase with underscores for Python files +- Use PascalCase for Svelte components +- Keep file names descriptive but concise + +## Django Best Practices + +### Models + +1. **Use abstract base models for common fields**: + ```python + from utils.models import BaseModel + + class MyModel(BaseModel): + # Inherits created_at, updated_at + name = models.CharField(max_length=100) + ``` + +2. **Add proper help_text and verbose names**: + ```python + class Contribution(models.Model): + points = models.PositiveIntegerField( + help_text="Base points before multiplier" + ) + + class Meta: + verbose_name = "Contribution" + verbose_name_plural = "Contributions" + ``` + +3. **Use database constraints**: + ```python + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['user', 'contribution_type'], + name='unique_user_contribution_type' + ) + ] + ``` + +### Views + +1. **Use select_related and prefetch_related**: + ```python + def get_queryset(self): + return Contribution.objects.select_related( + 'user', 'contribution_type' + ).prefetch_related( + 'evidence_items' + ) + ``` + +2. **Return appropriate HTTP status codes**: + ```python + from rest_framework import status + + return Response(data, status=status.HTTP_201_CREATED) + ``` + +3. **Use DRF decorators for actions**: + ```python + @action(detail=True, methods=['post']) + def approve(self, request, pk=None): + instance = self.get_object() + # ... + ``` + +### Serializers + +1. **Use nested serializers carefully**: + ```python + # Use SerializerMethodField for complex logic + class ContributionSerializer(serializers.ModelSerializer): + user_details = serializers.SerializerMethodField() + + def get_user_details(self, obj): + return LightUserSerializer(obj.user).data + ``` + +2. **Implement validation in validate() method**: + ```python + def validate(self, data): + if data['min_points'] > data['max_points']: + raise serializers.ValidationError( + "min_points cannot exceed max_points" + ) + return data + ``` + +## Svelte 5 Best Practices + +### State Management + +1. **Use runes properly**: + ```svelte + + ``` + +2. **Avoid unnecessary reactivity**: + ```svelte + + let filtered = $derived( + items.filter(item => item.active) + ); + + + {#each items.filter(item => item.active) as item} + ``` + +### Component Design + +1. **Use props with defaults**: + ```svelte + + ``` + +2. **Keep components focused**: + - One responsibility per component + - Extract reusable logic to lib/ + +### API Integration + +1. **Handle loading and error states**: + ```svelte + + + {#if loading} + + {:else if error} + + {:else} + + {/if} + ``` + +## API Design + +### URL Patterns + +- Use nouns, not verbs: `/contributions/` not `/getContributions/` +- Use plural names: `/users/` not `/user/` +- Nest resources logically: `/users/{id}/contributions/` + +### Response Format + +```json +{ + "count": 100, + "next": "http://api/v1/items/?page=2", + "previous": null, + "results": [...] +} +``` + +### Error Responses + +```json +{ + "error": "Validation failed", + "details": { + "field_name": ["Error message"] + } +} +``` + +## Database Optimization + +### Query Optimization + +1. **Use database indexes**: + ```python + class Meta: + indexes = [ + models.Index(fields=['created_at']), + models.Index(fields=['user', 'contribution_type']), + ] + ``` + +2. **Avoid N+1 queries**: + ```python + # Bad: N+1 query + for contribution in Contribution.objects.all(): + print(contribution.user.name) + + # Good: Single query with join + for contribution in Contribution.objects.select_related('user'): + print(contribution.user.name) + ``` + +3. **Use `only()` and `defer()` for partial selects**: + ```python + User.objects.only('id', 'name', 'address') + ``` + +### Caching + +Consider caching for: +- Leaderboard rankings +- Contribution type statistics +- User profile data + +## Error Handling + +### Backend + +```python +from rest_framework.exceptions import ValidationError, PermissionDenied + +def my_view(request): + try: + # operation + except Model.DoesNotExist: + raise ValidationError("Resource not found") + except PermissionError: + raise PermissionDenied("Not authorized") +``` + +### Frontend + +```javascript +try { + const response = await api.post('/endpoint/', data); + showSuccess('Success!'); +} catch (error) { + if (error.response?.status === 400) { + showError(error.response.data.error || 'Validation failed'); + } else if (error.response?.status === 403) { + showError('Permission denied'); + } else { + showError('An unexpected error occurred'); + } +} +``` + +## Testing + +### Backend Tests + +```python +from django.test import TestCase +from rest_framework.test import APITestCase + +class ContributionTests(APITestCase): + def setUp(self): + self.user = User.objects.create_user(...) + self.client.force_authenticate(user=self.user) + + def test_create_contribution(self): + response = self.client.post('/api/v1/contributions/', {...}) + self.assertEqual(response.status_code, 201) +``` + +### Frontend Tests + +```javascript +import { render, fireEvent } from '@testing-library/svelte'; +import Component from './Component.svelte'; + +test('handles click', async () => { + const { getByRole } = render(Component); + const button = getByRole('button'); + await fireEvent.click(button); + // assertions +}); +``` + +## Performance + +### Backend + +1. Use database connection pooling in production +2. Implement pagination for list endpoints +3. Use async views for I/O-bound operations +4. Cache expensive computations + +### Frontend + +1. Lazy load routes and large components +2. Debounce search inputs +3. Use virtual scrolling for large lists +4. Optimize images and assets + +### Monitoring + +- Log slow queries (>100ms) +- Monitor API response times +- Track frontend performance metrics diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index e3eb9e94..7cd852b0 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -154,6 +154,34 @@ export const buildersAPI = { getNewestBuilders: (limit = 5) => api.get('/builders/newest/', { params: { limit } }) }; +// User Submissions API (for user's own submissions) +export const submissionsAPI = { + // Get current user's submissions + getMySubmissions: (params = {}) => api.get('/submissions/my/', { params }), + + // Get a single submission by ID + getSubmission: (id) => api.get(`/submissions/${id}/`), + + // Create a new submission + createSubmission: (data) => { + if (data instanceof FormData) { + return api.post('/submissions/', data, { + headers: { 'Content-Type': 'multipart/form-data' } + }); + } + return api.post('/submissions/', data); + }, + + // Update an existing submission + updateSubmission: (id, data) => api.patch(`/submissions/${id}/`, data), + + // Cancel (soft delete) a submission + cancelSubmission: (id) => api.delete(`/submissions/${id}/`), + + // Add evidence to a submission + addEvidence: (id, data) => api.post(`/submissions/${id}/add-evidence/`, data) +}; + // Journey API export const journeyAPI = { startValidatorJourney: () => api.post('/users/start_validator_journey/'),