Skip to content
Open
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
13 changes: 13 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand Down
88 changes: 87 additions & 1 deletion app/api_v1/__init__.py
Original file line number Diff line number Diff line change
@@ -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 <string:id> or <id>
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: <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
6 changes: 4 additions & 2 deletions app/utils/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 6 additions & 1 deletion flask_app.py
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ shortuuid==1.0.0
google-cloud-storage==2.14.0
boto3
authlib
google-cloud-logging
google-cloud-logging
flask-swagger-ui==3.36.0