diff --git a/README.md b/README.md index d044606..5fa4ae6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,94 @@ # Chirpy -This is a project I built as part of [boot.dev](https://boot.dev). +A Twitter-style microblogging API built in Go as part of the [boot.dev](https://boot.dev) back-end course. + +## Features + +- Create, list, sort, and delete chirps (140-char limit, profanity-filtered) +- User registration and login with password hashing (argon2id) +- JWT access tokens and revocable refresh tokens +- Chirpy Red subscription upgrades via Polka webhooks +- Graceful shutdown and a lightweight admin metrics/reset surface + +## Tech stack + +- Go 1.26 (standard `net/http` with `http.ServeMux` routing) +- PostgreSQL +- [sqlc](https://sqlc.dev) for type-safe query generation +- [goose](https://pressly.github.io/goose/) for migrations +- [golang-jwt/jwt](https://github.com/golang-jwt/jwt) and [argon2id](https://github.com/alexedwards/argon2id) + +## Getting started + +### Prerequisites + +- Go 1.26+ +- PostgreSQL running locally +- `sqlc`, `goose`, and `make` (run `make install-tools` to install the first two) + +### Setup + +```sh +# 1. Configure environment variables +cp .env.example .env + +# fill in DB_URL, PLATFORM, JWT_SECRET, and POLKA_KEY + +# 2. Apply database migrations +make migrate-up + +# 3. Run the server +make run +``` + +The server listens on `http://localhost:8080`. + +### Development + +Run `make help` to see the full list of targets. Highlights: + +| Target | Description | +| --------------- | ---------------------------------------------- | +| `make run` | Build and run the server | +| `make dev` | Run with the race detector enabled | +| `make test` | Run all tests | +| `make test-race`| Run all tests with the race detector | +| `make check` | Run formatting, vet, and test checks | +| `make fmt` | Format all Go source files | +| `make lint` | Run golangci-lint (if installed) | +| `make build` | Compile the binary into `bin/` | +| `make generate` | Regenerate sqlc query code | +| `make migrate-up` / `migrate-down` / `migrate-status` / `migrate-reset` | Database migrations | +| `make install-tools` | Install goose, sqlc, and golangci-lint | +| `make clean` | Remove build artifacts | + +## API reference + +| Method | Path | Description | +| ------ | --------------------- | ----------------------------------------------- | +| POST | `/api/users` | Register a user | +| PUT | `/api/users` | Update email/password (Bearer access token) | +| POST | `/api/login` | Log in, receive access + refresh tokens | +| POST | `/api/refresh` | Exchange a refresh token for a new access token | +| POST | `/api/revoke` | Revoke a refresh token | +| GET | `/api/chirps` | List chirps (`?author_id=`, `?sort=asc\|desc`) | +| GET | `/api/chirps/{id}` | Get a single chirp | +| POST | `/api/chirps` | Create a chirp (Bearer access token) | +| DELETE | `/api/chirps/{id}` | Delete a chirp you authored | +| POST | `/api/polka/webhooks` | Polka webhook (`user.upgraded` -> Chirpy Red) | +| GET | `/api/healthz` | Health check | +| GET | `/app/` | Static index page | +| GET | `/admin/metrics` | File server hit counter (dev only) | +| POST | `/admin/reset` | Reset the database (dev only) | + +## Regenerating sqlc code + +After changing `sql/queries/`, run: + +```sh +make generate +``` + +## License + +MIT — see [LICENSE.md](LICENSE.md). diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 1aad80a..f7ed4eb 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -1,3 +1,5 @@ +// Package auth provides password hashing, JWT, and token helpers used to +// authenticate and authorize API requests. package auth import ( @@ -13,14 +15,18 @@ import ( "github.com/google/uuid" ) +// HashPassword hashes a plaintext password using argon2id. func HashPassword(password string) (string, error) { return argon2id.CreateHash(password, argon2id.DefaultParams) } +// CheckPasswordHash reports whether password matches the given argon2id hash. func CheckPasswordHash(password string, hash string) (bool, error) { return argon2id.ComparePasswordAndHash(password, hash) } +// MakeJWT creates a signed HS256 JWT for the given user that expires after +// expiresIn. func MakeJWT(userID uuid.UUID, tokenSecret []byte, expiresIn time.Duration) (string, error) { token := jwt.New(jwt.GetSigningMethod(jwt.SigningMethodHS256.Name)) @@ -34,6 +40,7 @@ func MakeJWT(userID uuid.UUID, tokenSecret []byte, expiresIn time.Duration) (str return token.SignedString(tokenSecret) } +// ValidateJWT verifies the token signature and returns the subject's user ID. func ValidateJWT(tokenString string, tokenSecret []byte) (uuid.UUID, error) { token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(t *jwt.Token) (any, error) { return tokenSecret, nil @@ -58,6 +65,7 @@ func ValidateJWT(tokenString string, tokenSecret []byte) (uuid.UUID, error) { return validUserId, nil } +// GetBearerToken extracts the "Bearer" token from the Authorization header. func GetBearerToken(headers http.Header) (string, error) { authorizationHeader := headers.Get("Authorization") @@ -70,6 +78,8 @@ func GetBearerToken(headers http.Header) (string, error) { return token, nil } +// GetAPIKey extracts the API key from the Authorization header using the +// "ApiKey" scheme. func GetAPIKey(headers http.Header) (string, error) { authorizationHeader := headers.Get("Authorization") @@ -82,6 +92,8 @@ func GetAPIKey(headers http.Header) (string, error) { return apiKey, nil } +// MakeRefreshToken generates a cryptographically random, hex-encoded refresh +// token. func MakeRefreshToken() string { bytes := make([]byte, 32) rand.Read(bytes)