Conversation
- Added API key creation, listing, and deletion endpoints. - Integrated Redis for rate limiting and PostgreSQL for persistent API key storage. - Updated Docker configuration to include PostgreSQL and Redis services. - Enhanced README with setup instructions and environment variables. - Removed unused fake user data and added names.json for random identity generation. - Updated requirements.txt to include new dependencies for Redis and PostgreSQL. - Refactored middleware to support Redis-based rate limiting. - Added tests for API key lifecycle and bearer authentication.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR bumps the project to v1.0.1 and restructures the app into clearer layers (config/routes/models/services), while adding new Random + Network endpoints, API key management, Redis-backed rate limiting, and a new archive layout/mounting strategy.
Changes:
- Update version routing/redirects to
v1.0.1and rework gateway/archive mounting to load routers fromarchives/*/routes. - Add API key issuance/listing/deletion (Argon2 hashing + optional Postgres persistence) and bearer-token auth support.
- Add Random/Network feature areas (routes + modules + assets) and reorganize tests + Docker configs.
Reviewed changes
Copilot reviewed 58 out of 62 changed files in this pull request and generated 19 comments.
Show a summary per file
| File | Description |
|---|---|
| vercel.json | Redirect defaults updated to v1.0.1 + add /v1 and /v1.0 aliases. |
| tests/test_modules.py | Removes old aggregated module tests. |
| tests/test_endpoints.py | Updates/extends endpoint tests incl. API key lifecycle + bearer auth. |
| tests/test_archives_layout.py | Adds test enforcing archive version directory layout. |
| tests/modules/random.py | Adds a “module runner” script under tests (not a pytest test). |
| tests/modules/network.py | Adds a “module runner” script under tests (not a pytest test). |
| tests/modules/dev.py | Adds a “module runner” script under tests (not a pytest test). |
| src/services/random.py | Adds parsing/validation helper for explicit content filters. |
| src/services/auth.py | Adds require_authenticated_user() service helper. |
| src/services/init.py | Exposes service helpers via package exports. |
| src/security.py | Removes old monolithic security module (moved into src/config/security.py). |
| src/routes/uuid_hashing.py | Adds UUID/hash routing module. |
| src/routes/text.py | Adds text routing module. |
| src/routes/root.py | Adds root/status/assets routing module. |
| src/routes/random.py | Adds random routing module (color/gradient/quote/jokes). |
| src/routes/network.py | Adds network routing module (DNS + subnet/IP range helpers). |
| src/routes/math.py | Adds math routing module (incl. numeric system conversion endpoint). |
| src/routes/geo.py | Adds geo routing module. |
| src/routes/datetime.py | Adds datetime routing module. |
| src/routes/auth.py | Adds auth routing module (OAuth + API key management). |
| src/routes/init.py | Central router export/registration list. |
| src/modules/random.py | Adds Random module implementation + loads random assets. |
| src/modules/network.py | Adds Network module implementation (DNS, CIDR/subnet, IP ranges). |
| src/modules/math.py | Adds numeric base conversion helper to math module. |
| src/modules/dev.py | Adds “dev tools” module (regex/json helpers, diff, debug). |
| src/models/auth.py | Adds request model for API key creation. |
| src/models/init.py | Exposes auth models. |
| src/middleware/rate_limit.py | Adds Redis-backed rate limiting fallbacking to in-memory. |
| src/middleware/docs_rate_limit.py | Adds Redis-backed docs rate limiting fallbacking to in-memory. |
| src/main.py | Refactors app wiring through routers; adds middleware for module error-code->HTTP status; rewrites archive loader + gateway redirect logic; adds unversioned auth endpoints on gateway. |
| src/logging.py | Removes old logging module (moved into src/config/logging.py). |
| src/config/security.py | Enhances auth (stable OAuth redirect URI + bearer token support + logging). |
| src/config/logging.py | Adds config-scoped JSON logging setup. |
| src/config/config.py | Adds Postgres URL builder + version/domain defaults + API key pepper config. |
| src/config/api_keys.py | Adds API key hashing/storage/authentication (memory + Postgres). |
| src/config/init.py | Exposes settings from config package. |
| src/assets/random/names.json | Adds name/email provider dataset for Random identity. |
| src/assets/random/jokes.json | Adds jokes dataset for Random joke endpoint. |
| src/assets/random/dad_jokes.json | Adds dad jokes dataset. |
| src/assets/dev/regex.json | Adds regex mapping asset for dev tools. |
| src/assets/dev/debug.json | Adds debug responses/docs asset for dev tools. |
| requirements.txt | Adds runtime deps for new features (argon2/psycopg/redis/numpy/dnspython). |
| docker/docker-compose.yml | Adds Postgres + Redis services and wires env vars for the API. |
| docker/dev-docker-compose.yml | Adds dev compose setup building locally with Postgres + Redis. |
| archives/v1.0.0/routes/uuid_hashing.py | Adds archived router module for uuid/hashing. |
| archives/v1.0.0/routes/text.py | Adds archived router module for text endpoints. |
| archives/v1.0.0/routes/root.py | Adds archived router module for root/status/assets endpoints. |
| archives/v1.0.0/routes/math.py | Adds archived router module for math endpoints. |
| archives/v1.0.0/routes/geo.py | Adds archived router module for geo endpoints. |
| archives/v1.0.0/routes/datetime.py | Adds archived router module for datetime endpoints. |
| archives/v1.0.0/routes/auth.py | Adds archived router module for auth endpoints. |
| archives/v1.0.0/routes/init.py | Exposes archived routers. |
| archives/v1.0.0/middleware/rate_limit.py | Removes archived middleware (archive no longer ships app bootstrap). |
| archives/v1.0.0/middleware/docs_rate_limit.py | Removes archived middleware (archive no longer ships app bootstrap). |
| archives/v1.0.0/main.py | Removes archived bootstrap app (archive now router-only). |
| archives/v1.0.0/config.py | Removes archived settings module (shared config now used). |
| archives/v1.0.0/init.py | Removes archived package marker. |
| README.md | Updates versioning + archive layout docs + Docker instructions + security notes. |
| .gitignore | Adds .vercel to ignores. |
| .env.example | Adds Redis/Postgres/API key env examples and guidance. |
Comments suppressed due to low confidence (1)
tests/test_endpoints.py:35
- These tests are hard-coded to
v1.0.0for redirects and versioned endpoints, but the PR sets the current version to1.0.1(and updates Vercel redirects accordingly). This will fail onceFREEAPI_CURRENT_VERSIONdefaults to 1.0.1. Derive the version prefix from settings (or a shared constant) and update expected redirect locations/payload versions.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Ensure a package module exists so relative imports inside the archive work. | ||
| # Keep BASE_DIR as a fallback so shared modules like config/services/models can | ||
| # be reused without duplicating them inside each archive version. | ||
| if package_name not in sys.modules: | ||
| pkg = types.ModuleType(package_name) | ||
| pkg.__path__ = [str(version_dir), str(BASE_DIR)] | ||
| sys.modules[package_name] = pkg |
There was a problem hiding this comment.
Adding BASE_DIR to the archive package __path__ means shared modules (e.g. config.api_keys, config.security) will be imported under the archive-specific module name (e.g. freeapi_archives.v1_0_0.config.api_keys) rather than the canonical src.config.api_keys. This creates duplicate module singletons (separate settings objects, separate _STORE API key stores, etc.) and can lead to inconsistent behavior between current and archived apps. Consider a different sharing strategy (e.g., require archives to import src.* explicitly, or provide a single shared package name for common code) to avoid double-loading shared modules.
| def random_color() -> dict: | ||
| COLOR = (rand.randint(0,255), rand.randint(0,255), rand.randint(0,255)) | ||
| HEX = f"#{cns('decimal', 'hexadecimal', COLOR[0])['result']}{cns('decimal', 'hexadecimal', COLOR[1])['result']}{cns('decimal', 'hexadecimal', COLOR[2])['result']}" | ||
| GOOGLE_COLOR_PICKER_URL = f"https://google.com/search?q={HEX.replace('#', '%23')}&utm_source={settings.freeapi_domain}" | ||
| return { "hex": HEX, "rgb": COLOR, "google": GOOGLE_COLOR_PICKER_URL} |
There was a problem hiding this comment.
HEX concatenates base-conversion results that are not zero-padded, so values < 16 produce 1-digit components (e.g., #A3F...) which is not a valid 6-digit hex color. Format each channel as exactly two hex digits (e.g., via format(x, "02X") or .zfill(2)) before concatenation.
| def dns_request(domain: str, type: str = 'A', nameserver: str = "1.1.1.1") -> dict: | ||
| """ | ||
| Perform a DNS request for the given domain and record type. | ||
|
|
||
| Args: | ||
| domain (str): The domain to query. | ||
| type (str): The type of DNS record to query (e.g., 'A', 'MX', 'CNAME'). | ||
| nameserver (str): The DNS server to use for the query (default is "1.1.1.1"). | ||
| Returns: | ||
| dict: A dictionary containing the DNS response. | ||
| """ | ||
| try: | ||
| client = _dns_client(nameserver) | ||
| if client is None: | ||
| return {"error": "dnspython is not installed."} | ||
|
|
||
| answers = client.resolve(domain, type) | ||
| return {"answers": [str(answer) for answer in answers]} | ||
| except Exception as e: | ||
| return {"error": str(e)} |
There was a problem hiding this comment.
dns_request() returns error payloads without a code, and exception handling returns raw exception strings. With the new {"error": ..., "code": ...} -> HTTP status middleware, these errors will still be returned as HTTP 200 and may leak internal details. Return consistent code values (e.g., 400/500) and map/normalize exception messages to user-safe errors.
| from pathlib import Path | ||
| import sys | ||
|
|
||
| # Allow direct execution of this file from any working directory. | ||
| PROJECT_ROOT = Path(__file__).resolve().parents[2] | ||
| SCRIPT_DIR = Path(__file__).resolve().parent | ||
| if str(PROJECT_ROOT) not in sys.path: | ||
| sys.path.insert(0, str(PROJECT_ROOT)) | ||
|
|
||
| # Avoid shadowing the stdlib `network` module with this file name. | ||
| sys.path = [path for path in sys.path if Path(path).resolve() != SCRIPT_DIR] | ||
|
|
||
| import src.modules.network as mnetwork | ||
|
|
||
| print(mnetwork.ip_range('24')) No newline at end of file |
There was a problem hiding this comment.
tests/modules/*.py look like ad-hoc debug scripts (they mutate sys.path and print() output) rather than pytest tests, so they won't be executed by default test discovery and can confuse the test suite structure. Consider moving these to a scripts/ or tools/ folder, or converting them into real test_*.py tests with assertions.
| from pathlib import Path | ||
| import sys | ||
|
|
||
| # Allow direct execution of this file from any working directory. | ||
| PROJECT_ROOT = Path(__file__).resolve().parents[2] | ||
| SCRIPT_DIR = Path(__file__).resolve().parent | ||
| if str(PROJECT_ROOT) not in sys.path: | ||
| sys.path.insert(0, str(PROJECT_ROOT)) | ||
|
|
||
| # Avoid shadowing the stdlib `dev` module with this file name. | ||
| sys.path = [path for path in sys.path if Path(path).resolve() != SCRIPT_DIR] | ||
|
|
||
| import src.modules.dev as mdev | ||
|
|
||
| print(mdev.test_regex(r'\b\w+\b', 'This is a test string.')) | ||
| print(mdev.generate_regex('This is a test string.')) | ||
|
|
||
| import re | ||
| pattern = re.compile(r"2020-03-12T13:34:56\.123Z INFO \[org\.example\.Class\]: This is a #simple #logline containing a 'value'\.", re.IGNORECASE) | ||
| print(pattern.match("This is a test string.")) No newline at end of file |
There was a problem hiding this comment.
tests/modules/*.py look like ad-hoc debug scripts (they mutate sys.path and print() output) rather than pytest tests, so they won't be executed by default test discovery and can confuse the test suite structure. Consider moving these to a scripts/ or tools/ folder, or converting them into real test_*.py tests with assertions.
| # OAUTH_SCOPES= | ||
|
|
||
| # Optional cookie secret. If omitted, the app falls back to OAUTH_CLIENT_SECRET. | ||
| # Optional cookie secret. Ifdev omitted, the app falls back to OAUTH_CLIENT_SECRET. |
There was a problem hiding this comment.
Typo in comment: "Ifdev" should be "If".
| # Optional cookie secret. Ifdev omitted, the app falls back to OAUTH_CLIENT_SECRET. | |
| # Optional cookie secret. If omitted, the app falls back to OAUTH_CLIENT_SECRET. |
| def _pepper() -> str: | ||
| return settings.api_key_pepper or settings.session_secret or os.getenv("SESSION_SECRET", "") or "freeapi-api-key-pepper" | ||
|
|
||
|
|
There was a problem hiding this comment.
_pepper() falls back to a hard-coded constant ("freeapi-api-key-pepper") when neither API_KEY_PEPPER nor SESSION_SECRET is set. That removes the security benefit of a pepper (hashes become portable/offline-crackable if the DB leaks). Consider requiring an explicit pepper/secret in production (and failing fast if missing), or at least making the fallback conditional on non-production environments.
| def _pepper() -> str: | |
| return settings.api_key_pepper or settings.session_secret or os.getenv("SESSION_SECRET", "") or "freeapi-api-key-pepper" | |
| def _is_production_environment() -> bool: | |
| configured_environment = getattr(settings, "environment", None) | |
| if isinstance(configured_environment, str) and configured_environment.strip(): | |
| return configured_environment.strip().lower() == "production" | |
| for env_var in ("ENVIRONMENT", "APP_ENV", "PYTHON_ENV", "FASTAPI_ENV"): | |
| value = os.getenv(env_var, "") | |
| if value.strip(): | |
| return value.strip().lower() == "production" | |
| return False | |
| def _pepper() -> str: | |
| pepper = settings.api_key_pepper or settings.session_secret or os.getenv("SESSION_SECRET", "") | |
| if pepper: | |
| return pepper | |
| if _is_production_environment(): | |
| raise RuntimeError("API key pepper is required in production; set API_KEY_PEPPER or SESSION_SECRET") | |
| return "freeapi-api-key-pepper" |
| ``` | ||
|
|
||
| This will build the Docker image and start the API in a container. The API will be accessible at `http://localhost:8000`. | ||
| This will build the API image locally and start the API together with Postgres and Redis. The API will be accessible at `http://localhost:8000`. |
There was a problem hiding this comment.
The Docker instructions say docker compose up -d "will build the API image locally", but docker/docker-compose.yml uses a prebuilt image: ghcr.io/freepyapi/freeapi:latest (no build:). Either update the compose file to build locally (or reference dev-docker-compose.yml), or adjust the README wording so it matches what actually happens.
| This will build the API image locally and start the API together with Postgres and Redis. The API will be accessible at `http://localhost:8000`. | |
| This will start the API together with Postgres and Redis using the prebuilt API image defined in the compose file. The API will be accessible at `http://localhost:8000`. |
| from pathlib import Path | ||
| import sys | ||
|
|
||
| # Allow direct execution of this file from any working directory. | ||
| PROJECT_ROOT = Path(__file__).resolve().parents[2] | ||
| SCRIPT_DIR = Path(__file__).resolve().parent | ||
| if str(PROJECT_ROOT) not in sys.path: | ||
| sys.path.insert(0, str(PROJECT_ROOT)) | ||
|
|
||
| # Avoid shadowing the stdlib `random` module with this file name. | ||
| sys.path = [path for path in sys.path if Path(path).resolve() != SCRIPT_DIR] | ||
|
|
||
| import src.modules.random as mrandom | ||
|
|
||
| print(mrandom.random_gradient()) No newline at end of file |
There was a problem hiding this comment.
tests/modules/*.py look like ad-hoc debug scripts (they mutate sys.path and print() output) rather than pytest tests, so they won't be executed by default test discovery and can confuse the test suite structure. Consider moving these to a scripts/ or tools/ folder, or converting them into real test_*.py tests with assertions.
| @router.get("/", tags=["Root"]) | ||
| def root() -> dict[str, Any]: | ||
| version = settings.freeapi_current_version | ||
| return { | ||
| "message": "Welcome to the FreeAPI!", | ||
| "version": f"v{version}", | ||
| "documentation": f"/v{version}/docs", | ||
| } |
There was a problem hiding this comment.
This archived root handler uses settings.freeapi_current_version to build the reported version and docs link. That will reflect the current deployment version (e.g. 1.0.1), not the archived version (v1.0.0), so /v1.0.0/ can report the wrong version and point to the wrong docs. Prefer deriving the version from the mounted app (e.g., request.app.version) or hard-coding v1.0.0 within this archive package.
PrereleaseNotes
All notable changes to this project are documented in this file.
[v1.0.1] - 2026-04-21
Added
GET /random/colorGET /random/gradientGET /random/quoteGET /random/jokeGET /random/dad_jokeGET /network/dnsGET /network/dns/reverseGET /network/subnet_mask_to_cidrGET /network/cidr_to_subnet_maskGET /network/ip_rangeGET /network/ip_range/recommendedPOST /auth/api-keysGET /auth/api-keysDELETE /auth/api-keys/{key_uuid}src/config,src/routes,src/models,src/services) to support cleaner module boundaries and easier maintenance.dev,network,random).Changed
assets/,modules/, androutes/.archive/andarchives/as archive roots at runtime.v1.0.1in Vercel redirects, including aliases from/v1and/v1.0.argon2-cffi,psycopg[binary],redis,numpy,dnspython).Fixed
{"error": ..., "code": ...}payloads, so returned status codes now align with module error codes.Security
API_KEY_PEPPER).Notes
v1.0.1./v1.0.1/*, while unversioned endpoints continue to redirect to the latest version.