118 lines
3.7 KiB
Go
118 lines
3.7 KiB
Go
|
|
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()
|
||
|
|
}
|