Skip to content

john-ayodeji/Chirpy

Repository files navigation

Chirpy

A minimal Twitter-like JSON API server built in Go. Chirpy supports user accounts, JWT-based authentication, refresh tokens, chirp creation and retrieval with filtering and sorting, secure deletion with ownership checks, and a webhook integration to upgrade users to "Chirpy Red" via an API key.

Features

  • User signup and login with Argon2id password hashing
  • JWT access tokens + Refresh token flow
  • Update user email and password (self only)
  • Create, list, fetch, and delete chirps
    • Filter by author_id
    • Sort by created_at ascending or descending
    • Delete requires ownership
  • Polka webhook (ApiKey auth) to upgrade a user to "Chirpy Red"
  • Admin utilities: metrics page and reset (dev only)
  • Health check endpoint

Tech Stack

  • Go 1.21+
  • Postgres
  • sqlc for type-safe DB access (generated in internal/database)
  • JWT (github.com/golang-jwt/jwt/v5)
  • Argon2id (github.com/alexedwards/argon2id)

Project Structure

.
├── main.go
├── adminHandlers.go
├── chirpsHandler.go
├── userHandlers.go
├── refresh_tokenHandler.go
├── chirpy_red.go              # Polka webhook handler
├── internal/
│   ├── auth/
│   │   ├── bearer_token.go
│   │   ├── jwt.go
│   │   ├── password_hash.go
│   │   ├── refresh_token.go
│   │   └── apikey.go
│   └── database/
│       ├── db.go
│       ├── models.go
│       ├── users.sql.go
│       ├── chirps.sql.go
│       ├── refresh_tokens.sql.go
│       └── custom.go          # non-sqlc helpers (UpdateUser, DeleteChirp, etc.)
├── sql/
│   ├── schema/                # SQL migrations (up/down)
│   └── queries/               # sqlc query definitions
└── assets/, index.html        # static files for /app/

Setup

Prerequisites

  • Go 1.21+
  • Postgres 13+

Environment Variables (.env)

PLATFORM=dev
DB_URL=postgres://<user>:<pass>@localhost:5432/chirpy?sslmode=disable
JWT_SECRET=replace-with-strong-secret
POLKA_KEY=replace-with-polka-webhook-key

Database

Create the database and apply the schema in order of the files under sql/schema (or use your preferred migration tool):

-- From psql
CREATE DATABASE chirpy;
\c chirpy
-- Run each file in sql/schema in ascending numeric order

If you use sqlc and modify sql/queries/*.sql, regenerate with:

sqlc generate

Run the Server

go run ./...

Server listens on :8080.

Auth Model

  • Access token: JWT in Authorization: Bearer <token> header, valid ~1 hour.
  • Refresh token: opaque string stored server-side; used to obtain a new access token via POST /api/refresh.
  • Passwords: stored using Argon2id hashes.
  • Webhook API key: Authorization: ApiKey <KEY> must match POLKA_KEY.

Endpoints

Below, all bodies are JSON unless stated otherwise.

Health

  • GET /api/healthz
    • 200 OK: "OK"

Admin

  • GET /admin/metrics (HTML)
    • Shows file server hit count.
  • POST /admin/reset (dev only: PLATFORM=dev)
    • Resets metrics and deletes all users. 200 on success, 403 otherwise.

Users

  • POST /api/users

    • Request: { "email": "...", "password": "..." }
    • Response 201: { id, email, password, created_at, updated_at, is_chirpy_red }
      • Note: password field contains the hashed password as stored (for parity with current code paths).
    • 400 if invalid or already exists.
  • POST /api/login

    • Request: { "email": "...", "password": "..." }
    • Response 200: { id, email, created_at, updated_at, token, refresh_token, is_chirpy_red }
    • 401 if invalid credentials.
  • PUT /api/users (Authenticated)

    • Headers: Authorization: Bearer <access_token>
    • Request: { "email": "...", "password": "..." } (update own account)
    • Response 200: { id, email, created_at, updated_at, is_chirpy_red }
    • 401 for missing/invalid token, 400 for bad input.

Tokens

  • POST /api/refresh

    • Headers: Authorization: Bearer <refresh_token>
    • Response 200: { "token": "<new_access_token>" }
    • 401 if missing/malformed header, invalid/expired token.
  • POST /api/revoke

    • Headers: Authorization: Bearer <refresh_token>
    • Response 204 on success.
    • 401 if missing or invalid refresh token.

Chirps

  • POST /api/chirps (Authenticated)

    • Headers: Authorization: Bearer <access_token>
    • Request: { "body": "<up to 140 chars>" }
      • Banned words (case-insensitive) are censored: kerfuffle, sharbert, fornax.
    • Response 201: { id, body, created_at, updated_at, user_id }
    • 400 if invalid; 401 if token missing/invalid.
  • GET /api/chirps

    • Query params:
      • author_id (optional, UUID): filter to a single author's chirps
      • sort (optional): asc or desc by created_at (default asc)
    • Response 200: [{ id, body, created_at, updated_at, user_id }, ...]
  • GET /api/chirps/{chirpID}

    • Response 200: { id, body, created_at, updated_at, user_id }
    • 404 if not found; 400 if invalid ID.
  • DELETE /api/chirps/{chirpID} (Authenticated)

    • Headers: Authorization: Bearer <access_token>
    • Only the author of the chirp can delete it.
    • 204 on success; 404 if not found; 403 if not author; 401 if unauthorized.

Webhooks (Polka)

  • POST /api/polka/webhooks
    • Headers: Authorization: ApiKey <POLKA_KEY>
    • Body example (only user.upgraded is acted upon):
      {
        "event": "user.upgraded",
        "data": { "user_id": "<uuid>" }
      }
    • Behavior: If key matches and event is user.upgraded, sets is_chirpy_red = true for the user. Responds 204 on success. 401 if invalid key.

Example Usage (WSL / curl)

Signup:

curl -s -X POST http://localhost:8080/api/users \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","password":"pass1234"}' | jq

Login:

LOGIN=$(curl -s -X POST http://localhost:8080/api/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","password":"pass1234"}')
TOKEN=$(echo "$LOGIN" | jq -r .token)
REFRESH=$(echo "$LOGIN" | jq -r .refresh_token)

Create a chirp:

curl -s -X POST http://localhost:8080/api/chirps \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"body":"My first chirp!"}' | jq

List chirps (author + order):

curl -s "http://localhost:8080/api/chirps?author_id=<AUTHOR_UUID>&sort=desc" | jq

Delete a chirp:

curl -i -X DELETE "http://localhost:8080/api/chirps/<CHIRP_UUID>" \
  -H "Authorization: Bearer $TOKEN"

Refresh access token:

curl -s -X POST http://localhost:8080/api/refresh \
  -H "Authorization: Bearer $REFRESH" | jq

Revoke refresh token:

curl -i -X POST http://localhost:8080/api/revoke \
  -H "Authorization: Bearer $REFRESH"

Webhook upgrade:

curl -i -X POST http://localhost:8080/api/polka/webhooks \
  -H "Authorization: ApiKey $POLKA_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"event":"user.upgraded","data":{"user_id":"<USER_UUID>"}}'

Development

  • Run auth unit tests:
go test ./internal/auth -v
  • Run all tests:
go test ./... -v

Notes & Constraints

  • DELETE /api/chirps/{id} checks ownership via the JWT sub claim.
  • POST /admin/reset is available only when PLATFORM=dev.
  • Responses use standard HTTP codes and JSON error payloads (e.g., { "error": "..." }).
  • For production, ensure strong JWT_SECRET, secure DB connection, and rotate POLKA_KEY as needed.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published