Skip to content

Latest commit

 

History

11 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LinkPulse

A link shortener with click analytics: paste a URL, get a short one back, and (optionally) sign in to claim it, pick a custom code, and see who's clicking it.

Why this exists

This is the fourth in a series of self-contained portfolio repos — BriefGenerator (Flask, OAuth2 API), OpsDesk (Next.js full-stack app), and WebhookRelay (Go, distributed webhook delivery) are the other three.

Unlike those, this one isn't modeled on a pattern from my day job — it's built specifically to give React its own showcase. BriefGenerator is API-only, WebhookRelay is API-only Go, and OpsDesk's React is filtered through Next.js's server-component/server-action conventions. LinkPulse is the one where the frontend is the point: a plain Vite + React + TypeScript SPA talking to a separate API, so the component architecture, hooks, and client-side data flow aren't sharing the spotlight with a meta-framework.

The backend is also deliberately different from the other two Python/Node repos: FastAPI instead of Flask, Postgres instead of SQLite, and a public, mostly-anonymous-by-default access model instead of an OAuth2 machine client or a session-gated internal tool.

Stack

  • Frontend: React + Vite + TypeScript, Tailwind CSS, TanStack Query, React Router, Recharts
  • Backend: FastAPI, SQLAlchemy + Alembic, PostgreSQL, Pydantic, JWT auth (python-jose + passlib/bcrypt), slowapi for rate limiting
  • Testing: pytest (backend)

Architecture

Browser (React SPA, Vite dev server / static build)
    |
    |  fetch() with an optional Bearer JWT
    v
FastAPI app (app/main.py)
    |
    +-- /api/auth/*    -> register, login, current-user   (app/routers/auth.py)
    +-- /api/links      -> create/list/delete links         (app/routers/links.py)
    +-- /api/links/{code}/analytics -> per-link breakdowns   (app/routers/analytics.py)
    +-- /{code}         -> 302 redirect + click logging      (app/routers/redirect.py)
    |
    v
SQLAlchemy models (User, Link, Click) -> PostgreSQL
  • Auth: app/security.py is pure crypto — bcrypt hashing via passlib, JWT sign/verify via python-jose — with no FastAPI dependency, so it's directly unit-tested. app/deps.py wraps it as get_current_user (401 if missing/invalid) and get_current_user_optional (used by link creation, since that endpoint works both signed-out and signed-in).
  • Anonymous vs. claimed links: POST /api/links works with no auth at all — it just rate-limits and validates the URL. If a valid JWT is attached, the link is associated with that user and can use a custom vanity code; anonymous requests only get an auto-generated code. That split lives in app/routers/links.py and app/crud.py.
  • URL validation: app/utils/url_validation.py rejects non-http(s) schemes (javascript:, data:, file:, …), malformed input, and loopback/private/link-local hosts — the last one specifically to stop this service from being used as an SSRF pivot into a private network via its own redirect endpoint.
  • Click logging: every hit to GET /{code} writes a Click row — timestamp, referrer header, and a parsed browser/OS/device type via the user-agents library — before issuing the 302. No raw IP is stored; the analytics dashboard only needs the parsed fields.
  • Rate limiting: slowapi limits registration, login, and link creation per client IP (see the decorators in app/routers/*.py), so the no-login shorten flow can't be trivially hammered.
  • Route ordering matters: GET /{code} is a catch-all path parameter, so app/main.py registers it dead last — otherwise it would shadow /health, /docs, and every /api/* route added before it. There's a comment on that include call explaining why.
  • Frontend composition: src/api/*.ts are plain fetch functions with no React in them; src/hooks/useLinks.ts wraps them in TanStack Query for caching/invalidation; src/context/AuthContext.tsx is the one piece of global state (the current user), read via a useAuth() hook everywhere else. Pages compose presentational components (ShortenForm, LinkTable, the Recharts wrappers under components/analytics/) rather than owning markup directly.

Quickstart

1. Postgres + API (Docker)

git clone <this-repo>
cd linkpulse
docker compose up --build

This starts Postgres and the FastAPI app (running alembic upgrade head on boot) at http://localhost:8000. Try it:

curl -X POST http://localhost:8000/api/links \
  -H "Content-Type: application/json" \
  -d '{"original_url": "https://example.com"}'

2. Frontend

cd frontend
cp .env.example .env
npm install
npm run dev

Visit http://localhost:5173. The dev server talks to the API at VITE_API_BASE_URL (defaults to http://localhost:8000).

Running the backend without Docker

cd backend
python -m venv .venv
source .venv/bin/activate   # .venv\Scripts\activate on Windows
pip install -r requirements.txt

cp .env.example .env
# point DATABASE_URL at your own Postgres instance

alembic upgrade head
uvicorn app.main:app --reload

Testing

cd backend
pytest

The suite runs against an in-memory SQLite database (see tests/conftest.py), so it needs no running Postgres. Coverage:

  • URL validation: scheme/host rejection, SSRF-style private/loopback addresses, length limits (tests/test_url_validation.py)
  • Password hashing and JWT sign/verify, including tampered and expired tokens (tests/test_security.py)
  • Auth endpoints: register, login, duplicate-email handling, /me (tests/test_auth_api.py)
  • Link creation (anonymous and authenticated), custom-code rules and collisions, ownership-scoped listing and deletion (tests/test_links_api.py)
  • The redirect endpoint: 302 behavior, 404 on unknown codes, and that a click row (with parsed user-agent fields) is actually written (tests/test_redirect.py)
  • Analytics aggregation: totals, clicks-over-time, and referrer/browser/device breakdowns, including the zero-clicks empty case (tests/test_analytics.py)

There's no frontend test suite yet — see Roadmap.

What's not tested / known simplifications

  • No frontend automated tests (component or e2e).
  • The redirect endpoint's rate limit is a simple in-memory, per-process bucket (slowapi's default), so it doesn't coordinate across multiple API instances — fine for a single-container demo, not for a real horizontally-scaled deployment.
  • The initial Alembic migration was written by hand against the SQLAlchemy models rather than autogenerated against a live Postgres instance (no local Postgres in the environment this was built in). It's a straightforward three-table schema; double-check it against alembic upgrade head before relying on it for anything beyond this demo.

Roadmap / stretch goals

  • Component tests for the React app (React Testing Library) and a Playwright smoke test for the shorten -> claim -> view analytics flow
  • QR code generation per link
  • Link expiration / deactivation
  • A Redis- or Postgres-backed rate limit store for multi-instance deployments, replacing slowapi's in-memory default
  • Bulk link import/export

License

MIT. See LICENSE.

About

A link shortener with click analytics: paste a URL, get a short one back, and (optionally) sign in to claim it, pick a custom code, and see who's clicking it.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages