Skip to content

feat(shortener): link + collection API — tenant-scoped, secure codes, SSRF-safe validation (B1-B2)#63

Open
PenguinzTech wants to merge 1 commit into
fix/backend-hygienefrom
feature/shortener-api
Open

feat(shortener): link + collection API — tenant-scoped, secure codes, SSRF-safe validation (B1-B2)#63
PenguinzTech wants to merge 1 commit into
fix/backend-hygienefrom
feature/shortener-api

Conversation

@PenguinzTech

Copy link
Copy Markdown
Contributor

Feature (Track B, part 1) — shortener foundation

Stacked on #62 (base = fix/backend-hygiene, the fully hardened backend). Targets release/v0.1.x.

First slice of the open-source bit.ly/short.io alternative: the data model + link & collection management API. Redirect, click-tracking, analytics, QR, and tier-gating are follow-up PRs.

  • Schema (startup-defined, DDL for postgres/mysql/sqlite): urls (unique short_code, tenant/team/collection FKs, expiry, click_count), collections (folders via parent_id), link_clicks (stores ip_hash only — never raw IP).
  • APIapp/urls.py + app/collections.py under /api/v1, all @auth_required + @require_scope, tenant-scoped (tenant from token, never body; no cross-tenant access; creator-or-tenant-admin for edits).
  • Secure codessecrets-based generation (not random), DB UNIQUE + bounded retry on collision; reserved-path list for custom aliases.
  • SSRF-safe URL validation (app/urlvalidation.py) — scheme allowlist; rejects literal + resolved private/loopback/link-local/reserved/metadata IPs (IPv4 incl. decimal/hex/octal, IPv6). Unresolvable public hostnames are allowed by design (the server 301-redirects, it never fetches the target).
  • RBACcollections:read/write/delete scopes added to role bundles (urls:*/analytics:read already existed).

Tests

595 passed, 90.92% coverage — SSRF bypass payloads (169.254.169.254, 127.0.0.1, 10.x, ::1, decimal-IP, javascript:/data:/file:) rejected; alias collision; tenant isolation; collection nesting + cycle prevention.

🤖 Generated with Claude Code

…des, SSRF-safe URL validation (B1-B2)

Foundation of the open-source URL shortener:
- Schema (models.py + DDL for postgres/mysql/sqlite, defined at startup): urls
  (short_code UNIQUE, tenant/team/collection FKs, expiry, click_count), collections
  (folders via parent_id, unique per tenant+parent), link_clicks (ip_hash only, for
  the redirect task).
- app/urls.py + app/collections.py blueprints under /api/v1, all @auth_required +
  @require_scope + tenant-scoped (tenant derived from token, never body; no cross-tenant
  access; creator-or-tenant-admin for edits).
- Short-code generation via secrets (not random); DB UNIQUE + bounded retry.
- app/urlvalidation.py: scheme allowlist (blocks javascript:/data:/file:), rejects
  literal + resolved private/loopback/link-local/reserved/multicast/metadata IPs
  (IPv4 incl. decimal/hex/octal, IPv6); unresolvable public hostnames allowed (the
  server 301-redirects, never fetches). Reserved-path list for aliases.
- rbac.py: collections:read/write/delete scopes added to role bundles.
- Redirect, click-tracking, analytics, QR, and tier-gating are follow-up tasks
  (TODO hooks left).

Tests: 595 passed, 90.92% coverage — incl. SSRF bypass payloads rejected, alias
collision, tenant isolation, collection nesting/cycle prevention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PenguinzTech PenguinzTech added this to the v0.1.x milestone Jul 14, 2026
@PenguinzTech PenguinzTech added the type:feature New feature label Jul 14, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @PenguinzTech, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant