An open API service indexing awesome lists of open source software.

https://github.com/earth-app/mantle2

Backend v2 API, Powered by Drupal
https://github.com/earth-app/mantle2

drupal drupal-backend earth-app openapi php symfony

Last synced: about 2 months ago
JSON representation

Backend v2 API, Powered by Drupal

Awesome Lists containing this project

README

          

# mantle2

> Backend for The Earth App, powered by Drupal 11

This is the second version of the backend system for The Earth App,
a comprehensive RESTful API built on top of PHP 8.4 and Drupal 11.3.
The module provides a complete backend infrastructure for a social
networking platform focused on novelty, activities, and user engagement.

## Table of Contents

- [Overview](#overview)
- [Technical Stack](#technical-stack)
- [Architecture](#architecture)
- [Installation](#installation)
- [Core Components](#core-components)
- [Security & Performance](#security--performance)
- [Development](#development)
- [Testing](#testing)

## Overview

Mantle2 is a custom Drupal 11 module that implements a RESTful API backend
for The Earth App. It leverages Drupal's entity system, field API, and routing
infrastructure while adding custom controllers, services, and event subscribers
to create a modern, scalable API platform.

### Key Features

- **RESTful API** with OpenAPI/Swagger documentation
- **User Management** with token auth, OAuth, profiles, and social features
- **Activity Tracking** for environmental activities with custom fields
- **Event Management** with participation, cancellation, and image submissions
- **Prompt System** with visibility-aware discovery and response threads
- **Article Content** with quizzes, moderation, and user attribution
- **Gamification** with badges, points, quests, and profile cosmetics
- **Rate Limiting** with configurable per-endpoint and global limits
- **CORS Support** with origin whitelisting
- **Redis Caching** with automatic fallback to Drupal cache
- **Email Notifications** with HTML rendering and verification codes

## Technical Stack

### Core Technologies

- **PHP**: 8.4+
- **Drupal Core**: 11.3+
- **Symfony**: 7.3+ (Event Dispatcher, Rate Limiter, Cache)
- **Redis**: Optional caching layer via `drupal/redis` module
- **PostgreSQL/MySQL**: Database backend (Drupal standard)

### Development Tools

- **Composer**: PHP dependency management
- **Bun**: JavaScript runtime for development tooling
- **PHPUnit**: 11.5+ for unit testing
- **Drush**: 13.7+ for Drupal CLI operations
- **PHPStan**: Static analysis
- **PHP CodeSniffer**: Code quality enforcement
- **Prettier**: Code formatting for PHP, XML, YAML, JSON

### Key Dependencies

```json
{
"drupal/core": "^11.3",
"drupal/json_field": "^1.4", // JSON field storage
"drupal/key": "1.22.0", // API key management
"drupal/smtp": "^1.4", // Email delivery
"drupal/redis": "^1.10", // Redis integration
"drupal/openid_connect": "^3.0@alpha", // OAuth/OpenID providers
"symfony/rate-limiter": "^7.3", // Rate limiting
"symfony/event-dispatcher": "^7.3", // Event system
"symfony/cache": "^7.3" // Cache abstractions
}
```

## Architecture

### Directory Structure

```text
mantle2/
├── src/
│ ├── Controller/ # API endpoint controllers
│ │ └── Schema/ # OpenAPI schema generators
│ ├── Custom/ # Domain models and enums
│ ├── EventSubscriber/ # Symfony event subscribers
│ ├── Plugin/ # OpenID Connect client plugins
│ └── Service/ # Business logic helpers
├── tests/
│ └── src/
│ ├── Unit/ # PHPUnit tests
│ └── Mocks.php # Test fixtures
├── mantle2.info.yml # Module metadata
├── mantle2.module # Hook implementations
├── mantle2.install # Installation & schema
├── mantle2.routing.yml # API route definitions (200+ endpoints)
├── mantle2.caching.yml # Custom server-side caching definitions
├── mantle2.services.yml # Service container definitions
├── composer.json # PHP dependencies
├── package.json # Dev tooling
└── phpunit.xml.dist # Test configuration
```

### Design Patterns

#### 1. Controller Layer

All API endpoints are implemented as controller methods extending Drupal's `ControllerBase`:

```php
class UsersController extends ControllerBase
{
public function users(Request $request): JsonResponse
{
// Handle paginated user listing
}

public function login(Request $request): JsonResponse
{
// Authenticate and return bearer token
}
}
```

#### 2. Service Layer

Business logic is encapsulated in helper services registered in `mantle2.services.yml`:

- **GeneralHelper**: Common utilities (pagination, validation)
- **UsersHelper**: User operations and authentication
- **ActivityHelper**: Activity-related business logic
- **PointsHelper**: Points, badges, cosmetics, and quest lifecycle
- **OAuthHelper**: OAuth token validation and provider linking
- **CampaignHelper**: Email campaign content and placeholder expansion
- **CloudHelper**: Cloud service requests and websocket notifications
- **RedisHelper**: Cache abstraction with fallback

#### 3. Domain Models

Custom PHP classes in `src/Custom/` represent business entities:

- Implement `JsonSerializable` for API responses
- Enforce validation in constructors
- Provide type-safe interfaces

```php
class Activity implements JsonSerializable
{
protected string $id;
protected string $name;
protected array $types = [];
protected ?string $description = null;

public const int MAX_TYPES = 5;

public function __construct(
string $id,
string $name,
array $types = [],
?string $description = null,
array $aliases = [],
array $fields = [],
) {
if (count($types) > self::MAX_TYPES) {
throw new InvalidArgumentException('Too many activity types');
}
// ... validation and initialization
}
}
```

#### 4. Event Subscribers

Symfony's event dispatcher handles cross-cutting concerns:

- **RateLimitSubscriber**: Pre-request rate limit enforcement
- **RateLimitResponseSubscriber**: Appends global and endpoint rate headers
- **CorsSubscriber**: CORS header injection
- **ApiExceptionSubscriber**: Global error handling
- **ResponseCacheSubscriber**: Config-driven read-through/invalidation caching
- **PostResponseSubscriber**: Post-response badge progress tracking

## Installation

### Prerequisites

- PHP 8.4 or higher
- Composer 2.x
- Drupal 11.3+ installed and configured
- Redis server (optional, recommended for production)
- SMTP server credentials for email functionality

### Steps

1. **Clone the repository** into your Drupal modules directory:

```bash
cd /path/to/drupal/modules/custom
git clone mantle2
cd mantle2
```

2. **Install PHP dependencies**:

```bash
composer install
```

3. **Enable required Drupal modules**:

```bash
drush en node user comment json_field key field options datetime smtp redis \
openid_connect -y
```

4. **Enable mantle2**:

```bash
drush en mantle2 -y
```

This runs the installation hooks in `mantle2.install` which:
- Creates custom content types (Activity, Event, Article, Prompt)
- Creates custom comment types (Activity Comments, Article Comments)
- Defines extensive custom fields with JSON storage
- Sets up user profile fields
- Configures field display settings

5. **Configure Redis** (optional):
Edit `settings.php`:

```php
$settings['redis.connection']['interface'] = 'PhpRedis';
$settings['redis.connection']['host'] = '127.0.0.1';
$settings['redis.connection']['port'] = 6379;
$settings['cache']['default'] = 'cache.backend.redis';
```

6. **Configure SMTP** (required for email features):
- Navigate to `/admin/config/system/smtp`
- Enter SMTP server credentials
- Test email delivery

7. **Clear cache**:

```bash
drush cr
```

### Verification

Access the API documentation:

- OpenAPI Schema: `https://your-domain.com/openapi`
- Swagger UI: `https://your-domain.com/swagger-ui`

Test a simple endpoint:

```bash
curl https://your-domain.com/v2/hello
```

## Core Components

### Controllers

#### UsersController

**Responsibilities:**

- User CRUD operations
- Authentication (token login/logout and provider-based OAuth)
- Profile management (photos, privacy, account tiers)
- Social graph (friends and circle management)
- Notifications, badges, and points
- Quest lifecycle and cosmetics
- Email verification, unsubscribe, and password reset flows
- Activity, prompt, article, and event associations

**Database Queries:**

- Uses both Entity API (`$storage->getQuery()`) and direct SQL (`Drupal::database()`)
- Random sorting implemented via `orderRandom()` for discovery features
- Supports search across multiple fields (username, first name, last name)

#### ActivityController

**Responsibilities:**

- Activity catalog management
- Type filtering and categorization
- Activity-user associations

**Key Features:**

- Supports up to 5 activity types per activity
- JSON field storage for flexible metadata
- Full-text search across name, description, aliases
- Randomized and deterministic list retrieval modes

#### EventsController

**Responsibilities:**

- Event lifecycle management
- Participation tracking
- Date-based filtering
- Event visibility and attendee list management
- Event image submission moderation

**Features:**

- Date range queries for event discovery
- RSVP/signup/leave participation management
- Event cancellation and uncancel flows
- Event type enumeration
- Geographic location and activity tagging support

#### PromptsController

**Responsibilities:**

- Prompt catalog and random prompt delivery
- User response collection
- Visibility-aware filtering and moderation

**Logic:**

- Tracks user responses
- Prevents duplicate responses per user per prompt
- Supports response update/delete and expiration checks

#### ArticlesController

**Responsibilities:**

- Article content management
- Content moderation
- Author attribution
- Expiration checks and quiz retrieval

**Features:**

- Rich text content support via JSON fields
- Tag/ocean metadata support
- Role-gated article creation and updates

### Services

#### GeneralHelper

**Utilities:**

- `paginatedParameters(Request)`: Validates and extracts pagination params
- `findOrdinal(array, enum)`: Maps enum values to database integers
- `validateJson(string)`: JSON validation
- Various format converters and validators

#### UsersHelper

**User Operations:**

- `getOwnerOfRequest(Request)`: Extract authenticated user from request
- `findBy(string)`: Flexible user lookup by ID/username/email
- `issueToken(UserInterface)`: Create bearer token with bounded session count
- `getUserByToken(string)`: Resolve and validate bearer tokens (with sliding expiry)
- `revokeToken(string)`: Revoke active authentication token
- Friend/circle relationship management

#### RedisHelper

**Caching Abstraction:**

```php
RedisHelper::set(string $key, array $data, int $ttl = 900): bool
RedisHelper::get(string $key): ?array
RedisHelper::delete(mixed $key): bool
RedisHelper::exists(string $key): bool
RedisHelper::ttl(string $key): int
RedisHelper::list(string $pattern): array
RedisHelper::cache(?string $key, callable $callback, int $ttl = 900): array
```

**Features:**

- Automatic fallback to Drupal cache backend if Redis unavailable
- JSON serialization of complex data structures
- TTL support for automatic expiration
- Connection pooling via Drupal Redis module

**Usage Example:**

```php
// Store email verification code
RedisHelper::set(
"email_verify:{$userId}",
[
'code' => $code,
'email' => $email,
'created' => time(),
],
900,
); // 15 minutes TTL

// Retrieve and validate
$data = RedisHelper::get("email_verify:{$userId}");
if ($data && $data['code'] === $userInputCode) {
// Verify successful
RedisHelper::delete("email_verify:{$userId}");
}
```

#### HTMLFactory

**Email Rendering:**

- Converts markdown to HTML for email templates
- Applies consistent styling
- Handles inline CSS for email clients
- Supports template variables

### Domain Models

#### Activity

```php
class Activity implements JsonSerializable
{
protected string $id;
protected string $name;
protected array $types; // ActivityType[]
protected ?string $description;
protected array $aliases; // Alternative names
protected array $fields; // Custom metadata

public const int MAX_TYPES = 5;
}
```

#### Event

```php
class Event implements JsonSerializable
{
private string $id;
private int $hostId;
private string $name;
private string $description;
private EventType $type;
private array $activities; // Activity[]|ActivityType[]
private float $latitude;
private float $longitude;
private int $date; // Unix timestamp in milliseconds
private ?int $endDate;
private Visibility $visibility;
private array $attendees;
private array $fields;
}
```

#### Notification

```php
class Notification implements JsonSerializable
{
public string $id;
public string $userId;
public string $title;
public string $message;
public ?string $link;
public string $type; // info, warning, error, success
public string $source;
public bool $isRead;
public int $timestamp;
}
```

#### Enums (PHP 8.1+)

**AccountType:**

- `FREE`: Regular user
- `PRO`: Pro user tier
- `WRITER`: Writer privileges
- `ORGANIZER`: Organizer privileges
- `ADMINISTRATOR`: Administrative access

**Visibility:**

- `PUBLIC`: Visible to all
- `UNLISTED`: Hidden from broad listing, visible by context/owner
- `PRIVATE`: Only user

**Privacy:**

- `PRIVATE`, `CIRCLE`, `MUTUAL`, `PUBLIC` settings for profile field visibility

**ActivityType and EventType:**

- Enumerated types for categorization (notifications use string severity values)

### Event Subscribers

#### RateLimitSubscriber

**Configuration:**

```php
// Global limits
Authenticated: 120 requests / 60 seconds
Anonymous: 60 requests / 60 seconds

// Per-endpoint limits (examples)
POST /v2/users/login: 3 requests / 60 seconds
POST /v2/users/create: 5 requests / 5 minutes
POST /v2/events/create: 3 requests / 2 minutes
```

**Implementation:**

- Uses Drupal's expirable key-value store
- Separate counters for global and per-endpoint limits
- Environment variable overrides for global limits
- IP-based tracking (Cloudflare-aware)
- Returns `429 Too Many Requests` with retry headers

**Headers Added:**

```text
X-RateLimit-Limit: 3
X-RateLimit-Remaining: 2
X-RateLimit-Reset: 1698765432
X-Global-RateLimit-Limit: 120
X-Global-RateLimit-Remaining: 119
```

#### CorsSubscriber

**Allowed Origins:**

```php
[
'https://api.earth-app.com',
'https://earth-app.com',
'https://app.earth-app.com',
'https://cloud.earth-app.com',
'capacitor://localhost', // iOS
'http://localhost', // Android
'http://localhost:3000', // Development only
'http://127.0.0.1:3000', // Development only
'http://localhost:3001', // Development only
'http://127.0.0.1:3001', // Development only
];
```

**Headers Set:**

```text
Access-Control-Allow-Origin:
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, Accept,
X-Requested-With, X-Admin-Key
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600
Vary: Origin
```

#### ApiExceptionSubscriber

**Error Handling:**

- Catches unhandled exceptions in API routes
- Converts to consistent JSON error responses
- Logs errors with context
- Prevents sensitive information leakage in production

## Security & Performance

### Security Features

#### 1. Authentication & Authorization

- Bearer token authentication with issuance/revocation and sliding expiry
- OAuth provider sign-in/linking (Google, Microsoft, Discord, GitHub, Facebook)
- Password hashing using Drupal's password API (bcrypt)
- Password strength validation
- Email verification for account creation
- Admin key validation for privileged operations

#### 2. Input Validation

- Request parameter sanitization
- JSON schema validation
- SQL injection prevention via Entity API and parameterized queries
- XSS protection through Drupal's filtering system
- File upload validation (type, size, permissions)

#### 3. Rate Limiting

- IP-based rate limiting
- Separate limits for authenticated vs. anonymous users
- Per-endpoint rate limiting for sensitive operations
- Configurable time windows and thresholds
- Cloudflare IP detection support

#### 4. Privacy Controls

- User-level visibility settings (PUBLIC, UNLISTED, PRIVATE)
- Field-level privacy for profile attributes
- Friend/circle-based content filtering
- Profile photo access control

#### 5. CORS Protection

- Origin whitelist enforcement
- Credentials support for trusted domains
- Preflight request handling

### Performance Optimizations

#### 1. Caching Strategy

```php
// Redis/key-value for token and short-lived verification data
RedisHelper::set("email_verify:{$userId}", ['code' => $code], 900);

// Entity caching via Drupal cache tags
$users = $storage->loadMultiple($uids);

// Config-driven response caching (mantle2.caching.yml)
// ResponseCacheSubscriber sets X-Cache: HIT/MISS
```

#### 2. Database Optimization

- Indexed fields for common queries (username, email, ID)
- Pagination to limit result sets
- Direct SQL for complex queries (random sorting)
- Query object cloning for count queries to avoid duplication

#### 3. Lazy Loading

- User entities loaded on-demand
- Related entities fetched only when needed
- JSON fields decoded on access

#### 4. Efficient Queries

```php
// Good: Load multiple entities at once
$users = $storage->loadMultiple($uids);

// Good: Paginated with limit
$query->range($offset, $limit);

// Good: Specific field loading
$query->fields('u', ['uid', 'name', 'mail']);
```

### Monitoring & Logging

**Drupal Logger Integration:**

```php
Drupal::logger('mantle2')->error('Error message', ['context' => $data]);
Drupal::logger('mantle2')->warning('Warning message');
Drupal::logger('mantle2')->info('Info message');
```

**Logged Events:**

- Failed login attempts
- Rate limit violations
- Email delivery failures
- Redis connection issues
- API exceptions
- User registrations
- Password changes

## Development

### Local Setup

1. **Install dependencies:**

```bash
composer install
bun install
```

2. **Configure local environment:**

```php
// settings.local.php
$config['system.logging']['error_level'] = 'verbose';
$settings['redis.connection']['host'] = 'localhost';
```

3. **Enable development modules:**

```bash
drush en devel devel_generate dblog -y
```

4. **Generate test data:**

```bash
drush devel-generate-users 50
drush devel-generate-content 100 --types=activity,event,article
```

### Code Quality

**Formatting:**

```bash
# PHP, YAML, XML, JSON
bun run prettier # Format all files
bun run prettier:check # Check formatting

# PHP-specific
vendor/bin/phpcbf # Auto-fix coding standards
vendor/bin/phpcs # Check coding standards
```

**Static Analysis:**

```bash
vendor/bin/phpstan analyse src/
```

**Pre-commit Hooks:**
Configured via Husky and lint-staged in `package.json`:

```json
{
"lint-staged": {
"*.{php,xml,dist,json,install,module,yml,md}": "prettier --write"
}
}
```

### API Documentation

**Generate OpenAPI Schema:**
Visit `/openapi` to see auto-generated schema based on route definitions in `mantle2.routing.yml`.

**Interactive Swagger UI:**
Visit `/swagger-ui` for interactive API testing and documentation.

**Schema Annotations:**
Routes include OpenAPI metadata:

```yaml
mantle2.users:
path: '/v2/users'
options:
tags: Users
description: Retrieves a list of Earth App users
schema/200: '#Users'
schema/400: Invalid Pagination Parameters
query: # Query parameter schema
limit:
type: integer
minimum: 1
maximum: 100
```

### Debugging

**Enable Verbose Errors:**

```php
// settings.local.php
$config['system.logging']['error_level'] = 'verbose';
error_reporting(E_ALL);
ini_set('display_errors', true);
```

**Database Queries:**

```bash
drush watchdog:show --type=mantle2
drush ws --tail # Live log tail
```

**Clear Caches:**

```bash
drush cr # Full cache rebuild
drush cc views # Clear specific bin
drush redis-cli flushall # Clear Redis
```

## Testing

### PHPUnit Configuration

**Location:** `phpunit.xml.dist`

**Run Tests:**

```bash
# All tests
vendor/bin/phpunit

# Specific test file
vendor/bin/phpunit tests/src/Unit/GeneralUnitTest.php

# With coverage
vendor/bin/phpunit --coverage-html coverage/
```

### Unit Tests

**Test Structure:**

```php
namespace Drupal\Tests\mantle2\Unit;

use PHPUnit\Framework\TestCase;
use Drupal\mantle2\Service\GeneralHelper;

class GeneralUnitTest extends TestCase
{
public function testFormatId()
{
$this->assertEquals('000000000000000000000123', GeneralHelper::formatId(123));
}
}
```

### Mocks

**Location:** `tests/src/Mocks.php`

Provides test fixtures for:

- User entities
- Activity nodes
- Event nodes
- Request objects
- Service mocks

### Integration Testing

**Use Drush:**

```bash
# Test API endpoints
drush php-eval "print_r(\Drupal::service('http_kernel')->handle(Request::create('/v2/hello')));"

# Test services
drush php-eval "print(\Drupal\mantle2\Service\GeneralHelper::formatId(123));"
```

### API Testing

**Using cURL:**

```bash
# Health check
curl http://localhost/v2/hello

# Login
curl -X POST http://localhost/v2/users/login \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"testpass"}'

# Get users (authenticated)
curl http://localhost/v2/users \
-H "Authorization: Bearer "
```

**Using Postman/Insomnia:**
Import the OpenAPI schema from `/openapi` for automatic request generation.

## License

See [LICENSE](LICENSE) file for details.

## Contributing

1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Run tests and formatting (`vendor/bin/phpunit && bun run prettier:check`)
5. Push to the branch (`git push origin feature/amazing-feature`)
6. Open a Pull Request

## Support

For issues, questions, or contributions, please open an issue on the GitHub repository.

---

Built with ❤️ for The Earth App