Files

157 lines
6.5 KiB
Markdown
Raw Permalink Normal View History

2026-07-16 10:13:46 +03:30
# go-simple-api
A small, heavily-commented Go REST API built as a learning project. It
implements user authentication two ways - email/password and "Sign in with
Google" - using server-side sessions stored in Redis, backed by MySQL for
user data.
This project was built incrementally, lesson by lesson, specifically to
teach Go web-service fundamentals. Every file has generous inline comments
explaining *why* the code is written the way it is, not just what it does.
See [`docs/LESSONS.md`](docs/LESSONS.md) for the full course this project
was built from, and [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for a
deeper explanation of how the pieces fit together.
## Stack
| Concern | Technology |
|---|---|
| HTTP routing | [chi](https://github.com/go-chi/chi) v5 |
| Structured logging | `log/slog` (Go standard library), JSON output |
| Database | MySQL, via `database/sql` + `go-sql-driver/mysql` |
| Sessions | [scs](https://github.com/alexedwards/scs) v2, backed by Redis |
| Password hashing | `golang.org/x/crypto/bcrypt` |
| Google login | `golang.org/x/oauth2` (Authorization Code flow) |
| Rate limiting | [httprate](https://github.com/go-chi/httprate) |
| CORS | [go-chi/cors](https://github.com/go-chi/cors) |
## Project layout
```
go-simple-api/
├── cmd/api/main.go # entrypoint: wires everything, runs the server
├── internal/
│ ├── config/ # env var loading
│ ├── logging/ # structured JSON logger (slog)
│ ├── database/ # MySQL connection + migrations
│ ├── models/ # User struct + UserRepository (all SQL lives here)
│ ├── session/ # scs session manager, backed by Redis
│ ├── oauth/ # Google oauth2.Config builder
│ ├── handlers/ # HTTP handlers (health, auth, google oauth)
│ ├── middleware/ # request logging + auth-guard middleware
│ └── router/ # wires routes + middleware together
├── docs/ # architecture, API reference, course notes
├── Dockerfile # multi-stage build
├── docker-compose.yml # app + MySQL + Redis
├── .env.example # every config variable, documented
└── go.mod
```
## Running locally (without Docker)
Requires Go 1.26+, and MySQL + Redis reachable somewhere.
```bash
# 1. Start MySQL and Redis (or point at existing instances)
docker run --name mysql-api -e MYSQL_ROOT_PASSWORD=devpass \
-e MYSQL_DATABASE=go_simple_api -p 3306:3306 -d mysql:9
docker run --name redis-api -p 6379:6379 -d redis:8
# 2. Set up environment
cp .env.example .env
# edit .env - at minimum, fill in GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET
# if you want to test Google login. Password login works without them.
export $(grep -v '^#' .env | xargs) # or use a tool like direnv
# 3. Fetch dependencies (go.sum is not included - see go.mod for why)
go mod tidy
# 4. Run
go run ./cmd/api
```
The server listens on `:8080` by default. Try:
```bash
curl http://localhost:8080/health
```
## Running with Docker Compose (recommended)
```bash
cp .env.example .env
# fill in GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET if you want Google login
docker compose up --build
```
This starts the API, MySQL, and Redis together, with the API waiting on
the other two. See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for how
service networking works inside Compose.
Stop everything:
```bash
docker compose down # stops containers, keeps the MySQL volume
docker compose down -v # also wipes the MySQL volume (fresh start)
```
## API reference
See [`docs/API.md`](docs/API.md) for every endpoint, request/response
shapes, and example `curl` commands.
Quick overview:
| Method | Path | Auth required? | Purpose |
|---|---|---|---|
| GET | `/health` | no | Liveness check |
| POST | `/register` | no | Create a password-based account |
| POST | `/login` | no | Log in with email + password, starts a session |
| POST | `/logout` | no (needs a session to destroy) | Ends the current session |
| GET | `/me` | **yes** | Returns the currently logged-in user |
| GET | `/auth/google/login` | no | Redirects the browser to Google |
| GET | `/auth/google/callback` | no | Google redirects here after login |
## Google OAuth setup
1. Go to the [Google Cloud Console credentials page](https://console.cloud.google.com/apis/credentials).
2. Create an **OAuth 2.0 Client ID** (Application type: Web application).
3. Add an **Authorized redirect URI**: `http://localhost:8080/auth/google/callback`
(must match `GOOGLE_REDIRECT_URL` in your `.env` exactly).
4. Copy the Client ID and Client Secret into `.env`.
## Security notes
This project deliberately implements several production-appropriate
security practices, explained in detail in the code comments where they
appear:
- Passwords are hashed with bcrypt, never stored or logged in plaintext.
- Login returns an identical, generic error for both "no such email" and
"wrong password", to avoid leaking which emails are registered.
- Sessions are server-side (Redis-backed) - the browser only ever holds a
random token, never the actual session data.
- `sessions.RenewToken()` is called on every successful login (password or
Google) to prevent session fixation.
- The session cookie is `HttpOnly` (JS can't read it) and `SameSite=Lax`
(mitigates CSRF); `Secure` is enabled automatically when `ENV=production`.
- The OAuth2 flow uses a random `state` value, checked on callback, to
prevent CSRF against the login flow itself.
- `/login` and `/register` have a much stricter rate limit than the rest
of the API, to slow down credential-stuffing / brute-force attempts.
- CORS is an explicit origin allowlist (`ALLOWED_ORIGINS`), never a
wildcard, since the API uses credentialed (cookie-based) requests.
## What's not included (possible next steps)
- Automated tests (`httptest`, table-driven tests, a mockable repository interface)
- A real migration tool (e.g. `golang-migrate`) instead of `CREATE TABLE IF NOT EXISTS`
- CSRF tokens for a same-origin HTML form frontend (SameSite=Lax already
covers the cookie-based JSON API case)
- Refresh/renewal flow for sessions beyond their fixed 24h lifetime
- Machine-readable error codes in API responses (currently just a message string)
- Shipping logs to Grafana Loki via Grafana Alloy (the JSON log shape
produced by this app is already Alloy/Loki-friendly - see
`internal/logging` and `internal/middleware/request_logger.go`)