# LUVEEDU VPS Manager API — Documentation

Region-routing middleware for cloud VPS provisioning. The middleware accepts a friendly region name (e.g. `germany`, `us-1`) and routes the request to the right upstream provider. Currently only Hetzner Cloud is wired; unwired regions are rejected with `400 provider_not_configured`.

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

---

## Quick Facts

```yaml
service: vps-manager-api
version: 1.0.0
auth: admin API key + IP allowlist + owner headers
admin_key: vpsmgr_admin_f37cabf0cae04df864991dca44e8a20d4f2dcc7c8321e4be
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
upstream: https://api.hetzner.cloud/v1
```

---

## 1. Authentication

All admin endpoints require:

```
X-API-Key: vpsmgr_admin_f37cabf0cae04df864991dca44e8a20d4f2dcc7c8321e4be
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 VPS is owned by a `(user_id, username)` pair. All endpoints require owner credentials and only return/modify servers belonging to that owner. Cross-owner access returns `404` (not `403`, to avoid leaking server existence).

---

## 2. Region Routing

| Friendly region | Provider | Datacenter |
|-----------------|----------|------------|
| `us-1`          | hetzner  | ash        |
| `us-2`          | hetzner  | hil        |
| `germany`       | hetzner  | fsn1       |
| `finland`       | hetzner  | hel1       |
| `singapore`     | hetzner  | sin        |
| `uk`            | unwired  | —          |
| `japan`         | unwired  | —          |
| `australia`     | unwired  | —          |

Unwired regions return `400 provider_not_configured`. New providers can be added by editing `LOCATION_ROUTES` in `/etc/vps-manager.env`.

---

## 3. Endpoints

### 3.1 `GET /api/locations`

List all regions and their routing.

**Response (200):**

```json
{
  "locations": [
    {"region": "us-1", "provider": "hetzner", "datacenter": "ash"},
    {"region": "germany", "provider": "hetzner", "datacenter": "fsn1"},
    {"region": "uk", "provider": "unwired", "datacenter": null}
  ]
}
```

---

### 3.2 `GET /api/server-types`

List available Hetzner server types (cx22, cpx22, ccx63, etc.).

**Response (200):**

```json
{"server_types": [{"id": 1, "name": "cx22", "description": "CX22", "cores": 2, "memory": 4, "disk": 40, ...}]}
```

---

### 3.3 `GET /api/images`

List installable OS images.

**Query parameters:**
- `type` (default: `system`) — `system`, `snapshot`, `backup`, `app`

**Response (200):**

```json
{"images": [{"id": 1, "type": "system", "name": "ubuntu-24.04", ...}]}
```

---

### 3.4 `GET /api/ssh-keys`

List SSH keys registered with Hetzner.

**Response (200):**

```json
{"ssh_keys": [{"id": 1, "name": "my-key", "fingerprint": "..."}]}
```

---

### 3.5 `POST /api/create`

Create a new VPS.

**Request body:**

```json
{
  "name": "my-server",
  "region": "germany",
  "server_type": "cx22",
  "image": "ubuntu-24.04",
  "ssh_keys": ["my-key"],
  "start_after_create": true,
  "labels": {"environment": "prod"},
  "user_data": "#cloud-config\n..."
}
```

**Validation:**
- `name`: 1-63 chars, RFC 1123 hostname (letters, digits, dashes)
- `region`: must be a wired region (see §2)
- `server_type`: name or ID from `/server-types`
- `image`: name or ID from `/images`

**Response (201):**

```json
{
  "server": {
    "id": 4242,
    "name": "my-server",
    "status": "initializing",
    "public_net": {"ipv4": {"ip": "1.2.3.4"}, "ipv6": {"ip": "2001:db8::1"}},
    "server_type": {"name": "cx22"},
    "location": {"name": "fsn1"},
    "image": {"name": "ubuntu-24.04"}
  },
  "next_actions": [...],
  "root_password": "YItygq1v3GYjjMomLaKc"
}
```

---

### 3.6 `GET /api/list`

List all VPS instances owned by the calling user.

**Response (200):**

```json
{
  "count": 2,
  "servers": [
    {"id": 4242, "name": "my-server", "status": "running", "region": "germany", "datacenter": "fsn1", ...}
  ]
}
```

---

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

Get current state of a VPS. Refreshes the local mirror from Hetzner.

**Response (200):**

```json
{
  "id": 4242,
  "name": "my-server",
  "status": "running",
  "public_net": {"ipv4": {"ip": "1.2.3.4"}, "ipv6": {"ip": "2001:db8::1"}},
  "server_type": {"name": "cx22"},
  "location": {"name": "fsn1"},
  "image": {"name": "ubuntu-24.04"},
  "protection": {"delete": false, "rebuild": false},
  "labels": {"environment": "prod"}
}
```

**Errors:**
- `404` — VPS not found (or not owned by caller)

---

### 3.8 `PUT /api/update/{id}`

Update name, labels, or protection flags.

**Request body:**

```json
{
  "id": 4242,
  "name": "my-server-renamed",
  "labels": {"environment": "staging"},
  "protection_delete": true,
  "protection_rebuild": true
}
```

All fields optional, but **at least one must be provided** (otherwise `400`).

**Response (200):**

```json
{
  "id": 4242,
  "name": "my-server-renamed",
  "labels": {"environment": "staging"},
  "protection": {"delete": true, "rebuild": true}
}
```

---

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

Delete a VPS. Detaches all volumes, IPs, firewalls, placement groups.

**Response (200):**

```json
{"deleted": 4242, "action": {"id": 100, "command": "delete", "status": "running"}}
```

---

### 3.10 Power & Lifecycle Actions

All return `{"id": <server_id>, "action": {...}}` on success.

| Endpoint | Action | Notes |
|----------|--------|-------|
| `POST /api/poweron/{id}` | Power on | ACPI start |
| `POST /api/poweroff/{id}` | Hard power off | Cuts power; data loss risk |
| `POST /api/reboot/{id}` | Soft reboot | ACPI request |
| `POST /api/shutdown/{id}` | Graceful shutdown | ACPI shutdown |
| `POST /api/reset-password/{id}` | Reset root password | Linux + qemu guest agent required |
| `POST /api/rebuild/{id}` | Rebuild from image | Optional `?image=<name>` query param |

---

### 3.11 `GET /api/metrics/{id}`

Get CPU / disk / network metrics for a VPS.

**Query parameters:**
- `type` (default: `cpu,disk,network`) — comma-separated
- `start` (RFC3339)
- `end` (RFC3339)

**Response (200):**

```json
{
  "metrics": {
    "start": "2026-01-01T00:00:00Z",
    "end": "2026-01-01T01:00:00Z",
    "step": 60,
    "cpu": {"values": [[1704067200, 0.1]], "average": 0.1},
    "disk": {"values": [], "average": null}
  }
}
```

---

### 3.12 `GET /api/action/{id}`

Poll status of any async action (create, poweron, rebuild, etc.).

**Response (200):**

```json
{
  "id": 100,
  "command": "create_server",
  "status": "success",
  "progress": 100,
  "started": "2026-01-01T00:00:00Z",
  "finished": "2026-01-01T00:00:01Z",
  "error": null,
  "resources": [{"id": 4242, "type": "server"}]
}
```

---

### 3.13 `POST /api/volume/create`

Create a block-storage volume.

**Request body:**

```json
{
  "name": "vol1",
  "region": "germany",
  "size_gb": 50,
  "format": "ext4",
  "server_id": 4242
}
```

**Validation:**
- `size_gb`: 10-10240
- `format`: `ext4` | `xfs` | `zfs` (default `ext4`)
- `region`: must be wired

**Response (201):**

```json
{
  "volume": {
    "id": 5001,
    "name": "vol1",
    "size": 50,
    "location": {"name": "fsn1"},
    "server": null,
    "format": "ext4",
    "status": "creating"
  },
  "next_actions": []
}
```

---

### 3.14 `GET /api/volume/list`

List volumes owned by the calling user.

**Response (200):**

```json
{"count": 1, "volumes": [{"id": 5001, "name": "vol1", "size_gb": 50, ...}]}
```

---

### 3.15 `DELETE /api/volume/delete/{id}`

Delete a volume.

**Response (200):**

```json
{"deleted": 5001}
```

---

### 3.16 `GET /api/sync`

Bulk export of servers, volumes, and regions for frontend mirroring.

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

**Response (200):**

```json
{
  "service": "vps-manager",
  "server_time": "2026-08-25T08:00:00.000000",
  "counts": {"servers": 2, "volumes": 1},
  "servers": [...],
  "volumes": [...],
  "regions": [{"region": "germany", "provider": "hetzner", "datacenter": "fsn1"}, ...]
}
```

---

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

Health check endpoint (no auth required).

**Response (200):**

```json
{
  "status": "healthy",
  "service": "vps-manager-api",
  "version": "1.0.0",
  "regions": ["us-1", "us-2", "germany", "finland", "singapore", "uk", "japan", "australia"],
  "timestamp": "2026-08-25T08:00:00.000000"
}
```

---

## 4. 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 / unwired region |
| `401` | Missing or invalid admin API key |
| `403` | Caller IP not in allowlist |
| `404` | VPS or volume not found (or not owned by caller) |
| `429` | Rate limit exceeded (DoS / attacker / per-key) |
| `500` | Internal server error |
| `502` | Hetzner API unreachable |

---

## 5. 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 |

---

## 6. Integration Example

```bash
# Create a VPS for customer (user_id=100, username=alice)
curl -X POST https://vps.luveedu.cloud/api/create \
  -H "X-API-Key: vpsmgr_admin_f37cabf0cae04df864991dca44e8a20d4f2dcc7c8321e4be" \
  -H "X-User-Id: 100" \
  -H "X-Username: alice" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "web-server",
    "region": "germany",
    "server_type": "cx22",
    "image": "ubuntu-24.04"
  }'

