# LUVEEDU Storage Manager API — Documentation

Admin middleware for Hetzner Storage Boxes. All endpoints proxy to the Hetzner Cloud API (`https://api.hetzner.com/v1`) with our own authentication, rate limiting, and audit logging.

**Base URL:** `https://storage.luveedu.cloud/api`

---

## Quick Facts

```yaml
service: storage-manager-api
version: 1.0.0
auth: admin API key + IP allowlist
admin_key: storagemgr_admin_4cfb33237d1a5af5cc81aa776ff20f4aa714742b7ef4b38e
allowed_ips: [171.50.171.29, 188.245.148.184, 157.90.244.133]
rate_limits:
  dos_guard: 100 req/s sustained 10s -> IP blocked 1h
  attacker_guard: 10 failures/min -> IP blocked 1h
  per_key: 200 req/min -> key blocked 5min
hetzner_api: https://api.hetzner.com/v1
```

---

## 1. Authentication

All admin endpoints require:

```
X-API-Key: storagemgr_admin_4cfb33237d1a5af5cc81aa776ff20f4aa714742b7ef4b38e
X-User-Id: <customer user_id>
X-Username: <customer username>
```

Plus caller IP must be in the allowlist (configured in `settings.allowed_ips`). The `/api/health` endpoint is public.

**Owner scoping:** Every storage box is owned by a `(user_id, username)` pair. All endpoints require owner credentials and only return/modify boxes belonging to that owner. Cross-owner access returns `404` (not `403`, to avoid leaking box existence).

---

## 2. Endpoints

### 2.1 `POST /api/create`

Create a new storage box.

**Request body:**

```json
{
  "name": "my-storage-box",
  "location": "fsn1",
  "storage_box_type": "bx20",
  "password": "SecurePassword123!",
  "labels": {"environment": "prod"},
  "ssh_keys": ["ssh-rsa AAAAB3NzaC1yc2E..."],
  "reachable_externally": true,
  "samba_enabled": true,
  "ssh_enabled": true,
  "webdav_enabled": true,
  "zfs_enabled": true
}
```

**Response (200):**

```json
{
  "storage_box": {
    "id": 42,
    "name": "my-storage-box",
    "username": "u12345",
    "server": "u42.your-storagebox.de",
    "status": "initializing",
    "storage_box_type": {"id": 1, "name": "bx20", "size": 1073741824},
    "location": {"id": 1, "name": "fsn1", "country": "DE", "city": "Falkenstein"},
    "access_settings": {"reachable_externally": true, "samba_enabled": true, ...},
    "stats": {"size": 0, "size_data": 0, "size_snapshots": 0},
    "created": "2026-01-30T23:50:00Z"
  },
  "action": {
    "id": 13,
    "command": "create",
    "status": "running",
    "progress": 0,
    "started": "2026-01-30T23:50:00Z"
  }
}
```

**Validation:**
- `name`: 1-255 characters, required
- `password`: 8-128 characters, required
- `location`: default `fsn1` (Falkenstein)
- `storage_box_type`: default `bx20`

---

### 2.2 `GET /api/status/{id}`

Get current state of a storage box.

**Response (200):**

```json
{
  "id": 42,
  "name": "my-storage-box",
  "username": "u12345",
  "server": "u42.your-storagebox.de",
  "status": "active",
  "storage_box_type": {"id": 1, "name": "bx20", "size": 1073741824},
  "location": {"id": 1, "name": "fsn1"},
  "access_settings": {...},
  "protection": {"delete": false},
  "stats": {"size": 1073741824, "size_data": 536870912, "size_snapshots": 0},
  "labels": {"environment": "prod"},
  "created": "2026-01-30T23:50:00Z"
}
```

**Errors:**
- `404` — Box not found

---

### 2.3 `DELETE /api/delete/{id}`

Delete a storage box.

**Response (200):**

```json
{
  "deleted": 42,
  "action": {
    "id": 14,
    "command": "delete",
    "status": "running",
    "progress": 0,
    "started": "2026-01-30T23:50:00Z"
  }
}
```

**Errors:**
- `404` — Box not found

---

### 2.4 `GET /api/list`

List all storage boxes.

**Query parameters:**
- `page` (default: 1)
- `per_page` (default: 50, max: 100)

**Response (200):**

```json
{
  "page": 1,
  "per_page": 50,
  "count": 2,
  "boxes": [
    {"id": 42, "name": "box-1", "status": "active", ...},
    {"id": 43, "name": "box-2", "status": "initializing", ...}
  ]
}
```

---

### 2.5 `GET /api/list-folders/{id}`

List folders in a storage box.

**Query parameters:**
- `path` (default: `./`)

**Response (200):**

```json
{
  "id": 42,
  "path": "./documents",
  "folders": ["offsite-backup", "photos"]
}
```

---

### 2.6 `POST /api/reset-password`

Reset the password for a storage box.

**Request body:**

```json
{
  "id": 42,
  "password": "NewSecurePassword456!"
}
```

**Response (200):**

```json
{
  "id": 42,
  "action": {
    "id": 15,
    "command": "reset_password",
    "status": "running",
    "progress": 0,
    "started": "2026-01-30T23:50:00Z"
  }
}
```

**Validation:**
- `password`: 8-128 characters, required

---

