first commit
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
# 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`)
|
||||
Reference in New Issue
Block a user