# SCS Spider API

> SCS Spider is the multi-tenant domain router for the Silk City Solution (SCS)
> school-management platform. Each school has its own domain and its own SQL Server
> database. Spider resolves `domain -> database source` and redirects users to the
> single SCS Web App. This document is complete and self-contained: everything an
> application or AI agent needs to integrate is on this page.

Machine-readable OpenAPI 3.0 spec: `GET /swagger/v1/swagger.json` (no auth).
Interactive docs (Swagger UI): `/swagger`. Human architecture overview: `/docs`.

## Base URL

All API routes are relative to the deployment host, production: `https://spider.silkcitysolution.com`.
The API is versioned in the path: current version is `v1` -> `/api/v1/...`.
All requests and responses are JSON (`Content-Type: application/json`).

## Authentication

Protected endpoints require an API key issued from the SCS Spider admin console
(`/Admin` -> API Keys). Send it on every request, either way:

- Header (preferred): `X-Api-Key: scs_<48 hex chars>`
- Query parameter: `?api_key=scs_<48 hex chars>`

| Status | Meaning |
|--------|---------|
| 401    | No key sent. Body is RFC 7807 problem+json. |
| 403    | Key unknown or revoked. |

Endpoints marked **auth: none** below work without a key.

## Core concept: tenant

A *tenant* is one school/organization:

```json
{
  "id": 1,
  "schoolName": "Rajshahi Model School & College",
  "domain": "epanel.rmscraj.edu.bd",
  "databaseName": "rmsc_db_new",
  "targetUrl": null,
  "dbHost": null,
  "dbUser": null,
  "dbPassword": null,
  "isActive": true,
  "notes": "optional free text",
  "createdAtUtc": "2026-06-11T14:02:07.18Z",
  "updatedAtUtc": "2026-06-11T14:02:07.18Z"
}
```

- `domain` — unique host the school's users browse to (no scheme/port/path).
- `databaseName` — the school's database on its SQL Server.
- `targetUrl` — optional per-tenant SCS Web App URL; `null` means the platform
  default is used.
- `dbHost` / `dbUser` / `dbPassword` — optional per-tenant SQL Server overrides for
  schools hosted on a different server; `null` means the platform defaults are used.
  The effective values (after fallback) are what appear in resolve responses and
  redirect tokens.
- `isActive: false` — Spider answers that domain with HTTP 403 instead of redirecting.

## Endpoints

### GET /api/v1/resolve?domain={domain}  (auth: API key)

Resolve a domain to its database source. `domain` accepts a bare host
(`epanel.rmscraj.edu.bd`) or a full URL (it is normalized: lowercased,
scheme/port/path stripped).

200 response (full database source including credentials — keep your API key safe):

```json
{
  "domain": "epanel.rmscraj.edu.bd",
  "schoolName": "Rajshahi Model School & College",
  "databaseName": "rmsc_db_new",
  "sqlServerHost": "sql-host\\instance",
  "sqlUser": "<sql login>",
  "sqlPassword": "<sql password>",
  "isActive": true,
  "targetUrl": "https://app.silkcitysolution.com/",
  "redirectUrl": "https://app.silkcitysolution.com?token=scs1.q1nA9X...",
  "token": "scs1.q1nA9X..."
}
```

Errors: `400` missing domain param; `404` domain not registered (problem+json with
detail).

### POST /api/v1/token/decrypt  (auth: API key)

Decrypt and validate a redirect token. Request body: `{ "token": "scs1...." }`.
200 returns the TokenPayload (see "The redirect contract" below); `400` problem+json
when the token is malformed, tampered with, or expired.

### GET /api/v1/tenants  (auth: API key)

List all tenants. Returns `TenantDto[]` (see "Core concept: tenant" above).

### GET /api/v1/tenants/{id}  (auth: API key)

One tenant by integer id. `404` if not found.

### POST /api/v1/tenants  (auth: API key)

Register a tenant. Request body:

```json
{
  "schoolName": "New Govt. High School",
  "domain": "epanel.newschool.edu.bd",
  "databaseName": "newschool_db",
  "targetUrl": null,
  "isActive": true,
  "notes": "optional"
}
```

Responses: `201` with the created TenantDto and a `Location` header;
`409` if the domain is already registered.

