# Headless Drupal Stack

Drupal 11 backend for Next.js frontend with JSON:API, OAuth authentication, and Redis caching.

## Quick Start

### 1. Setup Shell Aliases (Optional but Recommended)
```bash
cd /var/deploy/drupal
./setup-aliases.sh
source ~/.bash-aliases
```

This creates convenient shortcuts in `~/.bash-aliases`:
- `ddrush` - Run Drush commands
- `dcomposer` - Run Composer commands
- `dphp` - Run PHP commands
- `dshell` - Open bash shell in container
- `dlogs` - View PHP-FPM logs (follow mode)
- `dredis` - Access Redis CLI

### 2. Start Services
```bash
cd /var/deploy
docker compose -f drupal/docker-compose.yml up -d
```

### 3. Install Headless Modules
```bash
cd /var/deploy/drupal
./install-headless-modules.sh
```

### 4. Enable Core Modules
```bash
# With alias
ddrush en next jsonapi_extras simple_oauth consumers pathauto cors decoupled_router -y

# Without alias
docker exec drupal-fpm bash -c "cd /opt/drupal && drush en next jsonapi_extras simple_oauth consumers pathauto cors decoupled_router -y"
```

### 5. Generate OAuth Keys
```bash
docker exec drupal-fpm bash -c 'cd /opt/drupal && mkdir -p oauth/keys && openssl genrsa -out oauth/keys/private.key 2048 && openssl rsa -in oauth/keys/private.key -pubout -out oauth/keys/public.key && chown www-data:www-data oauth/keys/private.key oauth/keys/public.key && chmod 640 oauth/keys/private.key oauth/keys/public.key'
```
(PHP-FPM runs as www-data; keys must be readable by it.)

Add to `drupal/root/web/sites/default/settings.php`:
```php
$settings['simple_oauth.keys'] = [
  'public' => '/opt/drupal/oauth/keys/public.key',
  'private' => '/opt/drupal/oauth/keys/private.key',
];
```

### 6. Configure Redis Cache
Add to `drupal/root/web/sites/default/settings.php`:
```php
// Redis Configuration.
$settings['redis.connection']['interface'] = 'PhpRedis';
$settings['redis.connection']['host'] = 'drupal-redis';
$settings['redis.connection']['port'] = 6379;
$settings['cache']['default'] = 'cache.backend.redis';
$settings['cache']['bins']['bootstrap'] = 'cache.backend.chainedfast';
$settings['cache']['bins']['discovery'] = 'cache.backend.chainedfast';
$settings['cache']['bins']['config'] = 'cache.backend.chainedfast';
```

## Stack Components

### Containers
- **drupal-reverse-proxy**: NGINX reverse proxy (port 80)
- **drupal-fpm**: PHP 8.4 FPM with Drupal 11
- **drupal-redis**: Redis 7 cache backend

### Key Modules
- `drupal/next` - Next.js integration
- `drupal/jsonapi_extras` - JSON:API enhancements
- `drupal/simple_oauth` - OAuth 2.0 authentication
- `drupal/pathauto` - URL aliases
- `drupal/cors` - CORS headers
- `drupal/decoupled_router` - Route resolution

## Common Commands

### Shell Aliases (Recommended)
After running `./setup-aliases.sh`:

```bash
# Composer commands
dcomposer require drupal/module_name
dcomposer update
dcomposer show

# Drush commands
ddrush cr                    # Clear cache
ddrush en module_name -y     # Enable module
ddrush updb -y               # Database updates
ddrush cex -y                # Export configuration
ddrush cim -y                # Import configuration
ddrush status                # Site status

# PHP commands
dphp -v                      # PHP version
dphp -m                      # PHP modules
dphp -i                      # PHP info

# Container access
dshell                       # Open bash shell
dlogs                        # View logs
dredis ping                  # Test Redis
```

### Without Aliases (Full Commands)

#### Composer
```bash
# Install module
docker exec drupal-fpm bash -c "cd /opt/drupal && composer require drupal/module_name"

# Update dependencies
docker exec drupal-fpm bash -c "cd /opt/drupal && composer update"

# Show installed packages
docker exec drupal-fpm bash -c "cd /opt/drupal && composer show"
```

#### Drush
```bash
# Clear cache
docker exec drupal-fpm bash -c "cd /opt/drupal && drush cr"

# Enable module
docker exec drupal-fpm bash -c "cd /opt/drupal && drush en module_name -y"

# Database updates
docker exec drupal-fpm bash -c "cd /opt/drupal && drush updb -y"

# Export configuration
docker exec drupal-fpm bash -c "cd /opt/drupal && drush cex -y"

# Import configuration
docker exec drupal-fpm bash -c "cd /opt/drupal && drush cim -y"

# Site status
docker exec drupal-fpm bash -c "cd /opt/drupal && drush status"
```

