Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ Default local services:
| Frontend | `http://localhost:5173` |
| Backend API | `http://localhost:4000` |
| Health check | `http://localhost:4000/health` |
| Interactive API docs (Swagger UI) | `http://localhost:4000/api-docs` |
| OpenAPI spec (JSON) | `http://localhost:4000/api-docs/openapi.json` |

Start PostgreSQL when database-backed development is needed:

Expand Down Expand Up @@ -186,13 +188,31 @@ npm run build --workspace backend
| `GET` | `/api/achievements` | Lists available achievement definitions. |
| `GET` | `/api/reputation/signals` | Returns reputation signal metadata. |
| `GET` | `/api/reputation/leaderboard` | Returns ranked reputation profiles. |
| `GET` | `/api-docs` | Interactive Swagger UI for the full API. |
| `GET` | `/api-docs/openapi.json` | Machine-readable OpenAPI 3.0 spec. |

Example:

```bash
curl http://localhost:4000/api/passport/sample
```

### Interactive API Documentation

The full API is described by an [OpenAPI 3.0 specification](docs/openapi.yaml) with
request/response schemas, error codes, query parameters, and examples for every
endpoint. When the backend is running, browse the interactive docs at
[`http://localhost:4000/api-docs`](http://localhost:4000/api-docs) or fetch the raw
spec from `http://localhost:4000/api-docs/openapi.json`.

The JSON spec can be imported directly into Postman, Insomnia, or an OpenAPI code
generator to scaffold a typed client:

```bash
# Save the spec while the backend is running
curl http://localhost:4000/api-docs/openapi.json -o openapi.json
```

---

## Reputation Model
Expand Down Expand Up @@ -236,6 +256,7 @@ Read more in `docs/architecture.md`.

## Docs

- [API Reference (OpenAPI 3.0)](docs/openapi.yaml)
- [Architecture](docs/architecture.md)
- [Reputation System](docs/reputation-system.md)
- [Achievements](docs/achievements.md)
Expand Down
3 changes: 3 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"helmet": "latest",
"morgan": "latest",
"pg": "latest",
"swagger-ui-express": "^5.0.1",
"yaml": "^2.9.0",
"zod": "latest"
},
"devDependencies": {
Expand All @@ -29,6 +31,7 @@
"@types/morgan": "latest",
"@types/node": "latest",
"@types/pg": "latest",
"@types/swagger-ui-express": "^4.1.8",
"eslint": "latest",
"tsx": "latest",
"typescript": "latest",
Expand Down
54 changes: 54 additions & 0 deletions backend/src/api/docs/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Router } from 'express';
import swaggerUi from 'swagger-ui-express';
import { parse as parseYaml } from 'yaml';

const RELATIVE_SPEC_PATH = join('docs', 'openapi.yaml');

/**
* Resolve the OpenAPI spec by walking up from this module's directory until a
* `docs/openapi.yaml` is found. This keeps the path stable whether the backend
* runs from TypeScript sources (tsx) or the compiled `dist/` output, and
* regardless of the process working directory.
*/
function resolveSpecPath(): string {
const candidates: string[] = [];
let current = dirname(fileURLToPath(import.meta.url));

for (let depth = 0; depth < 8; depth += 1) {
candidates.push(join(current, RELATIVE_SPEC_PATH));
current = dirname(current);
}

candidates.push(join(process.cwd(), RELATIVE_SPEC_PATH));

const specPath = candidates.find((candidate) => existsSync(candidate));

if (!specPath) {
throw new Error(`Unable to locate OpenAPI specification (${RELATIVE_SPEC_PATH}).`);
}

return specPath;
}

const specPath = resolveSpecPath();
export const openApiDocument = parseYaml(readFileSync(specPath, 'utf8')) as Record<string, unknown>;

export const docsRouter = Router();

// Raw machine-readable spec, useful for codegen and Postman/Insomnia import.
docsRouter.get('/openapi.json', (_request, response) => {
response.json(openApiDocument);
});

// Interactive Swagger UI.
docsRouter.use('/', swaggerUi.serve);
docsRouter.get(
'/',
swaggerUi.setup(openApiDocument, {
customSiteTitle: 'TAO Passport API Docs',
swaggerOptions: { displayRequestDuration: true },
}),
);
12 changes: 11 additions & 1 deletion backend/src/api/reputation/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Router } from 'express';
import { z } from 'zod';
import { buildReputationSignals, getPaginatedLeaderboard } from '../../services/reputation/reputationService.js';
import { getWalletSnapshot } from '../../blockchain/bittensor/client.js';
import { badRequest } from '../../utils/http.js';

export const reputationRouter = Router();

Expand Down Expand Up @@ -34,7 +35,16 @@ const leaderboardQuerySchema = z.object({
reputationRouter.get('/leaderboard', async (request, response, next) => {
try {
void getWalletSnapshot;
const query = leaderboardQuerySchema.parse(request.query);
const parsedQuery = leaderboardQuerySchema.safeParse(request.query);

if (!parsedQuery.success) {
const detail = parsedQuery.error.issues
.map((issue) => `${issue.path.join('.') || 'query'}: ${issue.message}`)
.join('; ');
return badRequest(response, `Invalid leaderboard query parameters: ${detail}`);
}

const query = parsedQuery.data;
response.json(
await getPaginatedLeaderboard({
category: query.category,
Expand Down
6 changes: 6 additions & 0 deletions backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import express from 'express';
import helmet from 'helmet';
import morgan from 'morgan';
import { achievementsRouter } from './api/achievements/router.js';
import { docsRouter } from './api/docs/router.js';
import { healthRouter } from './api/health/router.js';
import { passportRouter } from './api/passport/router.js';
import { reputationRouter } from './api/reputation/router.js';
Expand All @@ -13,6 +14,11 @@ dotenv.config();
const app = express();
const port = Number(process.env.PORT ?? 4000);

// Swagger UI ships inline styles/scripts that the default helmet CSP blocks, so
// the docs surface gets a relaxed CSP. It is registered before the global helmet
// so the rest of the API keeps its strict security headers.
app.use('/api-docs', helmet({ contentSecurityPolicy: false }), docsRouter);

app.use(helmet());
app.use(cors({ origin: process.env.CORS_ORIGIN ?? 'http://localhost:5173' }));
app.use(express.json());
Expand Down
Loading
Loading