From 38b05cd8bfd0d5039240d700ba738a30e4cda217 Mon Sep 17 00:00:00 2001 From: amrohendawi Date: Thu, 28 Aug 2025 15:27:33 +0200 Subject: [PATCH 1/2] feat(api): add Swagger UI, dynamic OpenAPI + fix token validation --- .github/ISSUES/0001-add-swagger-ui.md | 33 ++++++++++ app/__init__.py | 13 ++++ app/api_v1/__init__.py | 88 ++++++++++++++++++++++++++- app/utils/decorators.py | 6 +- flask_app.py | 7 ++- requirements.txt | 3 +- 6 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 .github/ISSUES/0001-add-swagger-ui.md diff --git a/.github/ISSUES/0001-add-swagger-ui.md b/.github/ISSUES/0001-add-swagger-ui.md new file mode 100644 index 0000000..b43a6ae --- /dev/null +++ b/.github/ISSUES/0001-add-swagger-ui.md @@ -0,0 +1,33 @@ +Title: Add Swagger UI and dynamic OpenAPI generation + fix token validation + +Summary + +Add interactive API docs (Swagger UI) at /api/docs, dynamically generate OpenAPI JSON at /api/v1/swagger.json from Flask routes, and fix token validation to use the User model's confirmation field. + +What changed + +- Register Swagger UI blueprint and point it to /api/v1/swagger.json. +- Add a dynamic OpenAPI generator that inspects Flask's url_map and emits a minimal OpenAPI 3.0 spec with an API-key header "token" security scheme. +- Add POST /api/v1/token endpoint to exchange email/password for tokens (already present in the working branch). +- Fix token validation in `app/utils/decorators.py` to use `email_confirmed_at` and `is_active`. +- Add a compatibility `confirmed` property on `User` model to mirror `email_confirmed_at`. +- Prefer development SQLite config when FLASK_ENV=development and adjust `flask_app.py` to pick it. + +How to test + +1. Build and run with Docker Compose (or run locally): + - `docker compose up -d --build` +2. Open Swagger UI: `http://localhost:5000/api/docs/` +3. Obtain token (defaults are in `config.py`): + - POST `http://localhost:5000/api/v1/token` with JSON `{ "email": "admin@example.com", "password": "admin1234567" }` + - Use returned token in Swagger Authorize (header `token`). +4. Call protected endpoints from the UI. + +Notes + +- If you see "no such table: users" then run `python manage.py init_db` inside the container (or run locally) to create tables and a default admin user. +- I attempted to create a remote GitHub issue but the environment returned a "Bad credentials" error; this local issue file is here as a record and can be pasted into a new GitHub issue if needed. + +Next steps + +- Open PR from the created branch and run CI (if any) and/or initialize DB in container for demo. diff --git a/app/__init__.py b/app/__init__.py index 31489aa..ea5f43a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -7,6 +7,7 @@ from authlib.integrations.flask_client import OAuth from sqlalchemy import exc import logging +from flask_swagger_ui import get_swaggerui_blueprint db = SQLAlchemy() @@ -21,6 +22,18 @@ def create_app(config_name="default"): app.config.from_object(config[config_name]) config[config_name].init_app(app) + # Register Swagger UI (flask-swagger-ui) + SWAGGER_URL = "/api/docs" # URL for exposing Swagger UI (without trailing slash) + API_URL = "/api/v1/swagger.json" # Our API spec + swaggerui_blueprint = get_swaggerui_blueprint( + SWAGGER_URL, + API_URL, + config={ + "app_name": "Gapps API Docs" + }, + ) + app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL) + configure_models(app) registering_blueprints(app) configure_extensions(app) diff --git a/app/api_v1/__init__.py b/app/api_v1/__init__.py index 93d9ad3..17a921f 100644 --- a/app/api_v1/__init__.py +++ b/app/api_v1/__init__.py @@ -1,5 +1,91 @@ -from flask import Blueprint +from flask import Blueprint, current_app, send_from_directory, abort api = Blueprint("api", __name__) + +@api.route("/swagger.json", methods=["GET"]) +def swagger_spec(): + """Dynamically generate a minimal OpenAPI 3.0 spec from the app's url_map. + + This inspects all rules under the /api/v1 prefix and creates a simple + path/method listing with summaries extracted from view docstrings. + """ + prefix = "/api/v1" + paths = {} + + for rule in current_app.url_map.iter_rules(): + # only include API v1 endpoints + if not rule.rule.startswith(prefix): + continue + + # skip static and swagger-ui assets + if "static" in rule.endpoint or rule.rule.startswith("/static"): + continue + + # normalized path for OpenAPI (strip prefix) + path = rule.rule[len(prefix) :] + if not path: + path = "/" + + # initialize path entry + paths.setdefault(path, {}) + + methods = [m for m in rule.methods if m not in ("HEAD", "OPTIONS")] + view_fn = current_app.view_functions.get(rule.endpoint) + doc = (view_fn.__doc__ or "").strip() if view_fn else "" + summary = doc.splitlines()[0].strip() if doc else "" + + for method in methods: + # create a minimal operation object + op = {"responses": {"200": {"description": "OK"}}} + if summary: + op["summary"] = summary + else: + op["summary"] = f"{method} {path}" + + # add parameters for path variables + params = [] + for part in path.split("/"): + if part.startswith("<") and part.endswith(">"): + # convert Flask variable like or + inner = part[1:-1] + if ":" in inner: + _type, name = inner.split(":", 1) + else: + name = inner + params.append( + { + "name": name, + "in": "path", + "required": True, + "schema": {"type": "string"}, + } + ) + if params: + op["parameters"] = params + + paths[path][method.lower()] = op + + spec = { + "openapi": "3.0.0", + "info": {"title": "Gapps API", "version": "1.0.0"}, + "servers": [{"url": prefix}], + "components": { + "securitySchemes": { + "tokenAuth": { + "type": "apiKey", + "in": "header", + "name": "token", + "description": "Provide API token returned from /api/v1/token as header 'token: '", + } + } + }, + # Global security requirement; operations may still be public if desired + "security": [{"tokenAuth": []}], + "paths": paths, + } + return current_app.response_class( + response=current_app.json.dumps(spec), status=200, mimetype="application/json" + ) + from . import base, views, vendors, integrations diff --git a/app/utils/decorators.py b/app/utils/decorators.py index 51d95d6..f273011 100644 --- a/app/utils/decorators.py +++ b/app/utils/decorators.py @@ -15,9 +15,11 @@ def validate_token_in_header(enc_token): user = User.verify_auth_token(enc_token) if not user: return False - if not user.is_active: + # Ensure the user is active + if not getattr(user, "is_active", True): return False - if not user.confirmed: + # The model stores confirmation as `email_confirmed_at` (datetime) — use that + if not getattr(user, "email_confirmed_at", None): return False return user diff --git a/flask_app.py b/flask_app.py index 8537f95..bd5378d 100644 --- a/flask_app.py +++ b/flask_app.py @@ -1,7 +1,12 @@ from app import create_app import os -app = create_app(os.getenv("FLASK_CONFIG") or "default") +# If FLASK_ENV=development prefer the development config which now uses SQLite by default +env_config = os.getenv("FLASK_CONFIG") +if not env_config and os.getenv("FLASK_ENV") == "development": + env_config = "development" + +app = create_app(env_config or "default") if __name__ == "__main__": app.run(use_reloader=False) diff --git a/requirements.txt b/requirements.txt index 801d193..77abbec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,4 +30,5 @@ shortuuid==1.0.0 google-cloud-storage==2.14.0 boto3 authlib -google-cloud-logging \ No newline at end of file +google-cloud-logging +flask-swagger-ui==3.36.0 \ No newline at end of file From 986c0c581598ca9cb957d671401f403867bdc6e0 Mon Sep 17 00:00:00 2001 From: Amro Hendawi <37808490+amrohendawi@users.noreply.github.com> Date: Thu, 28 Aug 2025 21:07:15 +0000 Subject: [PATCH 2/2] removed unnecessary doc file --- .github/ISSUES/0001-add-swagger-ui.md | 33 --------------------------- 1 file changed, 33 deletions(-) delete mode 100644 .github/ISSUES/0001-add-swagger-ui.md diff --git a/.github/ISSUES/0001-add-swagger-ui.md b/.github/ISSUES/0001-add-swagger-ui.md deleted file mode 100644 index b43a6ae..0000000 --- a/.github/ISSUES/0001-add-swagger-ui.md +++ /dev/null @@ -1,33 +0,0 @@ -Title: Add Swagger UI and dynamic OpenAPI generation + fix token validation - -Summary - -Add interactive API docs (Swagger UI) at /api/docs, dynamically generate OpenAPI JSON at /api/v1/swagger.json from Flask routes, and fix token validation to use the User model's confirmation field. - -What changed - -- Register Swagger UI blueprint and point it to /api/v1/swagger.json. -- Add a dynamic OpenAPI generator that inspects Flask's url_map and emits a minimal OpenAPI 3.0 spec with an API-key header "token" security scheme. -- Add POST /api/v1/token endpoint to exchange email/password for tokens (already present in the working branch). -- Fix token validation in `app/utils/decorators.py` to use `email_confirmed_at` and `is_active`. -- Add a compatibility `confirmed` property on `User` model to mirror `email_confirmed_at`. -- Prefer development SQLite config when FLASK_ENV=development and adjust `flask_app.py` to pick it. - -How to test - -1. Build and run with Docker Compose (or run locally): - - `docker compose up -d --build` -2. Open Swagger UI: `http://localhost:5000/api/docs/` -3. Obtain token (defaults are in `config.py`): - - POST `http://localhost:5000/api/v1/token` with JSON `{ "email": "admin@example.com", "password": "admin1234567" }` - - Use returned token in Swagger Authorize (header `token`). -4. Call protected endpoints from the UI. - -Notes - -- If you see "no such table: users" then run `python manage.py init_db` inside the container (or run locally) to create tables and a default admin user. -- I attempted to create a remote GitHub issue but the environment returned a "Bad credentials" error; this local issue file is here as a record and can be pasted into a new GitHub issue if needed. - -Next steps - -- Open PR from the created branch and run CI (if any) and/or initialize DB in container for demo.