feat(api): projects, sections, tasks, quick add, views and live sync
Completes the MVP surface: project and section CRUD, task CRUD with sub-trees, drag-and-drop ordering, the quick-add endpoint over the natural-language parser, Today and Upcoming, search, sharing with roles, an activity log and a WebSocket for live updates. Permission checks are route middleware rather than a first line in each handler, so an endpoint is guarded by where it sits instead of by the author remembering. Anything the caller may not see answers 404, never 403 — a 403 confirms the thing exists. Visibility is enforced inside the list query, so another user's project cannot be reached by filtering or searching at it. Ordering is a float, so a drop writes one row instead of renumbering the list. When repeated drops into one gap exhaust the precision, the section is spread out again and the move retried — in a separate transaction, since the attempt that discovered the problem rolls back. The connection pool is not 1: a single connection makes the pool a lock, and a query run while an earlier result set is still open then waits for a connection it is itself holding. The store now closes its rows before querying again, and the pool leaves that mistake slow rather than fatal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1bb644f4d1
commit
dff86dd23d
@@ -7,6 +7,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/kristianwind/verdande/internal/auth"
|
||||
"github.com/kristianwind/verdande/internal/store"
|
||||
)
|
||||
@@ -206,3 +208,29 @@ func (s *Server) isOwnOrigin(origin string) bool {
|
||||
// A local dev frontend on another port talks to the API directly.
|
||||
return s.cfg.Dev && strings.HasPrefix(origin, "http://localhost:")
|
||||
}
|
||||
|
||||
// requireProject checks the caller's standing in the {projectID} of the route
|
||||
// before any handler under it runs.
|
||||
//
|
||||
// Doing this as middleware rather than as a first line in each handler is what
|
||||
// makes it hard to forget: a new endpoint added under the route is guarded by
|
||||
// virtue of where it sits, not by the author remembering to add a check.
|
||||
func (s *Server) requireProject(min store.Role) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
projectID := chi.URLParam(r, "projectID")
|
||||
user := userFrom(r.Context())
|
||||
if user == nil || projectID == "" {
|
||||
writeError(w, http.StatusNotFound, CodeNotFound, "not found")
|
||||
return
|
||||
}
|
||||
if _, err := store.RequireProjectRole(r.Context(), s.db, projectID, user.ID, min); err != nil {
|
||||
// 404 rather than 403: a 403 confirms the project exists, which
|
||||
// is what somebody trying ids is hoping to learn.
|
||||
writeError(w, http.StatusNotFound, CodeNotFound, "not found")
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user