### PUT /api/v1/tenants/{id}  (auth: API key)

Full update with the same body as POST. Responses: `200` updated TenantDto,
`404` unknown id, `409` domain belongs to another tenant.

### DELETE /api/v1/tenants/{id}  (auth: API key)

Remove a tenant. `204` on success, `404` unknown id.

### GET /api/v1/health  (auth: none)

```json
{
  "status": "ok",
  "sqlServer": { "connected": true },
  "utcNow": "2026-06-12T08:00:00Z"
}
```

`sqlServer.connected: false` means Spider is up but the SQL Server is unreachable.

### GET /api/v1/info  (auth: none)

Machine-readable architecture summary: name, description, the four-step request
flow, and an endpoint catalog. Useful as a discovery entry point.

### GET /go/{domain}  (auth: none)

Demo/test route: performs the same 302 redirect a real tenant-domain request
gets, without needing DNS. `404` JSON if the domain is not registered.

## The redirect contract (what consuming apps receive)

When a user hits a registered school domain (via nginx -> Spider), Spider answers:

```
HTTP/1.1 302 Found
Location: {targetUrl}?token=scs1.<base64url blob>
```

The token is an **encrypted** payload — nothing about the tenant or its database is
guessable from the URL. Decrypted, it contains the complete database source:

```json
{
  "v": 1,
  "domain": "epanel.rmscraj.edu.bd",
  "school": "Rajshahi Model School & College",
  "server": "sql-host\\instance",
  "database": "rmsc_db_new",
  "user": "<sql login>",
  "password": "<sql password>",
  "path": "/students?class=9",
  "iat": 1781251200,
  "exp": 1781251500
}
```

`path` is the user's original path+query (null when they requested `/`). `iat`/`exp`
are unix seconds; **reject tokens past `exp`** (Spider issues 5-minute tokens).

Two ways to read the token:

1. **Call Spider** (simplest): `POST /api/v1/token/decrypt` with your API key —
   Spider validates expiry and returns the JSON above.
2. **Decrypt locally** (no network hop): you need the shared secret `PayloadKey`
   (ask the Spider administrator). Algorithm:
   - key = SHA-256(UTF-8 bytes of PayloadKey)  -> 32 bytes
   - strip the `scs1.` prefix, base64url-decode the rest
   - blob layout: `nonce (12 bytes) | ciphertext | tag (16 bytes)`
   - AES-256-GCM decrypt -> UTF-8 JSON

```csharp
// C# local decryption
static JsonElement DecryptToken(string token, string payloadKey)
{
    var key = SHA256.HashData(Encoding.UTF8.GetBytes(payloadKey));
    var b64 = token["scs1.".Length..].Replace('-', '+').Replace('_', '/');
    var blob = Convert.FromBase64String(b64.PadRight(b64.Length + (4 - b64.Length % 4) % 4, '='));
    var nonce = blob[..12]; var tag = blob[^16..]; var cipher = blob[12..^16];
    var plain = new byte[cipher.Length];
    using var aes = new AesGcm(key, 16);
    aes.Decrypt(nonce, cipher, tag, plain);
    var payload = JsonSerializer.Deserialize<JsonElement>(plain);
    if (DateTimeOffset.UtcNow.ToUnixTimeSeconds() > payload.GetProperty("exp").GetInt64())
        throw new InvalidOperationException("Token expired");
    return payload;
}
```

## Error format

All API errors use RFC 7807 `application/problem+json`:

```json
{ "title": "Domain not registered", "status": 404, "detail": "No tenant is configured for 'x.example'." }
```

## Deployment — publish + nginx with dynamic domains

How to run Spider in production so that **domains added through the API start
working immediately, with no nginx changes per school**.

### 1. Publish and run as a service

```bash
# build on the dev machine
dotnet publish -c Release -o published
rsync -az --delete published/ root@SERVER:/var/www/spider.silkcitysolution.com/
```

```ini
# /etc/systemd/system/scs-spider.service
[Unit]
Description=SCS Spider - ASP.NET Core 8 domain router
After=network.target

[Service]
WorkingDirectory=/var/www/spider.silkcitysolution.com
ExecStart=/usr/bin/dotnet /var/www/spider.silkcitysolution.com/ScsSpider.dll
Restart=always
RestartSec=5
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://127.0.0.1:5217
# Needed only when the SQL Server is 2012 without TLS 1.2 patches:
Environment=OPENSSL_CONF=/etc/scs-spider-openssl.cnf

[Install]
WantedBy=multi-user.target
```

