diff --git a/.env.example b/.env.example index 3b81bf3..078c1a8 100644 --- a/.env.example +++ b/.env.example @@ -7,4 +7,4 @@ MATRIX_OIDC_CLIENT_ID=replace-with-your-registered-client-id # MATRIX_OIDC_CLIENT_SECRET=only-if-the-registered-client-has-one MATRIX_OIDC_REDIRECT_URI=https://your-public-host/api/auth/matrix/callback SESSION_COOKIE_SECURE=false # set to true when using HTTPS in production -VITE_DOCS_URL=docs_url \ No newline at end of file +VITE_DOCS_URL=http://127.0.0.1:8001 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 80d2aaa..65d6813 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,7 +37,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install -e ".[dev]" + python -m pip install -e ".[dev,docs]" - name: Check formatting run: black --check app db tests @@ -48,6 +48,9 @@ jobs: - name: Run tests run: pytest -q + - name: Build documentation + run: cd ../docs && mkdocs build --strict + frontend: name: Frontend quality runs-on: ubuntu-latest @@ -74,4 +77,4 @@ jobs: run: npm run lint - name: Type-check and build - run: npm run build \ No newline at end of file + run: npm run build diff --git a/.gitignore b/.gitignore index 7d1318a..ea8a673 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ __pycache__/ coverage.xml htmlcov/ backend/matrix_directory.db +backend/static/ frontend/node_modules/ frontend/dist/ frontend/.env.* @@ -18,4 +19,4 @@ frontend/vite.config.d.ts .vscode matrix_directory_backend.egg-info .mypy_cache -docs/site \ No newline at end of file +docs/site/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e2dbe4..a0ecd2d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,7 @@ Docker with Docker Compose is required. The services will be available at: - Frontend: http://localhost:5173 -- API documentation: http://localhost:8000/docs +- Swagger UI: http://localhost:8000/api/docs To stop the environment: diff --git a/README.md b/README.md index 781d5fe..b50a80e 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ docker compose up --build The services will be available at: - [Frontend](http://localhost:5173) -- [API documentation](http://localhost:8000/docs) +- [Swagger UI](http://localhost:8000/api/docs) The backend automatically applies database migrations on startup. @@ -89,7 +89,7 @@ See [Authentication Architecture](docs/docs/architecture/authentication.md) for Interactive API documentation is available at: -- [Swagger UI](http://localhost:8000/docs) +- [Swagger UI](http://localhost:8000/api/docs) Core endpoints include: diff --git a/backend/app/main.py b/backend/app/main.py index 94cf7b2..3134189 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -12,7 +12,13 @@ from app.config import get_settings settings = get_settings() -app = FastAPI(title=settings.app_name, version="0.1.0") +app = FastAPI( + title=settings.app_name, + version="0.1.0", + docs_url="/api/docs", + redoc_url="/api/redoc", + openapi_url="/api/openapi.json", +) app.add_middleware( SessionMiddleware, @@ -44,28 +50,40 @@ def health() -> dict[str, str]: return {"status": "ok"} -frontend_directory = Path(__file__).resolve().parent.parent / "static" -frontend_index = frontend_directory / "index.html" -frontend_assets = frontend_directory / "assets" +def mount_static_sites(application: FastAPI, static_directory: Path) -> None: + """Mount the built documentation and frontend when their files exist.""" + documentation_directory = static_directory / "docs" + frontend_index = static_directory / "index.html" + frontend_assets = static_directory / "assets" + + if documentation_directory.is_dir(): + application.mount( + "/docs", + StaticFiles(directory=documentation_directory, html=True), + name="documentation", + ) + + if not frontend_index.is_file(): + return -if frontend_index.is_file(): if frontend_assets.is_dir(): - app.mount( + application.mount( "/assets", StaticFiles(directory=frontend_assets), name="frontend-assets", ) - @app.get("/{path:path}", include_in_schema=False) + @application.get("/{path:path}", include_in_schema=False) def frontend(path: str) -> FileResponse: - if path == "api" or path.startswith("api/"): + reserved_path = path in {"api", "docs"} or path.startswith(("api/", "docs/")) + if reserved_path: raise HTTPException(status_code=404, detail="Not Found") - requested_file = (frontend_directory / path).resolve() - if ( - requested_file.is_relative_to(frontend_directory) - and requested_file.is_file() - ): + requested_file = (static_directory / path).resolve() + if requested_file.is_relative_to(static_directory) and requested_file.is_file(): return FileResponse(requested_file) return FileResponse(frontend_index) + + +mount_static_sites(app, Path(__file__).resolve().parent.parent / "static") diff --git a/backend/tests/test_static_sites.py b/backend/tests/test_static_sites.py new file mode 100644 index 0000000..4e87c6e --- /dev/null +++ b/backend/tests/test_static_sites.py @@ -0,0 +1,73 @@ +from pathlib import Path + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from app.main import app, mount_static_sites + + +def test_api_documentation_routes__expect_swagger_and_schema() -> None: + client = TestClient(app) + + swagger_response = client.get("/api/docs") + schema_response = client.get("/api/openapi.json") + + assert swagger_response.status_code == 200 + assert "swagger-ui" in swagger_response.text + assert schema_response.status_code == 200 + assert schema_response.json()["info"]["title"] == "Matrix Directory API" + + +def test_documentation_static_files__expect_served_under_docs( + tmp_path: Path, +) -> None: + static_directory = tmp_path / "static" + documentation_directory = static_directory / "docs" + guide_directory = documentation_directory / "guide" + guide_directory.mkdir(parents=True) + (documentation_directory / "index.html").write_text( + "

