Skip to content

earth-app/mantle2

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

420 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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

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

{
	"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

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:

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
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:

    cd /path/to/drupal/modules/custom
    git clone <repository-url> mantle2
    cd mantle2
  2. Install PHP dependencies:

    composer install
  3. Enable required Drupal modules:

    	drush en node user comment json_field key field options datetime smtp redis \
    		openid_connect -y
  4. Enable mantle2:

    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:

    $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:

    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:

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:

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:

// 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

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

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

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:

// 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:

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:

[
	'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:

Access-Control-Allow-Origin: <matched-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

// 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

// 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:

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:

    composer install
    bun install
  2. Configure local environment:

    // settings.local.php
    $config['system.logging']['error_level'] = 'verbose';
    $settings['redis.connection']['host'] = 'localhost';
  3. Enable development modules:

    drush en devel devel_generate dblog -y
  4. Generate test data:

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

Code Quality

Formatting:

# 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:

vendor/bin/phpstan analyse src/

Pre-commit Hooks: Configured via Husky and lint-staged in package.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:

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:

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

Database Queries:

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

Clear Caches:

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:

# 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:

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:

# 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:

# 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 <token>"

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

License

See 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

Sponsor this project

Contributors