diff --git a/README.md b/README.md index 58d7b34..0b6a441 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,18 @@ # Rhythm Backend (Go) -Lean Go API backend structure for ISeeU Tracker. +Lean Go API backend for ISeeU Tracker. + +## Current Project State + +- [x] Go module initialized (`go.mod`) +- [x] Entry point created (`cmd/api/main.go`) +- [x] Env config package created (`internal/config/config.go`) +- [x] `.env` loading added with required DB variables +- [x] Local Postgres service available in `compose.yaml` (dev profile) +- [ ] Database connection package (`internal/db`) not implemented yet +- [ ] Goose migrations folder/files not created yet +- [ ] HTTP router and handlers not implemented yet +- [ ] User DTO/model/repository/service not implemented yet ## General Folder Structure @@ -11,9 +23,9 @@ cmd/ internal/ config/ # env and app config - db/ # postgres/sqlx connection + tx helpers + db/ # postgres/sqlx connection + migrations http/ # router, middleware, handlers - auth/ # JWT, password hashing, auth middleware + auth/ # jwt, password hashing, auth middleware service/ # business logic repository/ # SQLx queries (no ORM) model/ # domain models + request/response DTOs @@ -22,9 +34,69 @@ migrations/ # Goose SQL migrations scripts/ # optional local/dev scripts ``` +## Roadmap Checklist (Do Not Delete) + +### Chapter 1 - Bootstrap and Config + +- [x] Create `cmd/api/main.go` +- [x] Create `internal/config` package +- [x] Load `.env` and validate required DB env vars +- [ ] Add `APP_PORT` env var with default fallback +- [ ] Improve startup logs (without printing secrets) + +### Chapter 2 - Database and Goose + +- [ ] Add `internal/db/postgres.go` with `sqlx` connection +- [ ] Add `internal/db/migrate.go` to run Goose at startup +- [ ] Create `migrations/` directory +- [ ] Create first migration for `users` table +- [ ] Wire migration call into app startup (before HTTP server) +- [ ] Add `goose status` and rollback notes in README + +### Chapter 3 - User Vertical Slice (First Feature) + +- [ ] Add user DTO (`username`, `password`) in `internal/model` +- [ ] Add user DB model (`id`, `username`, `password_hash`, timestamps) +- [ ] Add user repository with SQLx (`CreateUser`, `GetByUsername`) +- [ ] Add user service with bcrypt hashing +- [ ] Add `POST /users/register` handler +- [ ] Add input validation and proper error responses + +### Chapter 4 - HTTP Layer and Health + +- [ ] Add router setup in `internal/http/router.go` +- [ ] Add `GET /health` endpoint +- [ ] Add JSON response helpers +- [ ] Add request logging middleware +- [ ] Add panic recovery middleware + +### Chapter 5 - Security Foundation + +- [ ] Add password hashing and compare helpers +- [ ] Add JWT generation/verification package +- [ ] Add auth middleware for protected routes +- [ ] Add `POST /auth/login` +- [ ] Add `GET /auth/me` + +### Chapter 6 - Developer Experience + +- [ ] Add `Makefile` targets (`run`, `db-up`, `migrate-up`, `migrate-down`) +- [ ] Add graceful shutdown in `main.go` +- [ ] Add structured logging (`log/slog`) +- [ ] Add `.env.example` updates for all required vars +- [ ] Add basic project usage section in README + +### Chapter 7 - Testing + +- [ ] Add unit tests for config package +- [ ] Add unit tests for user service +- [ ] Add repository integration test setup (test DB) +- [ ] Add handler tests for register endpoint +- [ ] Add CI step to run tests + ## Notes - Keep handlers thin, business logic in `service`, SQL in `repository`. - Use `sqlx` for explicit SQL and scan helpers. -- Use `goose` for schema versioning (`up`, `down`, `status`). -- Prefer small packages and avoid over-abstraction early. +- Use `goose` for schema versioning and run migrations automatically at startup. +- Never store plain passwords; always use `password_hash`. diff --git a/internal/config/config.go b/internal/config/config.go index b00695c..fe6d8d7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,9 +1,13 @@ package config import ( - "github.com/joho/godotenv" + "fmt" "log" + "net" + "net/url" "os" + + "github.com/joho/godotenv" ) type Config struct { @@ -12,24 +16,47 @@ type Config struct { DBName string DBUser string DBPassword string + DBSchema string } -func mustEnv(key string) string { +func getEnv(key string) string { v := os.Getenv(key) if v == "" { log.Fatalf("missing required env var: %s", key) } return v } +func getEnvOrDefault(key, default_string string) string { + v := os.Getenv(key) + if v == "" { + return default_string + } + return v +} func Load() Config { _ = godotenv.Load() return Config{ - DBHost: mustEnv("DB_HOST"), - DBPort: mustEnv("DB_PORT"), - DBName: mustEnv("DB_NAME"), - DBUser: mustEnv("DB_USERNAME"), - DBPassword: mustEnv("DB_PASSWORD"), + DBHost: getEnv("DB_HOST"), + DBPort: getEnv("DB_PORT"), + DBName: getEnv("DB_NAME"), + DBUser: getEnv("DB_USERNAME"), + DBPassword: getEnv("DB_PASSWORD"), + DBSchema: getEnvOrDefault("DB_SCHEMA", "public"), } } + +func (cfg Config) DatabaseURL() string { + u := &url.URL{ + Scheme: "postgres", + User: url.UserPassword(cfg.DBUser, cfg.DBPassword), + Host: net.JoinHostPort(cfg.DBHost, cfg.DBPort), + Path: cfg.DBName, + } + q := u.Query() + q.Set("sslmode", "false") + q.Set("search_path", cfg.DBSchema) + u.RawQuery = q.Encode() + return u.String() +} diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 0000000..e69de29