# List alice's servers
curl https://vps.luveedu.cloud/api/list \
  -H "X-API-Key: vpsmgr_admin_f37cabf0cae04df864991dca44e8a20d4f2dcc7c8321e4be" \
  -H "X-User-Id: 100" \
  -H "X-Username: alice"

# Power on
curl -X POST https://vps.luveedu.cloud/api/poweron/4242 \
  -H "X-API-Key: vpsmgr_admin_f37cabf0cae04df864991dca44e8a20d4f2dcc7c8321e4be" \
  -H "X-User-Id: 100" \
  -H "X-Username: alice"

# Get metrics
curl "https://vps.luveedu.cloud/api/metrics/4242?type=cpu&start=2026-08-25T00:00:00Z&end=2026-08-25T01:00:00Z" \
  -H "X-API-Key: vpsmgr_admin_f37cabf0cae04df864991dca44e8a20d4f2dcc7c8321e4be" \
  -H "X-User-Id: 100" \
  -H "X-Username: alice"

# Delete
curl -X DELETE https://vps.luveedu.cloud/api/delete/4242 \
  -H "X-API-Key: vpsmgr_admin_f37cabf0cae04df864991dca44e8a20d4f2dcc7c8321e4be" \
  -H "X-User-Id: 100" \
  -H "X-Username: alice"
```

---

## 7. Database Schema

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

- `vps_servers` — current state of all VPS instances (mirrored from Hetzner)
- `vps_volumes` — current state of all volumes
- `vps_audit` — admin action log (who did what when)
- `settings` — configuration (IP allowlist, etc.)

---

## 8. Deployment

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

---

## Support

For issues or questions, contact the platform team.