`systemctl daemon-reload && systemctl enable --now scs-spider`. The app listens
only on localhost; nginx is the public entry.

### 2. nginx — the key to dynamic domains

Two server blocks. The first is the trick: its `server_name` is a **regex /
catch-all**, and `proxy_set_header Host $host` forwards the original domain.
Spider matches the Host header against its tenant registry **at request time**,
so registering a tenant via `POST /api/v1/tenants` (or the admin panel) is all
it takes — nginx never needs another edit.

```nginx
# /etc/nginx/sites-available/scs-spider

# (1) DYNAMIC tenant domains — any epanel.* host goes to Spider.
#     Widen the regex (or use "listen 80 default_server;") to accept
#     arbitrary custom domains too.
server {
    listen 80;
    server_name ~^epanel\..+$;

    location / {
        proxy_pass http://127.0.0.1:5217;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;   # <- Spider resolves by this
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# (2) Management UI + API (landing, /Admin, /swagger, /api, /llms.txt)
server {
    listen 80;
    server_name spider.silkcitysolution.com;

    location / {
        proxy_pass http://127.0.0.1:5217;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
```

```bash
ln -s /etc/nginx/sites-available/scs-spider /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx          # reload, never restart/stop
certbot --nginx -d spider.silkcitysolution.com --redirect   # SSL for the management domain
```

### 3. Adding a new school — the dynamic flow

1. `POST /api/v1/tenants` with `{schoolName, domain, databaseName, ...}` (or use /Admin).
2. Point the school domain's DNS A record at the Spider server.
3. Done — the next request on that domain is matched live (tenant cache refreshes
   within 30 s; API writes invalidate it instantly) and redirected with its
   encrypted token. No publish, no restart, no nginx edit.

School domains only ever receive a 302 from Spider, so serving them on port 80
is normally fine; add per-domain certbot certs only if you want HTTPS on the
school domains themselves.

### 4. Redeploying a new build

```bash
dotnet publish -c Release -o published
rsync -az --delete published/ root@SERVER:/var/www/spider.silkcitysolution.com/
ssh root@SERVER 'chown -R www-data:www-data /var/www/spider.silkcitysolution.com && systemctl restart scs-spider'
```

## Operational notes

- Tenant lookups are cached in-process for 30 seconds; writes through the API or
  admin console invalidate the cache immediately.
- Every resolution/redirect is logged (domain, action, tenant, client IP, UTC time).
- Backing store is SQL Server 2012; identifiers above are examples from the live
  seed data.

## Quick start (copy-paste)

```bash
# health (no key)
curl https://spider.silkcitysolution.com/api/v1/health

# resolve a domain
curl -H "X-Api-Key: $KEY" "https://spider.silkcitysolution.com/api/v1/resolve?domain=epanel.rmscraj.edu.bd"

# register a school
curl -X POST -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"schoolName":"New School","domain":"epanel.new.edu.bd","databaseName":"new_db","isActive":true}' \
  https://spider.silkcitysolution.com/api/v1/tenants

# decrypt a redirect token
curl -X POST -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"token":"scs1.q1nA9X..."}' \
  https://spider.silkcitysolution.com/api/v1/token/decrypt
```

```python
# Python
import requests
BASE, KEY = "https://spider.silkcitysolution.com", "scs_..."
r = requests.get(f"{BASE}/api/v1/resolve",
                 params={"domain": "epanel.rmscraj.edu.bd"},
                 headers={"X-Api-Key": KEY})
r.raise_for_status()
print(r.json()["databaseName"])
```

```csharp
// C# (.NET 8)
var http = new HttpClient { BaseAddress = new Uri("https://spider.silkcitysolution.com") };
http.DefaultRequestHeaders.Add("X-Api-Key", "scs_...");
var tenant = await http.GetFromJsonAsync<JsonElement>(
    "/api/v1/resolve?domain=epanel.rmscraj.edu.bd");
Console.WriteLine(tenant.GetProperty("databaseName").GetString());
```