package application import ( "context" "github.com/go-chi/chi/v5" "github.com/google/uuid" "code.nochebuena.dev/go/httpserver" "code.nochebuena.dev/go/httpmw" "code.nochebuena.dev/go/launcher" "code.nochebuena.dev/go/logz" "code.nochebuena.dev/go/sqlite" "code.nochebuena.dev/go/valid" "code.nochebuena.dev/go/todo-api/internal/domain" "code.nochebuena.dev/go/todo-api/internal/handler" appMW "code.nochebuena.dev/go/todo-api/internal/middleware" "code.nochebuena.dev/go/todo-api/internal/repository" "code.nochebuena.dev/go/todo-api/internal/service" ) const migrate = ` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS user_role ( user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, resource TEXT NOT NULL, permissions INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (user_id, resource) ); CREATE TABLE IF NOT EXISTS todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, done BOOLEAN NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP );` // logAdapter bridges logz.Logger (With returns logz.Logger) to httpmw.Logger // (With must return httpmw.Logger). Defined locally — no extra package needed. type logAdapter struct{ l logz.Logger } func (a *logAdapter) Info(msg string, args ...any) { a.l.Info(msg, args...) } func (a *logAdapter) Error(msg string, err error, args ...any) { a.l.Error(msg, err, args...) } func (a *logAdapter) With(args ...any) httpmw.Logger { return &logAdapter{a.l.With(args...)} } // Run wires all dependencies, starts the launcher, and blocks until shutdown. func Run() error { logger := logz.New(logz.Options{JSON: false}) // --- Infrastructure --- db := sqlite.New(logger, sqlite.Config{Path: "todos.db"}) // --- Repositories --- todoRepo := repository.NewTodoRepository(db) userRepo := repository.NewUserRepository(db) permProvider := repository.NewPermissionProvider(db) // --- Services --- v := valid.New() todoSvc := service.NewTodoService(todoRepo) userSvc := service.NewUserService(userRepo, uuid.NewString) // --- Handlers --- todoH := handler.NewTodoHandler(todoSvc, v) userH := handler.NewUserHandler(userSvc, v) // --- HTTP server --- srv := httpserver.New(logger, httpserver.Config{Port: 3000}, httpserver.WithMiddleware( httpmw.RequestID(uuid.NewString), httpmw.Recover(), httpmw.RequestLogger(&logAdapter{logger}), ), ) // --- Launcher --- lc := launcher.New(logger) lc.Append(db, srv) lc.BeforeStart(func() error { // Migrations run after db.OnInit, before srv.OnStart. ctx := context.Background() if _, err := db.GetExecutor(ctx).ExecContext(ctx, migrate); err != nil { return err } // POST /users — open; no auth required (bootstrapping) srv.Post("/users", userH.Create) // All other routes require a valid X-User-ID (Auth middleware populates rbac.Identity). srv.Group(func(r chi.Router) { r.Use(appMW.Auth(userRepo)) // Todos — permission-gated r.With(appMW.Require(permProvider, domain.ResourceTodos, domain.PermReadTodo)). Get("/todos", todoH.FindAll) r.With(appMW.Require(permProvider, domain.ResourceTodos, domain.PermWriteTodo)). Post("/todos", todoH.Create) // Users — read-only list, also requires read permission on todos // (keeps the surface small; demonstrates RBAC on a second endpoint) r.With(appMW.Require(permProvider, domain.ResourceTodos, domain.PermReadTodo)). Get("/users", userH.FindAll) }) return nil }) return lc.Run() }