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