### 2.7 `POST /api/update`

Update access settings for a storage box. Only the fields you include are changed; omitted fields keep their current values.

**Request body:**

```json
{
  "id": 42,
  "reachable_externally": true,
  "samba_enabled": false,
  "ssh_enabled": true,
  "webdav_enabled": true,
  "zfs_enabled": true
}
```

All five access settings are optional, but **at least one must be provided** (otherwise `400`).

**Response (200):**

```json
{
  "id": 42,
  "action": {
    "id": 16,
    "command": "update_access_settings",
    "status": "running",
    "progress": 0,
    "started": "2026-01-30T23:50:00Z"
  },
  "applied": {
    "samba_enabled": false,
    "webdav_enabled": true
  }
}
```

The `applied` field echoes exactly which settings were sent to Hetzner. The new values are also mirrored into the local DB so `/api/list` reflects them immediately.

---

### 2.8 `GET /api/sync`

Bulk export of boxes, actions, and audit log for frontend mirroring.

**Query parameters:**
- `since` (optional, ISO datetime): only rows updated after this timestamp

**Response (200):**

```json
{
  "service": "storage-manager",
  "server_time": "2026-08-25T07:41:00.000000",
  "counts": {"boxes": 2, "actions": 5, "audit": 12},
  "boxes": [...],
  "actions": [...],
  "audit": [...]
}
```

---

### 2.9 `GET /api/health` — public

Health check endpoint (no auth required).

**Response (200):**

```json
{
  "status": "healthy",
  "service": "storage-manager-api",
  "version": "1.0.0",
  "timestamp": "2026-08-25T07:41:00.000000"
}
```

---

## 3. Error Responses

All errors return:

```json
{"error": "<message>", "status_code": <code>}
```

Rate limit errors additionally include:

```json
{"error": "...", "status_code": 429, "reason": "...", "retry_after": 3600}
```

| Code | Trigger |
|------|---------|
| `400` | Invalid request body / validation failure |
| `401` | Missing or invalid admin API key |
| `403` | Caller IP not in allowlist |
| `404` | Storage box not found |
| `429` | Rate limit exceeded (DoS / attacker / per-key) |
| `500` | Internal server error |
| `502` | Hetzner API unreachable |

---

## 4. Rate Limits

All limits enforced app-side, return `429` with `Retry-After` header.

| Tier | Trigger | Scope | Action |
|------|---------|-------|--------|
| DoS guard | ≥100 req/s sustained ≥10s | IP | blocked 1 hour |
| Attacker guard | >10 failed requests/min | IP | blocked 1 hour |
| Per-key abuse | >200 requests/min | API key | blocked 5 minutes |

---

## 5. Integration Example

```bash
# Create a storage box for customer (user_id=100, username=alice)
curl -X POST https://storage.luveedu.cloud/api/create \
  -H "X-API-Key: storagemgr_admin_4cfb33237d1a5af5cc81aa776ff20f4aa714742b7ef4b38e" \
  -H "X-User-Id: 100" \
  -H "X-Username: alice" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-backup-box",
    "location": "fsn1",
    "storage_box_type": "bx20",
    "password": "SecurePass123!"
  }'

# List alice's boxes only
curl https://storage.luveedu.cloud/api/list \
  -H "X-API-Key: storagemgr_admin_4cfb33237d1a5af5cc81aa776ff20f4aa714742b7ef4b38e" \
  -H "X-User-Id: 100" \
  -H "X-Username: alice"

# Get box status (alice's box only)
curl https://storage.luveedu.cloud/api/status/42 \
  -H "X-API-Key: storagemgr_admin_4cfb33237d1a5af5cc81aa776ff20f4aa714742b7ef4b38e" \
  -H "X-User-Id: 100" \
  -H "X-Username: alice"

# Reset password
curl -X POST https://storage.luveedu.cloud/api/reset-password \
  -H "X-API-Key: storagemgr_admin_4cfb33237d1a5af5cc81aa776ff20f4aa714742b7ef4b38e" \
  -H "X-User-Id: 100" \
  -H "X-Username: alice" \
  -H "Content-Type: application/json" \
  -d '{"id": 42, "password": "NewSecurePass456!"}'

# Delete box
curl -X DELETE https://storage.luveedu.cloud/api/delete/42 \
  -H "X-API-Key: storagemgr_admin_4cfb33237d1a5af5cc81aa776ff20f4aa714742b7ef4b38e" \
  -H "X-User-Id: 100" \
  -H "X-Username: alice"
```

---

## 6. Database Schema

The service mirrors Hetzner state into a local MariaDB database for fast queries and audit trail:

- `storage_boxes` — current state of all boxes (mirrored from Hetzner)
- `storage_actions` — async action history (create/delete/reset_password)
- `storage_audit` — admin action log (who did what when)
- `settings` — configuration (IP allowlist, etc.)

---

## 7. Deployment

- **Service:** systemd unit `storage-manager-api.service`
- **Port:** 5410 (localhost only, nginx reverse proxy)
- **Logs:** `/var/log/nginx/storage.luveedu.cloud.{access,error}.log`
- **SSL:** Let's Encrypt (auto-renewed)
- **Rate limit zone:** `storage_api_limit` (300 r/s nginx backstop)

---

## Support

For issues or questions, contact the platform team.