### Container Management
```bash
# View logs
docker logs drupal-fpm
docker logs drupal-reverse-proxy

# Access container shell
docker exec -it drupal-fpm bash

# Restart services
docker compose -f drupal/docker-compose.yml restart

# Rebuild container
docker compose -f drupal/docker-compose.yml up -d --build
```

### Redis
```bash
# With alias
dredis ping                  # Test connection
dredis monitor               # Monitor Redis
dredis keys '*'              # Check keys
dredis flushall              # Clear cache

# Without alias
docker exec drupal-redis redis-cli ping
docker exec drupal-redis redis-cli monitor
docker exec drupal-redis redis-cli keys '*'
docker exec drupal-redis redis-cli flushall
```

## Configuration

### Next.js Site Configuration
1. Navigate to `/admin/config/services/next`
2. Add Next.js site:
   - Label: Production Frontend
   - Base URL: `https://nasarek.dev`
   - Preview URL: `https://nasarek.dev/api/preview`
   - Preview Secret: Generate secure random string
3. The env page (`/admin/config/services/next/sites/{site_id}/env`) shows a template; `DRUPAL_CLIENT_ID` and `DRUPAL_CLIENT_SECRET` will say "Retrieve this from /admin/config/services/consumer" — get the actual Client ID and secret from the OAuth consumer (see OAuth Consumer section below).

**Preview secret expired:** If you see "The provided secret has expired", increase the secret lifetime (default is 30 seconds):
- **Via UI:** `/admin/config/services/next/settings` → Preview URL generator → Simple OAuth → Secret expiration time → set to 300 (5 min) or 3600 (1 h)
- **Via Drush:** `ddrush cset next.settings preview_url_generator_configuration.secret_expiration 300 -y`

### CORS Configuration
1. Navigate to `/admin/config/services/cors`
2. Add configuration:
   - Enabled: ✓
   - Domains: `https://nasarek.dev|*`
   - Methods: `GET|POST|OPTIONS`
   - Headers: `Authorization|Content-Type|*`
   - Credentials: ✓

### OAuth Consumer

**1. Create a dynamic scope** (required; Drupal rejects token requests without a valid scope):
- Navigate to `/admin/config/people/simple_oauth/oauth2_scope/dynamic/add`
- Role: Authenticated user (or a dedicated role)
- Granularity: Role
- Grant Types: Enable **Client credentials**
- Description: e.g. Next.js Frontend
- Machine-readable name: e.g. `nextjs_frontend`
- Save

**2. Create or edit a consumer:**
- Navigate to `/admin/config/services/consumer` (or add at `/admin/config/services/consumer/add`)
- Label: e.g. Next.js Frontend (or use existing default_consumer)
- **Client ID**: Use the **Client ID** column value from the consumer list (e.g. `lcXmAvaCtkjDhgKsLeAfUucOibVwUxKXtla8b_4CC4w`), **not** the UUID or label
- **Secret**: Not shown in the list; edit the consumer to generate or regenerate it
- Grant Types: Enable **Client credentials**
- Scopes: Select the scope created in step 1 (e.g. `nextjs_frontend`)
- Redirect URI: `https://nasarek.dev`
- Is 3rd party?: Leave **unchecked** (Next.js is your own frontend)
- Save

**3. Add to `nextjs/.env.local`:**
```
DRUPAL_CLIENT_ID=<Client ID from the consumer list>
DRUPAL_CLIENT_SECRET=<Secret from the consumer edit form>
DRUPAL_OAUTH_SCOPE=<machine name from step 1, e.g. nextjs_frontend>
```

**4. Recreate Next.js container** (env vars are read at container start):
```bash
cd /var/deploy/drupal && docker compose -f docker-compose.yml up -d nextjs --force-recreate
```

**Verify OAuth:**
```bash
cd /var/deploy/drupal && docker compose -f docker-compose.yml exec nextjs node -e "
const b=process.env.NEXT_PUBLIC_DRUPAL_BASE_URL,i=process.env.DRUPAL_CLIENT_ID,s=process.env.DRUPAL_CLIENT_SECRET,scope=process.env.DRUPAL_OAUTH_SCOPE;
const body=new URLSearchParams({grant_type:'client_credentials',client_id:i,client_secret:s});
if(scope) body.set('scope',scope);
fetch(b+'/oauth/token',{method:'POST',body}).then(r=>r.json()).then(j=>console.log(j.access_token?'OK':JSON.stringify(j)));
"
```

### Next.js Environment Variables
Required in `nextjs/.env.local` (loaded via docker-compose `env_file`):

| Variable | Required | Description |
|----------|----------|-------------|
| `NEXT_PUBLIC_DRUPAL_BASE_URL` | Yes | Drupal JSON:API base URL (e.g. `https://cms.nasarek.dev`) |
| `DRUPAL_CLIENT_ID` | Yes* | OAuth consumer Client ID (from consumer list, not UUID) |
| `DRUPAL_CLIENT_SECRET` | Yes* | OAuth consumer secret from edit form |
| `DRUPAL_OAUTH_SCOPE` | Yes* | OAuth scope machine name (e.g. `nextjs_frontend`) |
| `DRUPAL_PREVIEW_SECRET` | No | For draft/preview mode webhook |
| `DRUPAL_REVALIDATE_SECRET` | No | For on-demand revalidation webhook |

