This commit is contained in:
Dmitri 2026-04-16 09:07:21 +02:00
parent 9f69d80906
commit 0ad2f8a2b0
Signed by: kanopo
GPG Key ID: 759ADD40E3132AC7
13 changed files with 9 additions and 333 deletions

38
.gitignore vendored
View File

@ -1,38 +0,0 @@
HELP.md
# Track jOOQ generated sources
!target/generated-sources/jooq/
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
.env

6
Cargo.toml Normal file
View File

@ -0,0 +1,6 @@
[package]
name = "rhythm_backend"
version = "0.1.0"
edition = "2024"
[dependencies]

View File

@ -1,45 +0,0 @@
# ---- STAGE 1: Build ----
# Use an OpenJDK image that matches the version you develop with.
# It contains the JDK, but not Maven.
FROM eclipse-temurin:25-jdk-jammy AS builder
# Set the working directory
WORKDIR /app
# ---- Caching Dependencies ----
# First, copy the files that define the build, including the Maven Wrapper
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
# Make the wrapper executable
RUN chmod +x ./mvnw
# Run a Maven command to download dependencies.
# Since pom.xml and wrapper files rarely change, this layer will be cached by Docker,
# speeding up subsequent builds significantly.
RUN ./mvnw dependency:go-offline
# ---- Building the Application ----
# Now, copy the source code. If only source code changes, the layers above are cached.
COPY src ./src
# Build the application JAR using the Maven Wrapper
RUN ./mvnw clean package -DskipTests -Djooq.codegen.skip=true -Dflyway.skip=true
# ---- STAGE 2: Runtime ----
# Use a lean Eclipse Temurin JRE image for a small and secure final container.
FROM eclipse-temurin:25-jre-jammy
# Set the working directory
WORKDIR /app
# Copy only the built JAR file from the 'builder' stage
COPY --from=builder /app/target/*.jar app.jar
# Expose the application port
EXPOSE 8080
# Command to run the application
ENTRYPOINT ["java", "-jar", "app.jar"]

107
README.md
View File

@ -1,107 +0,0 @@
# Rhythm Backend (Go)
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] DB URL builder added in config (`DatabaseURL`) with schema `search_path`
- [x] DB package scaffold created (`internal/db/`)
- [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
```text
cmd/
api/
main.go # application entrypoint
internal/
config/ # env and app config
db/ # postgres/sqlx connection + migrations
http/ # router, middleware, handlers
auth/ # jwt, password hashing, auth middleware
service/ # business logic
repository/ # SQLx queries (no ORM)
model/ # domain models + request/response DTOs
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
- [x] Add DB URL builder method in config
- [ ] Add `APP_PORT` env var with default fallback
- [ ] Improve startup logs (without printing secrets)
### Chapter 2 - Database and Goose
- [ ] Implement `internal/db/postgres.go` with `sqlx` connection (`pgx` driver)
- [ ] 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 and run migrations automatically at startup.
- Never store plain passwords; always use `password_hash`.
- Keep one shared `*sqlx.DB` pool for the app lifetime; do not open DB per request.
- Pass `context.Context` from handler (`r.Context()`) to service/repository methods.

View File

@ -1,11 +0,0 @@
package main
import (
"git.kanopo.dev/rhythm/rhythm-backend/internal/config"
"log"
)
func main() {
cfg := config.Load()
log.Println(cfg)
}

View File

@ -1,57 +0,0 @@
services:
# api-prod:
# build: .
# restart: unless-stopped
# container_name: qrcode-api
# ports:
# - "8080:8080"
# environment:
# DB_HOST: db-prod
# env_file:
# - ".env"
# depends_on:
# db-prod:
# condition: service_healthy
# profiles:
# - prod
#
# db-prod:
# image: postgres:18.0-alpine
# restart: unless-stopped
# container_name: qrcode-database-prod
# ports:
# - "${DB_PORT:-5432}:5432"
# environment:
# POSTGRES_USER: ${DB_USERNAME}
# POSTGRES_PASSWORD: ${DB_PASSWORD}
# POSTGRES_DB: ${DB_NAME}
# volumes:
# - postgres_data:/var/lib/postgresql/data
# profiles:
# - prod
# healthcheck:
# test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_NAME}"]
# interval: 5s
# timeout: 3s
# retries: 10
db-dev:
image: postgres:18.0-alpine
restart: unless-stopped
container_name: rhythm-db-dev
ports:
- "${DB_PORT:-5432}:5432"
environment:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
profiles:
- dev
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_NAME}"]
interval: 5s
timeout: 3s
retries: 10
volumes:
postgres_data:

View File

@ -1,5 +0,0 @@
DB_USERNAME=user
DB_PASSWORD=password
DB_NAME=rhythm-dev
DB_PORT=5432
DB_HOST=localhost

5
go.mod
View File

@ -1,5 +0,0 @@
module git.kanopo.dev/rhythm/rhythm-backend
go 1.26.2
require github.com/joho/godotenv v1.5.1

2
go.sum
View File

@ -1,2 +0,0 @@
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=

View File

@ -1,61 +0,0 @@
package config
import (
"log"
"net"
"net/url"
"os"
"github.com/joho/godotenv"
)
type Config struct {
DBHost string
DBPort string
DBName string
DBUser string
DBPassword string
DBSchema 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: 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()
}

View File

@ -1 +0,0 @@
package db

View File

@ -1 +0,0 @@
package db

3
src/main.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}