Documentation home

", + encoding="utf-8", + ) + (guide_directory / "index.html").write_text( + "

Documentation guide

", + encoding="utf-8", + ) + + application = FastAPI() + mount_static_sites(application, static_directory) + client = TestClient(application) + + home_response = client.get("/docs/") + guide_response = client.get("/docs/guide/") + + assert home_response.status_code == 200 + assert "Documentation home" in home_response.text + assert guide_response.status_code == 200 + assert "Documentation guide" in guide_response.text + + +def test_reserved_routes__expect_not_fall_through_to_frontend( + tmp_path: Path, +) -> None: + static_directory = tmp_path / "static" + static_directory.mkdir() + (static_directory / "index.html").write_text( + "

Frontend application

", + encoding="utf-8", + ) + + application = FastAPI() + mount_static_sites(application, static_directory) + client = TestClient(application) + + api_response = client.get("/api/not-a-real-route") + docs_response = client.get("/docs/not-a-real-page") + frontend_response = client.get("/bots/example") + + assert api_response.status_code == 404 + assert "Frontend application" not in api_response.text + assert docs_response.status_code == 404 + assert "Frontend application" not in docs_response.text + assert frontend_response.status_code == 200 + assert "Frontend application" in frontend_response.text diff --git a/docs/.dockerignore b/docs/.dockerignore deleted file mode 100644 index 98e8236..0000000 --- a/docs/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -site/ -__pycache__/ -*.py[cod] diff --git a/docs/Dockerfile b/docs/Dockerfile deleted file mode 100644 index 3ae7183..0000000 --- a/docs/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM python:3.13-slim AS builder - -WORKDIR /docs - -COPY requirements.txt . -RUN python -m pip install --no-cache-dir -r requirements.txt - -COPY . . -RUN mkdocs build --strict - - -FROM nginx:alpine AS runtime - -COPY nginx.conf.template /etc/nginx/templates/default.conf.template -COPY --from=builder /docs/site /usr/share/nginx/html - -ENV PORT=8080 - -EXPOSE 8080 diff --git a/docs/docs/development.md b/docs/docs/development.md index 162af6f..89ca35e 100644 --- a/docs/docs/development.md +++ b/docs/docs/development.md @@ -41,7 +41,7 @@ services are ready, open: | Service | Address | | --- | --- | | Frontend | | -| API documentation | | +| Swagger UI | | You now have the frontend, backend, and database running together. The frontend reloads as its source changes; restart the API service after backend changes. diff --git a/docs/docs/index.md b/docs/docs/index.md index 0b8655c..d835cd5 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -93,7 +93,7 @@ profiles, project ownership, and directory data. Once the services are ready, open: - [Matrix Directory](http://localhost:5173) - - [Interactive API documentation](http://localhost:8000/docs) + - [Interactive API documentation](http://localhost:8000/api/docs) === "Stop or reset" diff --git a/docs/docs/reference/api.md b/docs/docs/reference/api.md index a5d59be..6d3ac60 100644 --- a/docs/docs/reference/api.md +++ b/docs/docs/reference/api.md @@ -3,7 +3,7 @@ The FastAPI backend exposes JSON application endpoints under `/api`. This page describes the stable behaviors a client needs; the running application provides the complete generated schemas in its -[Swagger UI](https://matrix-directory.codesociety.xyz/docs). +[Swagger UI](https://matrix-directory.codesociety.xyz/api/docs). ## At a glance @@ -217,4 +217,4 @@ Changing `matrix_id` clears any existing verification. Clients cannot set - [Development guide](../development.md) - [Authentication architecture](../architecture/authentication.md) -- [Interactive API documentation](https://matrix-directory.codesociety.xyz/docs) +- [Interactive API documentation](https://matrix-directory.codesociety.xyz/api/docs) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 63e5827..8708c66 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -2,7 +2,7 @@ site_name: Matrix Directory site_description: A community directory for projects in the Matrix ecosystem. -site_url: https://docs.matrix-directory.codesociety.xyz/ +site_url: https://matrix-directory.codesociety.xyz/docs/ dev_addr: 127.0.0.1:8001 repo_url: https://github.com/Code-Society-Lab/matrix-directory repo_name: Code-Society-Lab/matrix-directory @@ -79,6 +79,7 @@ markdown_extensions: emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.snippets: base_path: [".."] + nav: - Home: index.md - Guides: diff --git a/docs/nginx.conf.template b/docs/nginx.conf.template deleted file mode 100644 index 74f0628..0000000 --- a/docs/nginx.conf.template +++ /dev/null @@ -1,19 +0,0 @@ -server { - listen ${PORT}; - listen [::]:${PORT}; - - server_name _; - - root /usr/share/nginx/html; - index index.html; - - location / { - try_files $uri $uri/ =404; - } - - error_page 404 /404.html; - - location = /404.html { - internal; - } -} diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 65732f9..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -mkdocs==1.6.1 -mkdocs-material==9.7.6 diff --git a/frontend/src/components/SiteHeader.vue b/frontend/src/components/SiteHeader.vue index 651cff6..4d25117 100644 --- a/frontend/src/components/SiteHeader.vue +++ b/frontend/src/components/SiteHeader.vue @@ -19,7 +19,8 @@ import logoUrl from '../assets/matrix-directory-mark.svg' const docsUrl = - import.meta.env.VITE_DOCS_URL ?? 'http://127.0.0.1:8001' + import.meta.env.VITE_DOCS_URL ?? + (import.meta.env.DEV ? 'http://127.0.0.1:8001' : '/docs/') const router = useRouter() const route = useRoute() diff --git a/railpack.json b/railpack.json index f330474..8bfaf1d 100644 --- a/railpack.json +++ b/railpack.json @@ -16,7 +16,7 @@ { "src": ".", "dest": "." }, "python3 -m venv .venv", ".venv/bin/pip install --upgrade pip", - ".venv/bin/pip install ./backend", + ".venv/bin/pip install ./backend[docs]", "cd frontend && npm ci" ] }, @@ -26,12 +26,16 @@ { "step": "install" } ], "variables": { - "VITE_API_URL": "/api" + "VITE_API_URL": "/api", + "VITE_DOCS_URL": "/docs/" }, "commands": [ "cd frontend && npm run build", + "cd docs && ../.venv/bin/mkdocs build --strict", "mkdir -p backend/static", - "cp -R frontend/dist/. backend/static/" + "cp -R frontend/dist/. backend/static/", + "mkdir -p backend/static/docs", + "cp -R docs/site/. backend/static/docs/" ] } },