\* Without OAuth credentials, JSON:API requests run as anonymous; protected content will return "Access denied". Use `--force-recreate` after changing env vars.

### JSON:API Configuration
1. Navigate to `/admin/config/services/jsonapi/extras`
2. Configure resource types:
   - Disable unwanted resources
   - Rename fields for frontend convenience
   - Configure includes and sparse fieldsets

## JSON:API Endpoints

### Examples
```bash
# Get all articles
curl https://cms.nasarek.dev/jsonapi/node/article

# Get specific article
curl https://cms.nasarek.dev/jsonapi/node/article/{uuid}

# Filter by status
curl https://cms.nasarek.dev/jsonapi/node/article?filter[status]=1

# Include related entities
curl https://cms.nasarek.dev/jsonapi/node/article?include=field_image,uid

# Sparse fieldsets
curl https://cms.nasarek.dev/jsonapi/node/article?fields[node--article]=title,body,created

# Sort and paginate
curl https://cms.nasarek.dev/jsonapi/node/article?sort=-created&page[limit]=10&page[offset]=0
```

### With Authentication
```bash
# Get access token (client credentials - used by next-drupal)
curl -X POST https://cms.nasarek.dev/oauth/token \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_UUID" \
  -d "client_secret=YOUR_SECRET"

# Use token
curl https://cms.nasarek.dev/jsonapi/node/article \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

## Debugging

### Enable Xdebug
Xdebug is pre-configured and enabled:
- Mode: debug
- Client host: host.docker.internal
- Client port: 9000
- IDE key: VSCODE
- Log: `/var/log/xdebug.log`

### View Logs
```bash
# PHP-FPM logs
docker logs drupal-fpm

# NGINX logs
docker logs drupal-reverse-proxy

# Xdebug log
docker exec drupal-fpm tail -f /var/log/xdebug.log
```

### Check PHP Configuration
```bash
# PHP version
docker exec drupal-fpm php -v

# PHP modules
docker exec drupal-fpm php -m

# PHP info
docker exec drupal-fpm php -i

# OPcache status
docker exec drupal-fpm php -r "echo json_encode(opcache_get_status(), JSON_PRETTY_PRINT);"
```

## Troubleshooting

### Module not found
```bash
# Check if module is installed
dcomposer show drupal/module_name

# Install if missing
dcomposer require drupal/module_name
```

### Clear all caches
```bash
# Drupal cache
ddrush cr

# Redis cache
docker exec drupal-redis redis-cli flushall

# OPcache (restart PHP-FPM)
docker compose -f drupal/docker-compose.yml restart drupal-fpm
```

### Permissions issues
```bash
# Fix file permissions
docker exec drupal-fpm chown -R www-data:www-data /opt/drupal/web/sites/default/files

# Settings.php permissions
docker exec drupal-fpm chmod 444 /opt/drupal/web/sites/default/settings.php
```

## File Structure
```
drupal/
├── docker-compose.yml          # Service definitions
├── install-headless-modules.sh # Module installation script
├── drupal/
│   ├── Dockerfile             # PHP-FPM image
│   └── root/                  # Drupal codebase (mounted volume)
│       ├── composer.json
│       ├── web/               # Document root
│       │   ├── core/
│       │   ├── modules/
│       │   │   ├── contrib/
│       │   │   └── custom/
│       │   ├── themes/
│       │   └── sites/
│       └── oauth/
│           └── keys/          # OAuth RSA keys
└── nginx/
    ├── Dockerfile
    └── nginx.conf.template    # NGINX configuration
```

## Development Workflow

1. Make changes to code in `drupal/root/`
2. Clear cache: `ddrush cr` (or `docker exec drupal-fpm bash -c "cd /opt/drupal && drush cr"`)
3. Export config: `ddrush cex -y` (or `docker exec drupal-fpm bash -c "cd /opt/drupal && drush cex -y"`)
4. Commit changes to git
5. Deploy: Pull changes and run `ddrush cim -y` on production

## Production Checklist

- [ ] Set `opcache.validate_timestamps=0` in production
- [ ] Configure Redis cache
- [ ] Generate OAuth keys
- [ ] Configure CORS restrictively
- [ ] Set up content revalidation webhooks
- [ ] Configure rate limiting
- [ ] Enable HTTPS only
- [ ] Set secure session cookies
- [ ] Configure backup strategy
- [ ] Monitor logs and performance
- [ ] Test JSON:API endpoints with different user roles

## Resources

- [Next.js for Drupal](https://next-drupal.org/)
- [JSON:API Documentation](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module)
- [Simple OAuth](https://www.drupal.org/project/simple_oauth)
- [Drupal.org](https://www.drupal.org/)
